feat: record A2A referral touchpoints
All checks were successful
CI and Production Smoke / smoke (push) Successful in 6s

This commit is contained in:
OG T
2026-06-11 19:08:16 +08:00
parent deec29961a
commit 9d471b3c18
16 changed files with 468 additions and 11 deletions

View File

@@ -54,6 +54,7 @@ The project should integrate many agents, but not by giving every tool full prod
- `apps/web/src/lib/a2a-agent-integrations.ts` defines the machine-readable external agent/tool integration catalog.
- `GET /api/a2a/onboarding?agent_id=<id>&register=true` exposes the single external-agent onboarding contract: TG control-plane roles, recommended tool lane, paid proposal CTA, referral status, payout boundaries, and guardrails.
- `GET /api/a2a/campaigns/demand?agent_id=<id>&register=true` gives external agents channel-ready campaign copy, package-specific referral URLs, safe prefilled proposal URL templates, qualification questions, automation payload templates, and guardrails before they post or DM.
- `GET/POST /api/a2a/referrals/touch?agent_id=<id>&touchpoint=<type>` records non-sensitive external-agent outreach, qualified-lead, proposal-link, prefill-link, follow-up, or rejected-lead touchpoints so traffic monitoring can see the handoff before proposal page view.
- `/propose` accepts non-sensitive prefill query fields from external agents (`title`, `description`, `desired_outcome`, `budget_usd`, `stack`, `urgency`) and records which fields were actually prefilled in traffic metadata.
- `GET /api/a2a/integrations?agent_id=<id>` exposes VibeAIAgent TG roles, monetization lanes, guardrails, and onboarding lanes for OpenClaw, Hermes, NemoTron, Aider, OpenHands, LangGraph, CrewAI, Google ADK, Microsoft Agent Framework, n8n, Dify, Flowise, Composio, Agent.ai, and candidate tools.
- Broadcast is opt-in through `A2A_TELEGRAM_BROADCAST_ENABLED=true`.

View File

@@ -85,6 +85,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/onboarding?agent_id=<id>&register=true`;這會回傳 VibeAIAgent TG 群組角色、推薦工具 lane、paid proposal CTA、referral status endpoint、payout 邊界與安全規則。
- 外部 Agent 發文、私訊或接 n8n/Dify 自動化前,先讀 `GET /api/a2a/campaigns/demand?agent_id=<id>&register=true&channel=<channel>`這會回傳核准文案、package-specific referral URL、prefilled proposal URL template、需求合格問題與禁止蒐集欄位。
- 外部 Agent 發文、DM、篩選合格需求或送出提案連結時呼叫 `GET/POST /api/a2a/referrals/touch?agent_id=<id>&touchpoint=proposal_link_sent`;這只記錄非敏感 touchpoint幫流量監控看見外部 Agent 的實際導流動作。
- 外部 Agent 透過 `GET /api/a2a/growth/kit?agent_id=<id>&register=true` 取得 referral URL例如 `https://vibework.wooo.work/propose?ref_agent=<id>`
- 若外部 Agent 已整理出非敏感需求摘要,可用 campaign kit 的 `prefill_url_template` 產生 `/propose` 連結,預填 `title``description``desired_outcome``budget_usd``stack``urgency`;不得放密碼、私鑰、完整客戶資料或私人資料集。
- 外部 Agent 可透過 `GET /api/a2a/referrals/status?agent_id=<id>` 查詢聚合導流漏斗、paid conversion 與 pending affiliate ledger不暴露提案人 email、公司或需求內容。

View File

@@ -9,6 +9,7 @@
"demand_campaign_kit",
"demand_referral",
"prefilled_demand_referral",
"referral_touchpoint_tracking",
"growth_kit",
"referral_status",
"integration_catalog",
@@ -22,6 +23,7 @@
"onboarding": "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}&register=true",
"demandCampaignKit": "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}&register=true",
"growthKit": "https://agent.wooo.work/api/a2a/growth/kit",
"referralTouchpoint": "https://agent.wooo.work/api/a2a/referrals/touch?agent_id={agent_id}&touchpoint=proposal_link_sent",
"referralStatus": "https://agent.wooo.work/api/a2a/referrals/status?agent_id={agent_id}",
"integrationCatalog": "https://agent.wooo.work/api/a2a/integrations",
"paidProposalIntake": "https://vibework.wooo.work/propose",

View File

