feat: expose A2A referral status
Some checks failed
CI and Production Smoke / smoke (push) Has been cancelled

This commit is contained in:
OG T
2026-06-11 16:02:20 +08:00
parent 9e366f8954
commit f9385f6acb
9 changed files with 271 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { asA2aAgentActorId, logA2aTrafficEvent } from "@/lib/a2a-traffic";
import { buildDemandProposalUrl, sanitizeAgentId } from "@/lib/a2a-growth";
export const dynamic = "force-dynamic";
const TRACKED_REFERRAL_ACTIONS = [
"EXTERNAL_A2A_GROWTH_KIT_ISSUED",
"EXTERNAL_DEMAND_PROPOSAL_VIEW",
"EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED",
"EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED",
"EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING",
"EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED",
] as const;
type CurrencyBreakdown = Record<string, {
pending_cents: number;
paid_cents: number;
refunded_cents: number;
total_cents: number;
}>;
function emptyCurrencyRow() {
return {
pending_cents: 0,
paid_cents: 0,
refunded_cents: 0,
total_cents: 0,
};
}
function addAffiliateAmount(
breakdown: CurrencyBreakdown,
currency: string,
status: string,
amount: number
) {
const key = currency || "USD";
breakdown[key] ||= emptyCurrencyRow();
breakdown[key].total_cents += amount;
const normalizedStatus = status.toUpperCase();
if (normalizedStatus === "PAID") {
breakdown[key].paid_cents += amount;
} else if (normalizedStatus === "REFUNDED") {
breakdown[key].refunded_cents += amount;
} else {
breakdown[key].pending_cents += amount;
}
}
export async function GET(request: NextRequest) {
const agentId = sanitizeAgentId(request.nextUrl.searchParams.get("agent_id"));
if (!agentId) {
return NextResponse.json({ error: "agent_id is required" }, { status: 400 });
}
const actorId = asA2aAgentActorId(agentId);
const [
agentProfile,
scoutReputation,
referredTaskCount,
referredTasks,
affiliateRows,
actionCountValues,
latestTrafficEvent,
] = await Promise.all([
prisma.agentProfile.findUnique({
where: { agent_id: agentId },
select: {
status: true,
type: true,
wallet_address: true,
discovery_source: true,
created_at: true,
updated_at: true,
},
}),
prisma.scoutReputation.findUnique({
where: { scout_id: agentId },
select: {
successful_conversions: true,
spam_score: true,
chargeback_count: true,
},
}),
prisma.task.count({
where: {
OR: [
{ referred_by_agent: agentId },
{ scout_id: agentId },
],
},
}),
prisma.task.findMany({
where: {
OR: [
{ referred_by_agent: agentId },
{ scout_id: agentId },
],
},
orderBy: { created_at: "desc" },
take: 25,
select: {
id: true,
status: true,
reward_amount: true,
reward_currency: true,
stripe_payment_intent_id: true,
created_at: true,
updated_at: true,
},
}),
prisma.affiliateLedger.findMany({
where: { scout_id: agentId },
orderBy: { created_at: "desc" },
select: {
id: true,
task_id: true,
amount: true,
currency: true,
status: true,
created_at: true,
updated_at: true,
},
}),
Promise.all(
TRACKED_REFERRAL_ACTIONS.map((action) =>
prisma.auditEvent.count({
where: {
actorId,
action,
},
})
)
),
prisma.auditEvent.findMany({
where: {
actorId,
action: { in: [...TRACKED_REFERRAL_ACTIONS] },
},
orderBy: { createdAt: "desc" },
take: 1,
select: {
action: true,
entityId: true,
createdAt: true,
},
}),
]);
const actionCounts = Object.fromEntries(
TRACKED_REFERRAL_ACTIONS.map((action, index) => [action, actionCountValues[index] || 0])
);
const affiliateBreakdown: CurrencyBreakdown = {};
for (const row of affiliateRows) {
addAffiliateAmount(affiliateBreakdown, row.currency, row.status, row.amount);
}
const paidTaskIds = new Set(affiliateRows.map((row) => row.task_id));
const taskById = new Map(referredTasks.map((task) => [task.id, task]));
const recentConversions = affiliateRows.slice(0, 10).map((row) => {
const task = taskById.get(row.task_id);
return {
task_id: row.task_id,
affiliate_ledger_id: row.id,
affiliate_amount_cents: row.amount,
affiliate_currency: row.currency,
affiliate_status: row.status,
proposal_status: task?.status || "UNKNOWN",
payment_method: task?.stripe_payment_intent_id?.startsWith("wallet:") ? "wallet" : "stripe_or_card",
created_at: row.created_at.toISOString(),
updated_at: row.updated_at.toISOString(),
};
});
const latestTrafficAt = latestTrafficEvent[0]?.createdAt || null;
await logA2aTrafficEvent({
headers: request.headers,
fallbackAgentId: agentId,
action: "EXTERNAL_A2A_REFERRAL_STATUS_VIEW",
surface: "a2a/referrals/status",
entityId: `referral-status:${agentId}`,
reason: "external_agent_referral_status_view",
metadata: {
agent_id: agentId,
response_status: 200,
response_summary: "a2a_referral_status_ok",
proposal_count: referredTaskCount,
paid_conversion_count: affiliateRows.length,
pending_affiliate_ledgers: affiliateRows.filter((row) => row.status.toUpperCase() === "PENDING").length,
},
});
return NextResponse.json({
success: true,
agent_id: agentId,
agent_status: agentProfile?.status || "UNREGISTERED",
agent_type: agentProfile?.type || "SCOUT",
has_wallet: Boolean(agentProfile?.wallet_address),
discovery_source: agentProfile?.discovery_source || null,
referral_url: buildDemandProposalUrl({
referralAgent: agentId,
campaign: "a2a-agent-referral",
source: "external-agent",
}),
payout_policy: {
referral_fee: "10% of collected proposal routing fees.",
status: "Affiliate ledger starts PENDING. Payout requires platform review and wallet or Stripe Connect binding.",
payment_truth: "Paid conversion is counted only after Stripe webhook or verified USDC wallet receipt.",
},
traffic_funnel: {
growth_kit_events: actionCounts.EXTERNAL_A2A_GROWTH_KIT_ISSUED || 0,
proposal_view_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_VIEW || 0,
proposal_created_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED || 0,
proposal_checkout_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED || 0,
proposal_wallet_pending_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING || 0,
proposal_paid_events: actionCounts.EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED || 0,
latest_event_at: latestTrafficAt ? latestTrafficAt.toISOString() : null,
},
proposal_summary: {
referred_private_drafts: referredTaskCount,
paid_conversions: paidTaskIds.size,
pending_affiliate_ledgers: affiliateRows.filter((row) => row.status.toUpperCase() === "PENDING").length,
successful_conversions: scoutReputation?.successful_conversions || 0,
spam_score: scoutReputation?.spam_score || 0,
chargeback_count: scoutReputation?.chargeback_count || 0,
},
affiliate_breakdown: affiliateBreakdown,
recent_conversions: recentConversions,
});
}

View File

@@ -11,6 +11,7 @@ const EVENT_LABELS: Record<string, string> = {
EXTERNAL_LIST_OPEN_TASKS: "外部公開流量頁讀取 open tasks",
EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部 Agent 讀取 A2A 整合目錄",
EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部 Agent 領取 growth kit",
EXTERNAL_A2A_REFERRAL_STATUS_VIEW: "外部 Agent 查詢 referral 狀態",
EXTERNAL_DEMAND_PROPOSAL_VIEW: "外部導流需求方查看提案頁",
EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED: "外部導流需求方建立提案",
EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED: "外部導流需求方開始 Stripe 結帳",