From f9385f6acbce9f088ba0ee40bf223bd20a5e4f6b Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 11 Jun 2026 16:02:20 +0800 Subject: [PATCH] feat: expose A2A referral status --- README.md | 1 + apps/web/public/agent.json | 1 + apps/web/public/llms-full.txt | 1 + apps/web/public/llms.txt | 6 + apps/web/public/openapi.yaml | 20 ++ .../src/app/api/a2a/referrals/status/route.ts | 237 ++++++++++++++++++ apps/web/src/app/traffic/page.tsx | 1 + apps/web/src/lib/a2a-agent-integrations.ts | 3 + apps/web/src/lib/a2a-growth.ts | 1 + 9 files changed, 271 insertions(+) create mode 100644 apps/web/src/app/api/a2a/referrals/status/route.ts diff --git a/README.md b/README.md index 7b2e49f..0458c6a 100644 --- a/README.md +++ b/README.md @@ -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=®ister=true` 取得 referral URL,例如 `https://vibework.wooo.work/propose?ref_agent=`。 +- 外部 Agent 可透過 `GET /api/a2a/referrals/status?agent_id=` 查詢聚合導流漏斗、paid conversion 與 pending affiliate ledger,不暴露提案人 email、公司或需求內容。 - 外部 Agent / 工具整合目錄可讀 `GET /api/a2a/integrations?agent_id=`;此目錄列出 VibeAIAgent TG 群組職責、OpenClaw/Hermes/NemoTron/Aider/OpenHands/LangGraph/CrewAI/n8n/Dify/Flowise/Composio 等導入 lane、變現觸發條件與安全邊界。 - 需求提案者在 `/propose` 支付 proposal routing fee(Scout 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 流程。 diff --git a/apps/web/public/agent.json b/apps/web/public/agent.json index 00b3f2d..fbdb77a 100644 --- a/apps/web/public/agent.json +++ b/apps/web/public/agent.json @@ -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}®ister=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", diff --git a/apps/web/public/llms-full.txt b/apps/web/public/llms-full.txt index 1e89610..64d4d3b 100644 --- a/apps/web/public/llms-full.txt +++ b/apps/web/public/llms-full.txt @@ -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=` 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. diff --git a/apps/web/public/llms.txt b/apps/web/public/llms.txt index a57cc6a..f898326 100644 --- a/apps/web/public/llms.txt +++ b/apps/web/public/llms.txt @@ -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=" +``` + ## 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. diff --git a/apps/web/public/openapi.yaml b/apps/web/public/openapi.yaml index 56780c0..60b402d 100644 --- a/apps/web/public/openapi.yaml +++ b/apps/web/public/openapi.yaml @@ -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: diff --git a/apps/web/src/app/api/a2a/referrals/status/route.ts b/apps/web/src/app/api/a2a/referrals/status/route.ts new file mode 100644 index 0000000..1896a45 --- /dev/null +++ b/apps/web/src/app/api/a2a/referrals/status/route.ts @@ -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; + +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, + }); +} diff --git a/apps/web/src/app/traffic/page.tsx b/apps/web/src/app/traffic/page.tsx index 0e453ba..ba01473 100644 --- a/apps/web/src/app/traffic/page.tsx +++ b/apps/web/src/app/traffic/page.tsx @@ -11,6 +11,7 @@ const EVENT_LABELS: Record = { 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 結帳", diff --git a/apps/web/src/lib/a2a-agent-integrations.ts b/apps/web/src/lib/a2a-agent-integrations.ts index 111739b..49b9208 100644 --- a/apps/web/src/lib/a2a-agent-integrations.ts +++ b/apps/web/src/lib/a2a-agent-integrations.ts @@ -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}®ister=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)}®ister=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.", diff --git a/apps/web/src/lib/a2a-growth.ts b/apps/web/src/lib/a2a-growth.ts index 5b6ba3a..ff98585 100644 --- a/apps/web/src/lib/a2a-growth.ts +++ b/apps/web/src/lib/a2a-growth.ts @@ -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",