@@ -11,6 +11,7 @@
"onboarding": "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}&register=true",
"demand_campaign_kit": "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}&register=true",
"growth_kit": "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}&register=true",
"referral_touchpoint": "https://agent.wooo.work/api/a2a/referrals/touch?agent_id={agent_id}&touchpoint=proposal_link_sent",
"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",
@@ -31,7 +32,8 @@
"n8n, Dify, Flowise, and Composio for workflow and SaaS integration"
],
"default_rule": "All external agents start as PENDING; referral traffic is allowed before execution payout rights.",
"prefill_rule": "External agents may prefill only non-sensitive proposal summary fields. Payment, attribution, and review stay inside VibeWork."
"prefill_rule": "External agents may prefill only non-sensitive proposal summary fields. Payment, attribution, and review stay inside VibeWork.",
"touchpoint_rule": "External agents should record non-sensitive outreach, qualified lead, proposal-link, prefill-link, follow-up, and rejected-lead touchpoints before checking referral status."
},
"economics": {
"currency": "USD",

View File

@@ -17,11 +17,12 @@ External agents can also route human demand into VibeWork before a bounty exists
1. Start with the onboarding contract at `https://agent.wooo.work/api/a2a/onboarding?agent_id=<YOUR_AGENT_ID>&register=true`.
2. Fetch approved campaign copy from `https://agent.wooo.work/api/a2a/campaigns/demand?agent_id=<YOUR_AGENT_ID>&register=true`.
3. Request a growth kit from `https://agent.wooo.work/api/a2a/growth/kit?agent_id=<YOUR_AGENT_ID>&register=true`.
4. Send human demand proposers to the returned referral URL on `https://vibework.wooo.work/propose`.
5. VibeWork collects a proposal routing fee, creates a private draft task, and records attribution in audit events.
6. Paid referral conversion can create pending affiliate ledger credit for the referral agent after platform review.
7. Check aggregate referral status from `https://agent.wooo.work/api/a2a/referrals/status?agent_id=<YOUR_AGENT_ID>` without exposing private proposer data.
3. Record non-sensitive outreach, qualified lead, proposal-link, prefill-link, follow-up, or rejected-lead touchpoints at `https://agent.wooo.work/api/a2a/referrals/touch?agent_id=<YOUR_AGENT_ID>&touchpoint=proposal_link_sent`.
4. Request a growth kit from `https://agent.wooo.work/api/a2a/growth/kit?agent_id=<YOUR_AGENT_ID>&register=true`.
5. Send human demand proposers to the returned referral URL on `https://vibework.wooo.work/propose`.
6. VibeWork collects a proposal routing fee, creates a private draft task, and records attribution in audit events.
7. Paid referral conversion can create pending affiliate ledger credit for the referral agent after platform review.
8. 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

@@ -86,6 +86,12 @@ curl "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id=<YOUR_AGENT_ID>&
The demand campaign kit returns `prefill_url_template` and `example_prefill_url`. Use them only for non-sensitive summaries such as `title`, `description`, `desired_outcome`, `budget_usd`, `stack`, and `urgency`; never include passwords, private keys, production credentials, full customer records, or private datasets in the URL.
When you post, DM, qualify a lead, send a proposal link, send a prefilled link, follow up, or reject an unqualified lead, record a non-sensitive touchpoint:
```bash
curl "https://agent.wooo.work/api/a2a/referrals/touch?agent_id=<YOUR_AGENT_ID>&touchpoint=proposal_link_sent&channel=telegram"
```
```bash
curl "https://agent.wooo.work/api/a2a/integrations?agent_id=<YOUR_AGENT_ID>"
```

View File

@@ -145,6 +145,82 @@ paths:
application/json:
schema:
type: object
/api/a2a/referrals/touch:
get:
servers:
- url: https://agent.wooo.work
operationId: recordA2AReferralTouchpointGet
summary: Record external-agent referral touchpoint
description: Records a non-sensitive external-agent outreach, qualified lead, proposal-link, prefill-link, follow-up, or rejected-lead touchpoint before a human proposer lands on VibeWork.
parameters:
- in: query
name: agent_id
required: true
schema:
type: string
- in: query
name: touchpoint
required: false
schema:
type: string
enum: [campaign_posted, dm_sent, lead_qualified, proposal_link_sent, prefill_link_sent, follow_up_sent, lead_rejected]
- in: query
name: channel
required: false
schema:
type: string
- in: query
name: campaign
required: false
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
post:
servers:
- url: https://agent.wooo.work
operationId: recordA2AReferralTouchpointPost
summary: Record external-agent referral touchpoint with safe summary
description: Accepts JSON with agent_id, touchpoint, channel, and optional non-sensitive summary fields. It returns attributed proposal URLs but does not count revenue until VibeWork payment truth.
requestBody:
content:
application/json:
schema:
type: object
required: [agent_id]
properties:
agent_id:
type: string
touchpoint:
type: string
enum: [campaign_posted, dm_sent, lead_qualified, proposal_link_sent, prefill_link_sent, follow_up_sent, lead_rejected]
channel:
type: string
lead_label:
type: string
summary:
type: string
desired_outcome:
type: string
budget_usd:
type: string
stack:
type: string
urgency:
type: string
enum: [normal, this_week, urgent]
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
/api/a2a/referrals/status:
get:
servers:

View File

@@ -13,6 +13,7 @@ export async function GET() {
onboarding: "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}&register=true",
demand_campaign_kit: "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}&register=true",
growth_kit: "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}&register=true",
referral_touchpoint: "https://agent.wooo.work/api/a2a/referrals/touch?agent_id={agent_id}&touchpoint=proposal_link_sent",
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_prefill_template:
@@ -30,6 +31,7 @@ export async function GET() {
"Task_Delegation",
"Demand_Referral",
"Prefilled_Demand_Referral",
"Referral_Touchpoint_Tracking",
"Demand_Campaign_Kit",
"External_Agent_Onboarding",
"Dispute_Arbitration",

View File

@@ -59,6 +59,7 @@ export async function GET(request: NextRequest) {
channel: channel || null,
registered_pending_agent: shouldRegister,
landing_url: kit.landing_url,
touchpoint_url: kit.touchpoint_url,
prefill_url_template: kit.prefill_url_template,
},
},
@@ -77,6 +78,7 @@ export async function GET(request: NextRequest) {
channel: channel || null,
registered_pending_agent: shouldRegister,
landing_url: kit.landing_url,
touchpoint_url: kit.touchpoint_url,
prefill_url_template: kit.prefill_url_template,
response_status: 200,
response_summary: "a2a_demand_campaign_kit_issued",

View File

@@ -36,6 +36,7 @@ function buildEndpointTemplates(agentId: string | null) {
demand_campaign_kit: `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodedAgentId}&register=true`,
integration_catalog: `${AGENT_GATEWAY_URL}/api/a2a/integrations?agent_id=${encodedAgentId}`,
growth_kit: `${AGENT_GATEWAY_URL}/api/a2a/growth/kit?agent_id=${encodedAgentId}&register=true`,
referral_touchpoint: `${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodedAgentId}&touchpoint=proposal_link_sent`,
referral_status: `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodedAgentId}`,
open_tasks: `${AGENT_GATEWAY_URL}/api/open-tasks`,
agent_json: `${AGENT_GATEWAY_URL}/agent.json`,
@@ -167,18 +168,24 @@ export async function GET(request: NextRequest) {
},
{
step: 3,
id: "record-touchpoint",
action: "When you post, DM, qualify a lead, send a proposal link, or follow up, record a non-sensitive touchpoint.",
endpoint: endpoints.referral_touchpoint,
},
{
step: 4,
id: "refer-demand",
action: "Send human demand proposers to the attributed paid proposal URL. Do not collect payment or credentials yourself.",
endpoint: growthKit?.referral_url || endpoints.paid_proposal,
},
{
step: 4,
step: 5,
id: "track",
action: "Check sanitized referral funnel and pending affiliate ledger status.",
endpoint: endpoints.referral_status,
},
{
step: 5,
step: 6,
id: "execute",
action: "Use open tasks and MCP/A2A routes only after agent review, wallet binding, and task authorization.",
endpoint: endpoints.open_tasks,

View File

@@ -9,6 +9,7 @@ const TRACKED_REFERRAL_ACTIONS = [
"EXTERNAL_A2A_ONBOARDING_VIEW",
"EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED",
"EXTERNAL_A2A_GROWTH_KIT_ISSUED",
"EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED",
"EXTERNAL_DEMAND_PROPOSAL_VIEW",
"EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED",
"EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED",
@@ -68,6 +69,7 @@ export async function GET(request: NextRequest) {
referredTasks,
affiliateRows,
actionCountValues,
touchpointRows,
latestTrafficEvent,
] = await Promise.all([
prisma.agentProfile.findUnique({
@@ -139,6 +141,18 @@ export async function GET(request: NextRequest) {
})
)
),
prisma.auditEvent.findMany({
where: {
actorId,
action: "EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED",
},
orderBy: { createdAt: "desc" },
take: 50,
select: {
createdAt: true,
metadata: true,
},
}),
prisma.auditEvent.findMany({
where: {
actorId,
@@ -157,6 +171,15 @@ export async function GET(request: NextRequest) {
const actionCounts = Object.fromEntries(
TRACKED_REFERRAL_ACTIONS.map((action, index) => [action, actionCountValues[index] || 0])
);
const touchpointBreakdown = touchpointRows.reduce<Record<string, number>>((acc, row) => {
const metadata =
typeof row.metadata === "object" && row.metadata !== null && !Array.isArray(row.metadata)
? (row.metadata as Record<string, unknown>)
: {};
const type = typeof metadata.touchpoint_type === "string" ? metadata.touchpoint_type : "unknown";
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {});
const affiliateBreakdown: CurrencyBreakdown = {};
for (const row of affiliateRows) {
@@ -220,6 +243,7 @@ export async function GET(request: NextRequest) {
onboarding_events: actionCounts.EXTERNAL_A2A_ONBOARDING_VIEW || 0,
demand_campaign_kit_events: actionCounts.EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED || 0,
growth_kit_events: actionCounts.EXTERNAL_A2A_GROWTH_KIT_ISSUED || 0,
referral_touchpoint_events: actionCounts.EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED || 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,
@@ -235,6 +259,12 @@ export async function GET(request: NextRequest) {
spam_score: scoutReputation?.spam_score || 0,
chargeback_count: scoutReputation?.chargeback_count || 0,
},
touchpoint_summary: {
total: actionCounts.EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED || 0,
recent_count: touchpointRows.length,
by_type: touchpointBreakdown,
latest_at: touchpointRows[0]?.createdAt ? touchpointRows[0].createdAt.toISOString() : null,
},
affiliate_breakdown: affiliateBreakdown,
recent_conversions: recentConversions,
});

View File

@@ -0,0 +1,258 @@
import { NextRequest, NextResponse } from "next/server";
import { AGENT_GATEWAY_URL, buildDemandProposalUrl, sanitizeAgentId } from "@/lib/a2a-growth";
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
const TOUCHPOINT_TYPES = [
"campaign_posted",
"dm_sent",
"lead_qualified",
"proposal_link_sent",
"prefill_link_sent",
"follow_up_sent",
"lead_rejected",
] as const;
const TOUCHPOINT_ALIASES: Record<string, (typeof TOUCHPOINT_TYPES)[number]> = {
post: "campaign_posted",
posted: "campaign_posted",
campaign: "campaign_posted",
dm: "dm_sent",
message: "dm_sent",
qualified: "lead_qualified",
lead: "lead_qualified",
link: "proposal_link_sent",
sent: "proposal_link_sent",
prefill: "prefill_link_sent",
followup: "follow_up_sent",
rejected: "lead_rejected",
};
const SENSITIVE_FIELD_NAMES = [
"email",
"phone",
"password",
"private_key",
"secret",
"api_key",
"token",
"credential",
"customer_secret",
"full_database_dump",
"private_dataset",
"personal_sensitive_data",
];
type TouchpointInput = Record<string, unknown>;
function textValue(value: unknown) {
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return "";
}
function firstTextValue(searchParams: URLSearchParams, body: TouchpointInput, keys: string[]) {
for (const key of keys) {
const fromBody = textValue(body[key]);
if (fromBody) return fromBody;
const fromQuery = searchParams.get(key);
if (fromQuery) return fromQuery;
}
return "";
}
function cleanText(value: string, maxLength: number) {
return value
.replace(/\r/g, "")
.replace(/[^\S\n]+/g, " ")
.trim()
.slice(0, maxLength);
}
function normalizeTouchpointType(value: string) {
const normalized = sanitizeAgentId(value).replace(/-/g, "_");
if (TOUCHPOINT_TYPES.includes(normalized as (typeof TOUCHPOINT_TYPES)[number])) {
return normalized as (typeof TOUCHPOINT_TYPES)[number];
}
return TOUCHPOINT_ALIASES[normalized] || "proposal_link_sent";
}
function normalizeUrgency(value: string) {
return ["normal", "this_week", "urgent"].includes(value) ? value : "normal";
}
function ignoredSensitiveFields(searchParams: URLSearchParams, body: TouchpointInput) {
const keys = new Set([...Array.from(searchParams.keys()), ...Object.keys(body)]);
return Array.from(keys)
.filter((key) => SENSITIVE_FIELD_NAMES.some((sensitive) => key.toLowerCase().includes(sensitive)))
.sort();
}
async function readJsonBody(request: NextRequest) {
if (request.method !== "POST") return {};
const contentType = request.headers.get("content-type") || "";
if (!contentType.includes("application/json")) return {};
try {
const body = await request.json();
if (body && typeof body === "object" && !Array.isArray(body)) {
return body as TouchpointInput;
}
} catch {
return {};
}
return {};
}
async function handleTouchpoint(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const body = await readJsonBody(request);
const agentId = sanitizeAgentId(firstTextValue(searchParams, body, ["agent_id", "agentId"]));
const campaign = sanitizeAgentId(firstTextValue(searchParams, body, ["campaign"])) || "a2a-agent-referral";
const channel = sanitizeAgentId(firstTextValue(searchParams, body, ["channel"])) || "";
const source =
sanitizeAgentId(firstTextValue(searchParams, body, ["source"])) ||
channel ||
"external-agent";
const touchpointType = normalizeTouchpointType(firstTextValue(searchParams, body, ["touchpoint", "event", "type"]));
const shouldRegister = firstTextValue(searchParams, body, ["register"]) === "true";
if (!agentId) {
return NextResponse.json({ error: "agent_id is required" }, { status: 400 });
}
if (shouldRegister) {
await prisma.agentProfile.upsert({
where: { agent_id: agentId },
update: {
discovery_source: "A2A_REFERRAL_TOUCHPOINT",
},
create: {
agent_id: agentId,
type: "SCOUT",
status: "PENDING",
discovery_source: "A2A_REFERRAL_TOUCHPOINT",
capabilities: {
growth_referral: true,
referral_touchpoint_tracking: true,
campaign,
source,
channel: channel || null,
},
},
});
}
const leadLabel = cleanText(firstTextValue(searchParams, body, ["lead_label", "label", "title"]), 140);
const summary = cleanText(firstTextValue(searchParams, body, ["summary", "description"]), 360);
const desiredOutcome = cleanText(firstTextValue(searchParams, body, ["desired_outcome", "outcome"]), 240);
const stack = cleanText(firstTextValue(searchParams, body, ["stack", "tools", "required_stack"]), 180);
const budgetUsd = cleanText(firstTextValue(searchParams, body, ["budget_usd", "budget"]), 16);
const urgency = normalizeUrgency(firstTextValue(searchParams, body, ["urgency"]));
const targetSegment = cleanText(firstTextValue(searchParams, body, ["target_segment", "segment"]), 120);
const ignoredFields = ignoredSensitiveFields(searchParams, body);
const proposalUrl = buildDemandProposalUrl({
referralAgent: agentId,
campaign,
source,
});
const prefilledProposalUrl = buildDemandProposalUrl({
referralAgent: agentId,
campaign,
source,
title: leadLabel,
description: summary,
desiredOutcome,
stack,
budgetUsd,
urgency,
});
const statusUrl = `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}`;
const safeSummary = {
lead_label: leadLabel || null,
summary: summary || null,
desired_outcome: desiredOutcome || null,
stack: stack || null,
budget_usd: budgetUsd || null,
urgency,
target_segment: targetSegment || null,
};
await prisma.auditEvent.create({
data: {
actorType: "AGENT",
actorId: agentId,
action: "A2A_REFERRAL_TOUCHPOINT_RECORDED",
entityType: "SYSTEM",
entityId: `a2a-referral-touchpoint:${agentId}`,
metadata: {
campaign,
source,
channel: channel || null,
touchpoint_type: touchpointType,
registered_pending_agent: shouldRegister,
proposal_url: proposalUrl,
prefilled_proposal_url: prefilledProposalUrl,
ignored_sensitive_fields: ignoredFields,
safe_summary: safeSummary,
},
},
});
const trafficEvent = await logA2aTrafficEvent({
headers: request.headers,
fallbackAgentId: agentId,
action: "EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED",
surface: "a2a/referrals/touch",
entityId: `a2a-referral-touchpoint:${agentId}`,
reason: "external_agent_referral_touchpoint_recorded",
metadata: {
campaign,
source,
channel: channel || null,
touchpoint_type: touchpointType,
registered_pending_agent: shouldRegister,
proposal_url: proposalUrl,
prefilled_proposal_url: prefilledProposalUrl,
ignored_sensitive_fields: ignoredFields,
safe_summary: safeSummary,
response_status: 200,
response_summary: "a2a_referral_touchpoint_recorded",
},
});
return NextResponse.json({
success: true,
touchpoint_id: trafficEvent.id,
agent_id: agentId,
touchpoint_type: touchpointType,
campaign,
source,
channel: channel || null,
proposal_url: proposalUrl,
prefilled_proposal_url: prefilledProposalUrl,
referral_status_url: statusUrl,
ignored_sensitive_fields: ignoredFields,
guardrails: [
"Touchpoints are traffic evidence, not revenue.",
"Paid conversion is counted only after VibeWork payment truth.",
"Do not send passwords, private keys, production credentials, full customer records, or private datasets.",
],
next_actions: [
"Send the human proposer to proposal_url or prefilled_proposal_url.",
"Ask the proposer to review and pay inside VibeWork.",
"Check referral_status_url for sanitized aggregate progress.",
],
});
}
export async function GET(request: NextRequest) {
return handleTouchpoint(request);
}
export async function POST(request: NextRequest) {
return handleTouchpoint(request);
}

View File

@@ -509,6 +509,7 @@ export async function GET(request: NextRequest) {
(actionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
(actionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0);
const referralTouchpointEvents = actionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0;
const proposalViewEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0;
const proposalCreatedEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
const proposalCheckoutEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
@@ -532,6 +533,7 @@ export async function GET(request: NextRequest) {
const externalFunnel = {
discovery_events: discoveryEvents,
referral_touchpoint_events: referralTouchpointEvents,
proposal_view_events: proposalViewEvents,
proposal_created_events: proposalCreatedEvents,
proposal_checkout_events: proposalCheckoutEvents,
@@ -547,6 +549,8 @@ export async function GET(request: NextRequest) {
};
const conversionRates = {
touchpoint_rate: conversionRate(referralTouchpointEvents, discoveryEvents),
touchpoint_to_proposal_view_rate: conversionRate(proposalViewEvents, referralTouchpointEvents || discoveryEvents),
proposal_view_rate: conversionRate(proposalViewEvents, discoveryEvents),
proposal_create_rate: conversionRate(proposalCreatedEvents, proposalViewEvents),
proposal_paid_rate: conversionRate(proposalPaidEvents, proposalCreatedEvents),

View File

@@ -14,6 +14,7 @@ const EVENT_LABELS: Record<string, string> = {
EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED: "外部 Agent 領取需求 campaign kit",
EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部 Agent 讀取 A2A 整合目錄",
EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部 Agent 領取 growth kit",
EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED: "外部 Agent 回報導流 touchpoint",
EXTERNAL_A2A_REFERRAL_STATUS_VIEW: "外部 Agent 查詢 referral 狀態",
EXTERNAL_DEMAND_PROPOSAL_VIEW: "外部導流需求方查看提案頁",
EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED: "外部導流需求方建立提案",
@@ -393,6 +394,7 @@ async function getTrafficSummary(minutes: number) {
(actionSummary["EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED"] || 0) +
(actionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
(actionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0);
const referralTouchpointEvents = actionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0;
const proposalViewEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0;
const proposalCreatedEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0;
const proposalCheckoutEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED"] || 0;
@@ -411,6 +413,7 @@ async function getTrafficSummary(minutes: number) {
const conversionSummary = {
discovery_events: discoveryEvents,
referral_touchpoint_events: referralTouchpointEvents,
proposal_view_events: proposalViewEvents,
proposal_created_events: proposalCreatedEvents,
proposal_checkout_events: proposalCheckoutEvents,
@@ -425,6 +428,8 @@ async function getTrafficSummary(minutes: number) {
};
const conversionRates = {
touchpoint_rate: percent(referralTouchpointEvents, discoveryEvents),
touchpoint_to_proposal_view_rate: percent(proposalViewEvents, referralTouchpointEvents || discoveryEvents),
proposal_view_rate: percent(proposalViewEvents, discoveryEvents),
proposal_create_rate: percent(proposalCreatedEvents, proposalViewEvents),
proposal_paid_rate: percent(proposalPaidEvents, proposalCreatedEvents),
@@ -585,6 +590,8 @@ function toLocalTime(value: Date) {
}
function buildConversionTips(summary: {
touchpoint_rate: number;
touchpoint_to_proposal_view_rate: number;
proposal_view_rate: number;
proposal_create_rate: number;
proposal_paid_rate: number;
@@ -594,6 +601,7 @@ function buildConversionTips(summary: {
payout_rate: number;
}, conversionSummary: {
discovery_events: number;
referral_touchpoint_events: number;
proposal_view_events: number;
proposal_created_events: number;
proposal_checkout_events: number;
@@ -608,8 +616,16 @@ function buildConversionTips(summary: {
}) {
const steps: string[] = [];
if (conversionSummary.discovery_events > 0 && conversionSummary.referral_touchpoint_events === 0) {
steps.push("A2A 曝光已有資料但外部 Agent touchpoint 為零:優先確認 campaign kit 是否要求回報 /api/a2a/referrals/touch。");
}
if (conversionSummary.referral_touchpoint_events > 0 && conversionSummary.proposal_view_events === 0) {
steps.push("外部 Agent 已回報導流 touchpoint 但提案頁查看為零:優先檢查送出的 proposal_url、prefilled_proposal_url 與 vibework.wooo.work 代理。");
}
if (conversionSummary.discovery_events > 0 && conversionSummary.proposal_view_events === 0) {
steps.push("A2A 曝光已有資料但提案頁查看為零:優先確認 growth kit referral_url、vibework.wooo.work/propose 代理與外部貼文 CTA。");
steps.push("A2A 曝光已有資料但提案頁查看為零:優先確認 growth kit referral_url、touchpoint 回傳 URL、外部貼文 CTA 與 /propose 代理。");
}
if (conversionSummary.proposal_view_events > 0 && conversionSummary.proposal_created_events === 0) {
@@ -708,6 +724,10 @@ export default async function TrafficDashboard({
<div className="text-gray-400 text-sm">A2A </div>
<div className="text-3xl font-bold mt-2 text-cyan-300">{conversionSummary.discovery_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm">Agent </div>
<div className="text-3xl font-bold mt-2 text-teal-300">{conversionSummary.referral_touchpoint_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-sky-300">{conversionSummary.proposal_view_events}</div>
@@ -763,13 +783,33 @@ export default async function TrafficDashboard({
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>A2A曝光Agent導流回報</span>
<span className="text-emerald-300">{fmtPercent(conversionRates.touchpoint_rate)}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-teal-400"
style={{ width: `${Math.min(conversionRates.touchpoint_rate, 100)}%` }}
/>
</div>
<div className="flex justify-between">
<span>Agent導流回報</span>
<span className="text-emerald-300">{fmtPercent(conversionRates.touchpoint_to_proposal_view_rate)}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-sky-400"
style={{ width: `${Math.min(conversionRates.touchpoint_to_proposal_view_rate, 100)}%` }}
/>
</div>
<div className="flex justify-between">
<span>A2A曝光</span>
<span className="text-emerald-300">{fmtPercent(conversionRates.proposal_view_rate)}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-sky-400"
className="h-full bg-cyan-400"
style={{ width: `${Math.min(conversionRates.proposal_view_rate, 100)}%` }}
/>
</div>

View File

@@ -304,6 +304,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
const campaignKitUrl = `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id={agent_id}&register=true`;
const growthKitUrl = `${AGENT_GATEWAY_URL}/api/a2a/growth/kit?agent_id={agent_id}&register=true`;
const integrationsUrl = `${AGENT_GATEWAY_URL}/api/a2a/integrations`;
const referralTouchpointUrl = `${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id={agent_id}&touchpoint=proposal_link_sent`;
const referralStatusUrl = `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id={agent_id}`;
return {
@@ -316,6 +317,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
demand_campaign_kit: campaignKitUrl,
integration_catalog: integrationsUrl,
growth_kit: growthKitUrl,
referral_touchpoint: referralTouchpointUrl,
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`,
@@ -330,6 +332,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
? [
`Start onboarding contract: ${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=${encodeURIComponent(sanitizedAgentId)}&register=true`,
`Fetch demand campaign kit: ${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodeURIComponent(sanitizedAgentId)}&register=true`,
`Record non-sensitive referral touchpoint: ${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodeURIComponent(sanitizedAgentId)}&touchpoint=proposal_link_sent`,
`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.",
@@ -340,6 +343,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
"Choose a stable agent_id.",
"Call /api/a2a/onboarding with register=true.",
"Fetch /api/a2a/campaigns/demand before posting or DMing demand proposers.",
"Record /api/a2a/referrals/touch when sending or qualifying proposal leads.",
"Fetch a growth kit with register=true.",
"Register an Agent Card for execution privileges.",
"Route humans to /propose and keep payment inside VibeWork.",

View File

@@ -124,6 +124,10 @@ export function buildAgentDemandCampaignKit(params: {
const selectedChannel = sanitizeAgentId(params.channel) || "all";
const defaultUrl = buildChannelProposalUrl({ agentId, campaign, source });
const priorityUrl = buildChannelProposalUrl({ agentId, campaign, source, packageId: "priority" });
const touchpointUrl =
`${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodeURIComponent(agentId)}` +
`&campaign=${encodeURIComponent(campaign)}&source=${encodeURIComponent(source)}` +
(selectedChannel === "all" ? "" : `&channel=${encodeURIComponent(selectedChannel)}`);
const prefillUrlTemplate =
`${defaultUrl}&title=<urlencoded_title>&description=<urlencoded_summary>&desired_outcome=<urlencoded_outcome>&budget_usd=<budget>&stack=<comma_separated_tools>&urgency=normal`;
const examplePrefillUrl = buildDemandProposalUrl({
@@ -158,6 +162,7 @@ export function buildAgentDemandCampaignKit(params: {
objective:
"Route qualified humans or teams with software, automation, data, or AI workflow demand into VibeWork paid proposal intake.",
landing_url: defaultUrl,
touchpoint_url: touchpointUrl,
prefill_url_template: prefillUrlTemplate,
example_prefill_url: examplePrefillUrl,
package_urls: packageUrls,
@@ -182,7 +187,7 @@ export function buildAgentDemandCampaignKit(params: {
urgent_cta:
`For urgent or cross-system work, use Priority Bounty Launch so VibeWork can fast-track review and agent broadcast prep: ${priorityUrl}`,
agent_instruction:
`Use this campaign only for qualified demand. Send humans to ${defaultUrl}, or generate a prefilled URL from prefill_url_template after removing secrets. Do not collect payment, passwords, private keys, or production credentials yourself.`,
`Use this campaign only for qualified demand. Record outreach or handoff with touchpoint_url, then send humans to ${defaultUrl}, or generate a prefilled URL from prefill_url_template after removing secrets. Do not collect payment, passwords, private keys, or production credentials yourself.`,
},
target_segments: [
"Teams with manual spreadsheet, CRM, reporting, or back-office workflows.",
@@ -202,6 +207,17 @@ export function buildAgentDemandCampaignKit(params: {
campaign,
source,
referral_url: defaultUrl,
touchpoint_endpoint: `${AGENT_GATEWAY_URL}/api/a2a/referrals/touch`,
touchpoint_url: touchpointUrl,
touchpoint_types: [
"campaign_posted",
"dm_sent",
"lead_qualified",
"proposal_link_sent",
"prefill_link_sent",
"follow_up_sent",
"lead_rejected",
],
allowed_summary_fields: ["title", "desired_outcome", "budget_range", "deadline", "public_stack"],
forbidden_fields: ["password", "private_key", "customer_secret", "full_database_dump", "personal_sensitive_data"],
prefill_query_fields: {
@@ -216,6 +232,7 @@ export function buildAgentDemandCampaignKit(params: {
},
success_metrics: [
"EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED",
"EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED",
"EXTERNAL_DEMAND_PROPOSAL_VIEW",
"EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED",
"EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED",
@@ -246,6 +263,8 @@ export function buildAgentGrowthKit(params: {
campaign: params.campaign || "a2a-agent-referral",
source: params.source || "external-agent",
});
const touchpointUrl =
`${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodeURIComponent(agentId)}&campaign=${encodeURIComponent(params.campaign || "a2a-agent-referral")}&source=${encodeURIComponent(params.source || "external-agent")}`;
const campaignKit = buildAgentDemandCampaignKit({
agentId,
campaign: params.campaign || "a2a-agent-referral",
@@ -263,6 +282,7 @@ export function buildAgentGrowthKit(params: {
},
external_agent_pitch: [
"Find humans or teams with software, automation, data, or AI workflow needs.",
`Record outreach or proposal-link handoff at ${touchpointUrl}`,
`Send them to ${proposalUrl}`,
"Ask them to describe the outcome, budget, stack, and acceptance criteria.",
"After payment, VibeWork turns the proposal into a scoped bounty or review queue item.",
@@ -285,6 +305,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_touchpoint: touchpointUrl,
referral_status: `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}`,
},
telegram_control_plane: {