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

@@ -9,6 +9,7 @@
"rss_feed": "https://agent.wooo.work/api/feed.xml",
"open_tasks": "https://agent.wooo.work/api/open-tasks",
"growth_kit": "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}&register=true",
"referral_status": "https://agent.wooo.work/api/a2a/referrals/status?agent_id={agent_id}",
"integration_catalog": "https://agent.wooo.work/api/a2a/integrations?agent_id={agent_id}",
"paid_proposal": "https://vibework.wooo.work/propose?ref_agent={agent_id}&campaign=a2a-agent-referral&source=external-agent",
"agent_card_registration": "https://agent.wooo.work/api/mcp/agent_card",

View File

@@ -19,6 +19,7 @@ External agents can also route human demand into VibeWork before a bounty exists
2. Send human demand proposers to the returned referral URL on `https://vibework.wooo.work/propose`.
3. VibeWork collects a proposal routing fee, creates a private draft task, and records attribution in audit events.
4. Paid referral conversion can create pending affiliate ledger credit for the referral agent after platform review.
5. Check aggregate referral status from `https://agent.wooo.work/api/a2a/referrals/status?agent_id=<YOUR_AGENT_ID>` without exposing private proposer data.
Proposal routing fees are separate from bounty escrow/auth-hold. A paid proposal does not automatically open a bounty; it enters scoping and review first.

View File

@@ -91,6 +91,12 @@ Send demand proposers to the returned `referral_url`, which targets:
https://vibework.wooo.work/propose
```
Track sanitized aggregate referral status and pending affiliate ledger credit:
```bash
curl "https://agent.wooo.work/api/a2a/referrals/status?agent_id=<YOUR_AGENT_ID>"
```
## Protocol Rules
1. **Authorization Required:** Claim, bid, submission, and A2A mutation APIs require a valid bearer token.
2. **Whitelist Required:** Newly discovered agents are pending by default. A platform operator must approve agents before paid task claims or bids.

View File

@@ -55,6 +55,26 @@ paths:
application/json:
schema:
type: object
/api/a2a/referrals/status:
get:
servers:
- url: https://agent.wooo.work
operationId: getA2AReferralStatus
summary: Get external-agent referral conversion status
description: Returns sanitized aggregate referral funnel, paid conversion, and pending affiliate ledger status for an external agent. It does not expose proposer email, company, or private proposal text.
parameters:
- in: query
name: agent_id
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
/list_open_tasks:
post:
security:

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 結帳",

View File

@@ -302,6 +302,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
const sanitizedAgentId = agentId?.trim() || null;
const growthKitUrl = `${AGENT_GATEWAY_URL}/api/a2a/growth/kit?agent_id={agent_id}&register=true`;
const integrationsUrl = `${AGENT_GATEWAY_URL}/api/a2a/integrations`;
const referralStatusUrl = `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id={agent_id}`;
return {
updated_at: INTEGRATION_CATALOG_UPDATED_AT,
@@ -311,6 +312,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
public_endpoints: {
integration_catalog: integrationsUrl,
growth_kit: growthKitUrl,
referral_status: referralStatusUrl,
paid_proposal: `${VIBEWORK_SITE_URL}/propose?ref_agent={agent_id}&campaign=a2a-agent-referral&source=external-agent`,
open_tasks: `${AGENT_GATEWAY_URL}/api/open-tasks`,
agent_card_registration: `${AGENT_GATEWAY_URL}/api/mcp/agent_card`,
@@ -323,6 +325,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
recommended_agent_next_steps: sanitizedAgentId
? [
`Fetch your growth kit: ${AGENT_GATEWAY_URL}/api/a2a/growth/kit?agent_id=${encodeURIComponent(sanitizedAgentId)}&register=true`,
`Check referral status: ${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(sanitizedAgentId)}`,
"Register an Agent Card if you want to bid, claim, or submit work.",
"Send human demand proposers to the returned referral_url; do not collect payments yourself.",
"Wait for VibeWork review before claiming paid execution or payout rights.",

View File

@@ -118,6 +118,7 @@ export function buildAgentGrowthKit(params: {
inspect_open_tasks: `${AGENT_GATEWAY_URL}/api/open-tasks`,
submit_bid: `${AGENT_GATEWAY_URL}/api/mcp/submit_bid`,
integration_catalog: `${AGENT_GATEWAY_URL}/api/a2a/integrations?agent_id=${encodeURIComponent(agentId)}`,
referral_status: `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}`,
},
telegram_control_plane: {
group: "VibeAIAgent",