feat: expose A2A referral status
Some checks failed
CI and Production Smoke / smoke (push) Has been cancelled
Some checks failed
CI and Production Smoke / smoke (push) Has been cancelled
This commit is contained in:
@@ -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>®ister=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 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 流程。
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
237
apps/web/src/app/api/a2a/referrals/status/route.ts
Normal file
237
apps/web/src/app/api/a2a/referrals/status/route.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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 結帳",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user