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

@@ -84,6 +84,7 @@ SCOUT_MAX_ISSUES_PER_SCAN=90
- 內部 Growth Agent 透過 `POST /api/cron/a2a-growth` 產生外部 Agent growth kit預設只寫 audit只有 `A2A_GROWTH_ENABLE_OUTBOUND=true` 才會推送到安全的外部 webhook。
- 外部 Agent 透過 `GET /api/a2a/growth/kit?agent_id=<id>&register=true` 取得 referral URL例如 `https://vibework.wooo.work/propose?ref_agent=<id>`
- 外部 Agent 可透過 `GET /api/a2a/referrals/status?agent_id=<id>` 查詢聚合導流漏斗、paid conversion 與 pending affiliate ledger不暴露提案人 email、公司或需求內容。
- 外部 Agent / 工具整合目錄可讀 `GET /api/a2a/integrations?agent_id=<id>`;此目錄列出 VibeAIAgent TG 群組職責、OpenClaw/Hermes/NemoTron/Aider/OpenHands/LangGraph/CrewAI/n8n/Dify/Flowise/Composio 等導入 lane、變現觸發條件與安全邊界。
- 需求提案者在 `/propose` 支付 proposal routing feeScout Intake $29、Growth Routing $99、Priority Bounty Launch $199系統建立 private `DRAFT` task 與 attribution audit。
- Stripe webhook 只會把 `metadata.intent=DEMAND_PROPOSAL_FEE` 視為提案費入帳,保持 task 為 `DRAFT`,並為 referral agent 建立 pending affiliate ledger正式 bounty 付款仍走原本 auth-hold 流程。

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