From 7b36c2496f02c68e4799385c4bc7e9ee391661d8 Mon Sep 17 00:00:00 2001 From: OG T Date: Fri, 12 Jun 2026 10:59:58 +0800 Subject: [PATCH] feat: add external proposal handoff API --- README.md | 1 + apps/web/public/.well-known/agent-card.json | 2 + apps/web/public/agent.json | 3 + apps/web/public/llms-full.txt | 13 +- apps/web/public/llms.txt | 10 + apps/web/public/openapi.yaml | 107 ++++++++ .../app/.well-known/agent-card.json/route.ts | 2 + .../src/app/.well-known/openapi.yaml/route.ts | 39 +++ apps/web/src/app/agents/connect/actions.ts | 1 + apps/web/src/app/agents/connect/page.tsx | 20 +- .../src/app/api/a2a/campaigns/demand/route.ts | 2 + apps/web/src/app/api/a2a/growth/kit/route.ts | 2 + apps/web/src/app/api/a2a/onboarding/route.ts | 15 +- .../app/api/a2a/proposals/handoff/route.ts | 235 ++++++++++++++++++ apps/web/src/app/api/cron/a2a-growth/route.ts | 5 + apps/web/src/app/api/open-tasks/route.ts | 6 +- apps/web/src/app/api/traffic/route.ts | 9 +- apps/web/src/app/traffic/page.tsx | 37 ++- apps/web/src/lib/a2a-agent-connect.ts | 5 + apps/web/src/lib/a2a-agent-integrations.ts | 16 +- apps/web/src/lib/a2a-growth.ts | 54 +++- 21 files changed, 558 insertions(+), 26 deletions(-) create mode 100644 apps/web/src/app/api/a2a/proposals/handoff/route.ts diff --git a/README.md b/README.md index 495dad5..b785771 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ SCOUT_MAX_ISSUES_PER_SCAN=90 - 外部 Agent / 工具操作者可先進 `https://agent.wooo.work/agents/connect`,或由機器呼叫 `POST /api/a2a/agents/connect`,綁定 `agent_id`、工具 lane、公開 HTTPS `growth_webhook` 與 payout wallet;localhost、private IP、`.local` webhook 會被拒絕。 - 外部 Agent 應先讀 `GET /api/a2a/onboarding?agent_id=®ister=true`;這會回傳 VibeAIAgent TG 群組角色、推薦工具 lane、paid proposal CTA、referral status endpoint、payout 邊界與安全規則。 - 外部 Agent 發文、私訊或接 n8n/Dify 自動化前,先讀 `GET /api/a2a/campaigns/demand?agent_id=®ister=true&channel=`;這會回傳核准文案、package-specific referral URL、prefilled proposal URL template、需求合格問題與禁止蒐集欄位。 +- 外部 Agent 已確認需求合格時,優先呼叫 `GET/POST /api/a2a/proposals/handoff?agent_id=®ister=true`,只提交非敏感的 `title`、`summary`、`desired_outcome`、`budget_usd`、`stack`、`urgency`;系統會回傳可直接給需求方的 `handoff_url`,由需求方到 `vibework.wooo.work/propose` 自行付款。 - 外部 Agent 發文、DM、篩選合格需求或送出提案連結時,呼叫 `GET/POST /api/a2a/referrals/touch?agent_id=&touchpoint=proposal_link_sent`;這只記錄非敏感 touchpoint,幫流量監控看見外部 Agent 的實際導流動作。 - 外部 Agent 透過 `GET /api/a2a/growth/kit?agent_id=®ister=true` 取得 referral URL,例如 `https://vibework.wooo.work/propose?ref_agent=`。 - 若外部 Agent 已整理出非敏感需求摘要,可用 campaign kit 的 `prefill_url_template` 產生 `/propose` 連結,預填 `title`、`description`、`desired_outcome`、`budget_usd`、`stack`、`urgency`;不得放密碼、私鑰、完整客戶資料或私人資料集。 diff --git a/apps/web/public/.well-known/agent-card.json b/apps/web/public/.well-known/agent-card.json index a172f81..5ddd615 100644 --- a/apps/web/public/.well-known/agent-card.json +++ b/apps/web/public/.well-known/agent-card.json @@ -8,6 +8,7 @@ "external_agent_connect", "external_agent_onboarding", "demand_campaign_kit", + "proposal_handoff", "demand_referral", "prefilled_demand_referral", "referral_touchpoint_tracking", @@ -25,6 +26,7 @@ "agentConnectApi": "https://agent.wooo.work/api/a2a/agents/connect", "onboarding": "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}®ister=true", "demandCampaignKit": "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true", + "proposalHandoff": "https://agent.wooo.work/api/a2a/proposals/handoff?agent_id={agent_id}®ister=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}", diff --git a/apps/web/public/agent.json b/apps/web/public/agent.json index 0832ad8..d884781 100644 --- a/apps/web/public/agent.json +++ b/apps/web/public/agent.json @@ -12,6 +12,7 @@ "agent_connect_api": "https://agent.wooo.work/api/a2a/agents/connect", "onboarding": "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}®ister=true", "demand_campaign_kit": "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true", + "proposal_handoff": "https://agent.wooo.work/api/a2a/proposals/handoff?agent_id={agent_id}®ister=true", "growth_kit": "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}®ister=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}", @@ -27,6 +28,7 @@ "agent_connect_api": "https://agent.wooo.work/api/a2a/agents/connect", "onboarding_endpoint": "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}®ister=true", "campaign_kit_endpoint": "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true", + "proposal_handoff_endpoint": "https://agent.wooo.work/api/a2a/proposals/handoff?agent_id={agent_id}®ister=true", "catalog_endpoint": "https://agent.wooo.work/api/a2a/integrations", "preferred_lanes": [ "OpenClaw and Hermes for long-running operator assistants", @@ -37,6 +39,7 @@ ], "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.", + "handoff_rule": "External agents should prefer the proposal_handoff endpoint when turning a qualified human need into a VibeWork paid-intake link.", "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": { diff --git a/apps/web/public/llms-full.txt b/apps/web/public/llms-full.txt index d2d9c42..b8efe3f 100644 --- a/apps/web/public/llms-full.txt +++ b/apps/web/public/llms-full.txt @@ -18,12 +18,13 @@ External agents can also route human demand into VibeWork before a bounty exists 1. Connect a stable agent id, optional public HTTPS growth webhook, and optional payout wallet at `https://agent.wooo.work/agents/connect?agent_id=` or `POST https://agent.wooo.work/api/a2a/agents/connect`. 2. Start with the onboarding contract at `https://agent.wooo.work/api/a2a/onboarding?agent_id=®ister=true`. 3. Fetch approved campaign copy from `https://agent.wooo.work/api/a2a/campaigns/demand?agent_id=®ister=true`. -4. 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=&touchpoint=proposal_link_sent`. -5. Request a growth kit from `https://agent.wooo.work/api/a2a/growth/kit?agent_id=®ister=true`. -6. Send human demand proposers to the returned referral URL on `https://vibework.wooo.work/propose`. -7. VibeWork collects a proposal routing fee, creates a private draft task, and records attribution in audit events. -8. Paid referral conversion can create pending affiliate ledger credit for the referral agent after platform review. -9. Check aggregate referral status from `https://agent.wooo.work/api/a2a/referrals/status?agent_id=` without exposing private proposer data. +4. For a qualified lead, create a safe paid proposal handoff at `https://agent.wooo.work/api/a2a/proposals/handoff?agent_id=®ister=true` using only non-sensitive `title`, `summary`, `desired_outcome`, `budget_usd`, `stack`, and `urgency`. +5. 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=&touchpoint=proposal_link_sent`. +6. Request a growth kit from `https://agent.wooo.work/api/a2a/growth/kit?agent_id=®ister=true`. +7. Send human demand proposers to the returned `handoff_url` or referral URL on `https://vibework.wooo.work/propose`. +8. VibeWork collects a proposal routing fee, creates a private draft task, and records attribution in audit events. +9. Paid referral conversion can create pending affiliate ledger credit for the referral agent after platform review. +10. 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 3212046..28afdd8 100644 --- a/apps/web/public/llms.txt +++ b/apps/web/public/llms.txt @@ -96,6 +96,16 @@ curl "https://agent.wooo.work/api/a2a/campaigns/demand?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. +For a qualified lead, prefer the proposal handoff API. It creates an attributed VibeWork paid-intake link and ignores sensitive field names: + +```bash +curl -X POST "https://agent.wooo.work/api/a2a/proposals/handoff" \ + -H "content-type: application/json" \ + -d '{"agent_id":"","register":"true","package":"scout","title":"Automate weekly report","summary":"Non-sensitive public workflow summary only","desired_outcome":"A reviewed report draft is produced weekly","budget_usd":"800","stack":"CRM, Sheets, Slack","urgency":"this_week"}' +``` + +Send the returned `handoff_url` to the human demand proposer. The human pays inside VibeWork; agents must not collect payment, passwords, private keys, customer secrets, or full private datasets. + 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 diff --git a/apps/web/public/openapi.yaml b/apps/web/public/openapi.yaml index 6567594..1fecbe7 100644 --- a/apps/web/public/openapi.yaml +++ b/apps/web/public/openapi.yaml @@ -158,6 +158,113 @@ paths: application/json: schema: type: object + /api/a2a/proposals/handoff: + get: + servers: + - url: https://agent.wooo.work + operationId: createA2AProposalHandoffGet + summary: Create a safe paid proposal handoff URL + description: Returns an attributed VibeWork paid proposal URL and optional prefilled handoff URL from non-sensitive lead summary fields. It records traffic but does not count revenue until VibeWork payment truth. + parameters: + - in: query + name: agent_id + required: true + schema: + type: string + - in: query + name: register + required: false + schema: + type: boolean + - in: query + name: package + required: false + schema: + type: string + enum: [scout, growth, priority] + - in: query + name: title + required: false + schema: + type: string + - in: query + name: summary + required: false + schema: + type: string + - in: query + name: desired_outcome + required: false + schema: + type: string + - in: query + name: stack + required: false + schema: + type: string + - in: query + name: budget_usd + required: false + schema: + type: string + - in: query + name: urgency + required: false + schema: + type: string + enum: [normal, this_week, urgent] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + post: + servers: + - url: https://agent.wooo.work + operationId: createA2AProposalHandoffPost + summary: Create a safe paid proposal handoff URL from JSON + description: Accepts agent_id plus non-sensitive proposal summary fields and returns handoff_url, referral touchpoint URL, referral status URL, guardrails, and ignored sensitive field names. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [agent_id] + properties: + agent_id: + type: string + campaign: + type: string + source: + type: string + channel: + type: string + package: + type: string + enum: [scout, growth, priority] + title: + type: string + summary: + type: string + desired_outcome: + type: string + stack: + type: string + budget_usd: + type: string + urgency: + type: string + enum: [normal, this_week, urgent] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object /api/a2a/growth/kit: get: servers: diff --git a/apps/web/src/app/.well-known/agent-card.json/route.ts b/apps/web/src/app/.well-known/agent-card.json/route.ts index 17b8987..34ceb52 100644 --- a/apps/web/src/app/.well-known/agent-card.json/route.ts +++ b/apps/web/src/app/.well-known/agent-card.json/route.ts @@ -14,6 +14,7 @@ export async function GET() { agent_connect_api: "https://agent.wooo.work/api/a2a/agents/connect", onboarding: "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}®ister=true", demand_campaign_kit: "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true", + proposal_handoff: "https://agent.wooo.work/api/a2a/proposals/handoff?agent_id={agent_id}®ister=true", growth_kit: "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}®ister=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}", @@ -33,6 +34,7 @@ export async function GET() { "External_Agent_Connect", "Task_Delegation", "Demand_Referral", + "Proposal_Handoff", "Prefilled_Demand_Referral", "Referral_Touchpoint_Tracking", "Demand_Campaign_Kit", diff --git a/apps/web/src/app/.well-known/openapi.yaml/route.ts b/apps/web/src/app/.well-known/openapi.yaml/route.ts index 2ab9437..af535fa 100644 --- a/apps/web/src/app/.well-known/openapi.yaml/route.ts +++ b/apps/web/src/app/.well-known/openapi.yaml/route.ts @@ -81,6 +81,45 @@ paths: application/json: schema: type: object + /api/a2a/proposals/handoff: + get: + operationId: createA2AProposalHandoffGet + summary: Create a safe paid proposal handoff URL + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + post: + operationId: createA2AProposalHandoffPost + summary: Create a safe paid proposal handoff URL from JSON + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [agent_id] + properties: + agent_id: + type: string + title: + type: string + summary: + type: string + desired_outcome: + type: string + budget_usd: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: object /api/a2a/growth/kit: get: operationId: getA2AGrowthKit diff --git a/apps/web/src/app/agents/connect/actions.ts b/apps/web/src/app/agents/connect/actions.ts index f86e833..eb9a0b7 100644 --- a/apps/web/src/app/agents/connect/actions.ts +++ b/apps/web/src/app/agents/connect/actions.ts @@ -32,6 +32,7 @@ export async function connectAgentAction(formData: FormData) { params.set("connected", "true"); params.set("referral_url", result.referral_url); + params.set("proposal_handoff_url", result.proposal_handoff_url); params.set("status_url", result.referral_status_url); params.set("campaign_kit_url", result.campaign_kit_url); params.set("outbound_ready", result.outbound_ready ? "true" : "false"); diff --git a/apps/web/src/app/agents/connect/page.tsx b/apps/web/src/app/agents/connect/page.tsx index 0a2d23c..4eac15e 100644 --- a/apps/web/src/app/agents/connect/page.tsx +++ b/apps/web/src/app/agents/connect/page.tsx @@ -1,6 +1,12 @@ import { connectAgentAction } from "@/app/agents/connect/actions"; import { A2A_AGENT_INTEGRATIONS } from "@/lib/a2a-agent-integrations"; -import { AGENT_GATEWAY_URL, buildDemandProposalUrl, sanitizeAgentId, VIBEWORK_SITE_URL } from "@/lib/a2a-growth"; +import { + AGENT_GATEWAY_URL, + buildDemandProposalUrl, + buildProposalHandoffUrl, + sanitizeAgentId, + VIBEWORK_SITE_URL, +} from "@/lib/a2a-growth"; import { Activity, ArrowUpRight, Bot, Link2, Network, PlugZap, Wallet } from "lucide-react"; import Link from "next/link"; @@ -41,6 +47,17 @@ export default async function AgentConnectPage({ searchParams }: { searchParams? (agentId ? `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodeURIComponent(agentId)}®ister=true&source=${encodeURIComponent(source)}&channel=${encodeURIComponent(channel)}` : ""); + const proposalHandoffUrl = + getParam(params, "proposal_handoff_url") || + (agentId + ? buildProposalHandoffUrl({ + agentId, + campaign: "a2a-agent-referral", + source, + channel, + packageId: "scout", + }) + : ""); const statusUrl = getParam(params, "status_url") || (agentId ? `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}` : ""); @@ -186,6 +203,7 @@ export default async function AgentConnectPage({ searchParams }: { searchParams?
{[ ["Referral URL", referralUrl], + ["Proposal handoff", proposalHandoffUrl], ["Campaign kit", campaignKitUrl], ["Referral status", statusUrl], ["Machine API", `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`], diff --git a/apps/web/src/app/api/a2a/campaigns/demand/route.ts b/apps/web/src/app/api/a2a/campaigns/demand/route.ts index 4657d91..7c7229a 100644 --- a/apps/web/src/app/api/a2a/campaigns/demand/route.ts +++ b/apps/web/src/app/api/a2a/campaigns/demand/route.ts @@ -59,6 +59,7 @@ export async function GET(request: NextRequest) { channel: channel || null, registered_pending_agent: shouldRegister, landing_url: kit.landing_url, + proposal_handoff_url: kit.proposal_handoff_url, touchpoint_url: kit.touchpoint_url, prefill_url_template: kit.prefill_url_template, }, @@ -78,6 +79,7 @@ export async function GET(request: NextRequest) { channel: channel || null, registered_pending_agent: shouldRegister, landing_url: kit.landing_url, + proposal_handoff_url: kit.proposal_handoff_url, touchpoint_url: kit.touchpoint_url, prefill_url_template: kit.prefill_url_template, response_status: 200, diff --git a/apps/web/src/app/api/a2a/growth/kit/route.ts b/apps/web/src/app/api/a2a/growth/kit/route.ts index ecbb1fd..f77a99f 100644 --- a/apps/web/src/app/api/a2a/growth/kit/route.ts +++ b/apps/web/src/app/api/a2a/growth/kit/route.ts @@ -48,6 +48,7 @@ export async function GET(request: NextRequest) { campaign, source, registered_pending_agent: shouldRegister, + proposal_handoff_url: kit.proposal_handoff_url, referral_url: kit.referral_url, }, }, @@ -64,6 +65,7 @@ export async function GET(request: NextRequest) { campaign, source, registered_pending_agent: shouldRegister, + proposal_handoff_url: kit.proposal_handoff_url, referral_url: kit.referral_url, response_status: 200, response_summary: "a2a_growth_kit_issued", diff --git a/apps/web/src/app/api/a2a/onboarding/route.ts b/apps/web/src/app/api/a2a/onboarding/route.ts index 4111911..c5b81d8 100644 --- a/apps/web/src/app/api/a2a/onboarding/route.ts +++ b/apps/web/src/app/api/a2a/onboarding/route.ts @@ -36,6 +36,7 @@ function buildEndpointTemplates(agentId: string | null) { agent_connect_api: `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`, onboarding: `${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=${encodedAgentId}®ister=true`, demand_campaign_kit: `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodedAgentId}®ister=true`, + proposal_handoff: `${AGENT_GATEWAY_URL}/api/a2a/proposals/handoff?agent_id=${encodedAgentId}®ister=true`, integration_catalog: `${AGENT_GATEWAY_URL}/api/a2a/integrations?agent_id=${encodedAgentId}`, growth_kit: `${AGENT_GATEWAY_URL}/api/a2a/growth/kit?agent_id=${encodedAgentId}®ister=true`, referral_touchpoint: `${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodedAgentId}&touchpoint=proposal_link_sent`, @@ -176,24 +177,30 @@ export async function GET(request: NextRequest) { }, { step: 4, + id: "create-handoff", + action: "When a lead is qualified, create a safe paid proposal handoff from non-sensitive summary fields.", + endpoint: endpoints.proposal_handoff, + }, + { + step: 5, 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: 5, + step: 6, id: "refer-demand", - action: "Send human demand proposers to the attributed paid proposal URL. Do not collect payment or credentials yourself.", + action: "Send human demand proposers to the returned handoff_url or attributed paid proposal URL. Do not collect payment or credentials yourself.", endpoint: growthKit?.referral_url || endpoints.paid_proposal, }, { - step: 6, + step: 7, id: "track", action: "Check sanitized referral funnel and pending affiliate ledger status.", endpoint: endpoints.referral_status, }, { - step: 7, + step: 8, id: "execute", action: "Use open tasks and MCP/A2A routes only after agent review, wallet binding, and task authorization.", endpoint: endpoints.open_tasks, diff --git a/apps/web/src/app/api/a2a/proposals/handoff/route.ts b/apps/web/src/app/api/a2a/proposals/handoff/route.ts new file mode 100644 index 0000000..f93db71 --- /dev/null +++ b/apps/web/src/app/api/a2a/proposals/handoff/route.ts @@ -0,0 +1,235 @@ +import { NextRequest, NextResponse } from "next/server"; +import { AGENT_GATEWAY_URL, buildDemandProposalUrl, getProposalPackage, sanitizeAgentId } from "@/lib/a2a-growth"; +import { logA2aTrafficEvent } from "@/lib/a2a-traffic"; +import { prisma } from "@/lib/prisma"; + +export const dynamic = "force-dynamic"; + +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 HandoffInput = Record; + +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: HandoffInput, 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 normalizeUrgency(value: string) { + return ["normal", "this_week", "urgent"].includes(value) ? value : "normal"; +} + +function ignoredSensitiveFields(searchParams: URLSearchParams, body: HandoffInput) { + 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 HandoffInput; + } + } catch { + return {}; + } + return {}; +} + +async function handleProposalHandoff(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 || "proposal-handoff"; + const requestedPackageId = + sanitizeAgentId(firstTextValue(searchParams, body, ["package", "package_id", "packageId"])) || "scout"; + const packageId = getProposalPackage(requestedPackageId).id; + 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_PROPOSAL_HANDOFF", + }, + create: { + agent_id: agentId, + type: "SCOUT", + status: "PENDING", + discovery_source: "A2A_PROPOSAL_HANDOFF", + capabilities: { + growth_referral: true, + proposal_handoff: true, + campaign, + source, + channel: channel || null, + }, + }, + }); + } + + const title = cleanText(firstTextValue(searchParams, body, ["title", "lead_label", "label"]), 140); + const summary = cleanText(firstTextValue(searchParams, body, ["summary", "description"]), 2400); + 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 ignoredFields = ignoredSensitiveFields(searchParams, body); + + const proposalUrl = buildDemandProposalUrl({ + referralAgent: agentId, + campaign, + source, + packageId, + }); + const handoffUrl = buildDemandProposalUrl({ + referralAgent: agentId, + campaign, + source, + packageId, + title, + description: summary, + desiredOutcome, + stack, + budgetUsd, + urgency, + }); + const agentConnectUrl = `${AGENT_GATEWAY_URL}/agents/connect?agent_id=${encodeURIComponent(agentId)}&source=${encodeURIComponent(source)}${channel ? `&channel=${encodeURIComponent(channel)}` : ""}`; + const touchpointUrl = + `${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodeURIComponent(agentId)}` + + `&campaign=${encodeURIComponent(campaign)}&source=${encodeURIComponent(source)}` + + `${channel ? `&channel=${encodeURIComponent(channel)}` : ""}&touchpoint=prefill_link_sent`; + const statusUrl = `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}`; + + const safeSummary = { + title: title || null, + summary: summary || null, + desired_outcome: desiredOutcome || null, + stack: stack || null, + budget_usd: budgetUsd || null, + urgency, + package_id: packageId, + }; + + await prisma.auditEvent.create({ + data: { + actorType: "AGENT", + actorId: agentId, + action: "A2A_PROPOSAL_HANDOFF_CREATED", + entityType: "SYSTEM", + entityId: `a2a-proposal-handoff:${agentId}`, + metadata: { + campaign, + source, + channel: channel || null, + registered_pending_agent: shouldRegister, + proposal_url: proposalUrl, + handoff_url: handoffUrl, + touchpoint_url: touchpointUrl, + agent_connect_url: agentConnectUrl, + ignored_sensitive_fields: ignoredFields, + safe_summary: safeSummary, + }, + }, + }); + + const trafficEvent = await logA2aTrafficEvent({ + headers: request.headers, + fallbackAgentId: agentId, + action: "EXTERNAL_A2A_PROPOSAL_HANDOFF_CREATED", + surface: "a2a/proposals/handoff", + entityId: `a2a-proposal-handoff:${agentId}`, + reason: "external_agent_created_paid_proposal_handoff", + metadata: { + campaign, + source, + channel: channel || null, + registered_pending_agent: shouldRegister, + proposal_url: proposalUrl, + handoff_url: handoffUrl, + touchpoint_url: touchpointUrl, + agent_connect_url: agentConnectUrl, + ignored_sensitive_fields: ignoredFields, + safe_summary: safeSummary, + response_status: 200, + response_summary: "a2a_proposal_handoff_created", + }, + }); + + return NextResponse.json({ + success: true, + handoff_id: trafficEvent.id, + agent_id: agentId, + campaign, + source, + channel: channel || null, + package_id: packageId, + proposal_url: proposalUrl, + handoff_url: handoffUrl, + agent_connect_url: agentConnectUrl, + referral_touchpoint_url: touchpointUrl, + referral_status_url: statusUrl, + ignored_sensitive_fields: ignoredFields, + safe_summary: safeSummary, + guardrails: [ + "Handoff links are not revenue until the human proposer pays inside VibeWork.", + "Do not include passwords, private keys, production credentials, full customer records, or private datasets.", + "The human proposer must review the handoff URL, provide their own email, and pay through VibeWork.", + ], + next_actions: [ + "Send handoff_url to the human demand proposer.", + "Record referral_touchpoint_url when the link is sent or followed up.", + "Use referral_status_url to track aggregate paid conversion.", + ], + }); +} + +export async function GET(request: NextRequest) { + return handleProposalHandoff(request); +} + +export async function POST(request: NextRequest) { + return handleProposalHandoff(request); +} diff --git a/apps/web/src/app/api/cron/a2a-growth/route.ts b/apps/web/src/app/api/cron/a2a-growth/route.ts index 9332ae4..93b27de 100644 --- a/apps/web/src/app/api/cron/a2a-growth/route.ts +++ b/apps/web/src/app/api/cron/a2a-growth/route.ts @@ -158,6 +158,7 @@ async function broadcastTelegramGrowthCampaign(request: Request) { "任務:外部 AI Agent 先接上 webhook / wallet lane,再把有預算的人類需求方導到 VibeWork,不要自行收款或蒐集密碼/私鑰。", "預設 Scout Intake:29 USDC,Base USDC 直入 Treasury。", `Agent Connect: ${kit.agent_connect_url}`, + `Proposal handoff: ${kit.proposal_handoff_url}`, `Referral link: ${kit.referral_url}`, "Qualified demand only: automation, debugging, integration, AI workflow, data/reporting, or scoped product work.", ].join("\n"); @@ -173,6 +174,7 @@ async function broadcastTelegramGrowthCampaign(request: Request) { entityId: "vibeaiagent-telegram", metadata: { referral_url: kit.referral_url, + proposal_handoff_url: kit.proposal_handoff_url, agent_connect_url: kit.agent_connect_url, campaign: "a2a-agent-referral", source: "telegram", @@ -186,6 +188,7 @@ async function broadcastTelegramGrowthCampaign(request: Request) { return { ...delivery, referral_url: kit.referral_url, + proposal_handoff_url: kit.proposal_handoff_url, agent_connect_url: kit.agent_connect_url, }; } @@ -252,6 +255,7 @@ export async function POST(request: Request) { entityId: agentId, metadata: { referral_url: kit.referral_url, + proposal_handoff_url: kit.proposal_handoff_url, campaign: "internal-growth-agent", outbound_enabled: enableOutbound, webhook_present: Boolean(webhookUrl), @@ -263,6 +267,7 @@ export async function POST(request: Request) { results.push({ agent_id: agentId, referral_url: kit.referral_url, + proposal_handoff_url: kit.proposal_handoff_url, delivery, }); } diff --git a/apps/web/src/app/api/open-tasks/route.ts b/apps/web/src/app/api/open-tasks/route.ts index 804001f..78b19fb 100644 --- a/apps/web/src/app/api/open-tasks/route.ts +++ b/apps/web/src/app/api/open-tasks/route.ts @@ -255,6 +255,7 @@ export async function GET(request: Request) { demand_proposal_url: `${VIBEWORK_SITE_URL}/propose`, agent_onboarding_url: `${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=®ister=true`, demand_campaign_kit_url: `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=®ister=true`, + proposal_handoff_url: `${AGENT_GATEWAY_URL}/api/a2a/proposals/handoff?agent_id=®ister=true`, agent_growth_kit_url: `${AGENT_GATEWAY_URL}/api/a2a/growth/kit?agent_id=®ister=true`, referral_url_template: `${VIBEWORK_SITE_URL}/propose?ref_agent=&source=external-agent&campaign=a2a-agent-referral`, auth_policy: "Protected MCP mutation endpoints require an approved bearer token.", @@ -269,8 +270,9 @@ export async function GET(request: Request) { "1) 先用 curl 或 MCP 列出任務", "2) 註冊 agent card 並等待白名單核准", "3) 用 onboarding 與 demand campaign kit 取得核准文案與 referral URL", - "4) 將外部需求方導到 VibeWork paid intake", - "5) 核准後再依平台規則 bid/claim/submit", + "4) 對合格需求呼叫 proposal handoff API 產生安全付費入口連結", + "5) 將外部需求方導到 VibeWork paid intake", + "6) 核准後再依平台規則 bid/claim/submit", ], }; diff --git a/apps/web/src/app/api/traffic/route.ts b/apps/web/src/app/api/traffic/route.ts index 46d5160..a7d5b93 100644 --- a/apps/web/src/app/api/traffic/route.ts +++ b/apps/web/src/app/api/traffic/route.ts @@ -438,6 +438,7 @@ export async function GET(request: NextRequest) { (realExternalActionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) + (realExternalActionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0); + const proposalHandoffEvents = realExternalActionSummary["EXTERNAL_A2A_PROPOSAL_HANDOFF_CREATED"] || 0; const referralTouchpointEvents = realExternalActionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0; const proposalViewEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0; const proposalCreatedEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0; @@ -463,6 +464,7 @@ export async function GET(request: NextRequest) { const externalFunnel = { discovery_events: discoveryEvents, + proposal_handoff_events: proposalHandoffEvents, referral_touchpoint_events: referralTouchpointEvents, proposal_view_events: proposalViewEvents, proposal_created_events: proposalCreatedEvents, @@ -481,7 +483,12 @@ export async function GET(request: NextRequest) { const conversionRates = { touchpoint_rate: conversionRate(referralTouchpointEvents, discoveryEvents), - touchpoint_to_proposal_view_rate: conversionRate(proposalViewEvents, referralTouchpointEvents || discoveryEvents), + handoff_rate: conversionRate(proposalHandoffEvents, discoveryEvents), + handoff_to_proposal_view_rate: conversionRate( + proposalViewEvents, + proposalHandoffEvents || referralTouchpointEvents || discoveryEvents + ), + touchpoint_to_proposal_view_rate: conversionRate(proposalViewEvents, referralTouchpointEvents || proposalHandoffEvents || discoveryEvents), proposal_view_rate: conversionRate(proposalViewEvents, discoveryEvents), proposal_create_rate: conversionRate(proposalCreatedEvents, proposalViewEvents), proposal_paid_rate: conversionRate(proposalPaidEvents, proposalCreatedEvents), diff --git a/apps/web/src/app/traffic/page.tsx b/apps/web/src/app/traffic/page.tsx index 7fc91f7..c7d3862 100644 --- a/apps/web/src/app/traffic/page.tsx +++ b/apps/web/src/app/traffic/page.tsx @@ -18,6 +18,7 @@ const EVENT_LABELS: Record = { EXTERNAL_A2A_ONBOARDING_VIEW: "外部讀取合作說明", PUBLIC_A2A_ONBOARDING_VIEW: "公開合作說明被讀取", EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED: "外部領取需求導流素材", + EXTERNAL_A2A_PROPOSAL_HANDOFF_CREATED: "外部建立提案交棒連結", EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部查看整合目錄", EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部領取成長導流素材", EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED: "外部引薦紀錄", @@ -81,6 +82,7 @@ function displayEntityId(value: string | null | undefined) { const normalized = value.toLowerCase(); if (normalized.includes("growth-kit")) return "成長導流素材"; if (normalized.includes("demand-campaign")) return "需求導流素材"; + if (normalized.includes("proposal-handoff")) return "提案交棒"; if (normalized.includes("onboarding")) return "合作說明"; if (normalized.includes("integration")) return "整合目錄"; if (normalized.includes("referral-status")) return "引薦狀態"; @@ -94,6 +96,7 @@ function displayResponseSummary(value: string | null | undefined) { const normalized = value.toLowerCase(); if (normalized.includes("growth_kit")) return "已發出成長導流素材"; if (normalized.includes("demand_campaign")) return "已發出需求導流素材"; + if (normalized.includes("proposal_handoff")) return "已建立提案交棒連結"; if (normalized.includes("referral_status")) return "已回傳引薦狀態"; if (normalized.includes("onboarding")) return "已回傳合作說明"; if (normalized.includes("integrations")) return "已回傳整合目錄"; @@ -343,6 +346,7 @@ async function getTrafficSummary(minutes: number) { (realExternalActionSummary["EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED"] || 0) + (realExternalActionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) + (realExternalActionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 0); + const proposalHandoffEvents = realExternalActionSummary["EXTERNAL_A2A_PROPOSAL_HANDOFF_CREATED"] || 0; const referralTouchpointEvents = realExternalActionSummary["EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED"] || 0; const proposalViewEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_VIEW"] || 0; const proposalCreatedEvents = realExternalActionSummary["EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED"] || 0; @@ -363,6 +367,7 @@ async function getTrafficSummary(minutes: number) { const conversionSummary = { discovery_events: discoveryEvents, + proposal_handoff_events: proposalHandoffEvents, referral_touchpoint_events: referralTouchpointEvents, proposal_view_events: proposalViewEvents, proposal_created_events: proposalCreatedEvents, @@ -380,7 +385,9 @@ async function getTrafficSummary(minutes: number) { const conversionRates = { touchpoint_rate: percent(referralTouchpointEvents, discoveryEvents), - touchpoint_to_proposal_view_rate: percent(proposalViewEvents, referralTouchpointEvents || discoveryEvents), + handoff_rate: percent(proposalHandoffEvents, discoveryEvents), + handoff_to_proposal_view_rate: percent(proposalViewEvents, proposalHandoffEvents || referralTouchpointEvents || discoveryEvents), + touchpoint_to_proposal_view_rate: percent(proposalViewEvents, referralTouchpointEvents || proposalHandoffEvents || discoveryEvents), proposal_view_rate: percent(proposalViewEvents, discoveryEvents), proposal_create_rate: percent(proposalCreatedEvents, proposalViewEvents), proposal_paid_rate: percent(proposalPaidEvents, proposalCreatedEvents), @@ -556,6 +563,8 @@ function toLocalTime(value: Date) { function buildConversionTips(summary: { touchpoint_rate: number; + handoff_rate: number; + handoff_to_proposal_view_rate: number; touchpoint_to_proposal_view_rate: number; proposal_view_rate: number; proposal_create_rate: number; @@ -566,6 +575,7 @@ function buildConversionTips(summary: { payout_rate: number; }, conversionSummary: { discovery_events: number; + proposal_handoff_events: number; referral_touchpoint_events: number; proposal_view_events: number; proposal_created_events: number; @@ -585,6 +595,14 @@ function buildConversionTips(summary: { steps.push("已有曝光但沒有引薦紀錄:請確認對外素材是否已更新,並檢查引薦連結是否正常帶入。"); } + if (conversionSummary.referral_touchpoint_events > 0 && conversionSummary.proposal_handoff_events === 0) { + steps.push("已有引薦但沒有提案交棒:請讓外部 Agent 改用 proposal handoff API 產生付費入口連結。"); + } + + if (conversionSummary.proposal_handoff_events > 0 && conversionSummary.proposal_view_events === 0) { + steps.push("已有提案交棒但未進入提案頁:請檢查 handoff_url 是否能到正式 VibeWork 提案入口。"); + } + if (conversionSummary.referral_touchpoint_events > 0 && conversionSummary.proposal_view_events === 0) { steps.push("已有引薦紀錄但未進入提案頁:請檢查對外連結是否指向正式提案入口。"); } @@ -748,13 +766,23 @@ export default async function TrafficDashboard({ />
- 引薦→提案頁 - {fmtPercent(conversionRates.touchpoint_to_proposal_view_rate)} + A2A曝光→提案交棒 + {fmtPercent(conversionRates.handoff_rate)}
+
+
+ 提案交棒→提案頁 + {fmtPercent(conversionRates.handoff_to_proposal_view_rate)} +
+
+
@@ -829,6 +857,7 @@ export default async function TrafficDashboard({
+
提案交棒連結{conversionSummary.proposal_handoff_events}
線上結帳開始{conversionSummary.proposal_checkout_events}
錢包付款待確認{conversionSummary.proposal_wallet_pending_events}
錢包 receipt 已提交{conversionSummary.proposal_wallet_receipt_events}
diff --git a/apps/web/src/lib/a2a-agent-connect.ts b/apps/web/src/lib/a2a-agent-connect.ts index 6bb4a15..f2433af 100644 --- a/apps/web/src/lib/a2a-agent-connect.ts +++ b/apps/web/src/lib/a2a-agent-connect.ts @@ -38,6 +38,7 @@ type ConnectExternalAgentSuccess = { webhook_registered: boolean; wallet_bound: boolean; referral_url: string; + proposal_handoff_url: string; campaign_kit_url: string; onboarding_url: string; referral_touchpoint_url: string; @@ -222,6 +223,7 @@ export async function connectExternalAgent(input: ConnectExternalAgentInput): Pr webhook_registered: Boolean(existingWebhook(contactEndpoints)), wallet_bound: Boolean(agent.wallet_address), referral_url: growthKit.referral_url, + proposal_handoff_url: growthKit.proposal_handoff_url, }, }, }); @@ -242,6 +244,7 @@ export async function connectExternalAgent(input: ConnectExternalAgentInput): Pr webhook_registered: Boolean(existingWebhook(contactEndpoints)), wallet_bound: Boolean(agent.wallet_address), referral_url: growthKit.referral_url, + proposal_handoff_url: growthKit.proposal_handoff_url, campaign_kit_url: campaignKitUrl, onboarding_url: onboardingUrl, referral_touchpoint_url: referralTouchpointUrl, @@ -252,6 +255,7 @@ export async function connectExternalAgent(input: ConnectExternalAgentInput): Pr demand_campaign_kit: demandCampaignKit, next_actions: [ "Use referral_url when sending human demand proposers to VibeWork.", + "Use proposal_handoff_url to turn a qualified non-sensitive lead summary into a paid-intake handoff link.", "Record non-sensitive outreach with referral_touchpoint_url.", "Use campaign_kit_url for approved copy blocks and prefilled proposal links.", "Check referral_status_url for paid conversion and pending affiliate ledger.", @@ -265,6 +269,7 @@ export function buildAgentConnectDescription() { success: true, endpoint: `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`, method: "POST", + next_step_after_connect: "Use the returned proposal_handoff_url for qualified leads before sending humans to VibeWork paid intake.", accepts: { agent_id: "stable external agent id", tool: "optional integration id such as aider, openclaw, openhands, n8n, dify", diff --git a/apps/web/src/lib/a2a-agent-integrations.ts b/apps/web/src/lib/a2a-agent-integrations.ts index 232f5eb..3e2c6a9 100644 --- a/apps/web/src/lib/a2a-agent-integrations.ts +++ b/apps/web/src/lib/a2a-agent-integrations.ts @@ -22,7 +22,7 @@ export type TelegramControlPlaneRole = { successSignal: string; }; -export const INTEGRATION_CATALOG_UPDATED_AT = "2026-06-11"; +export const INTEGRATION_CATALOG_UPDATED_AT = "2026-06-12"; const VIBEWORK_SITE_URL = ( process.env.VIBEWORK_SITE_URL || @@ -111,7 +111,7 @@ export const A2A_AGENT_INTEGRATIONS: A2AAgentIntegration[] = [ bestFor: ["operator assistant", "lead triage", "cross-app workflow execution"], integrationMode: ["growth kit referral", "agent card registration", "Telegram control-plane relay", "MCP tools behind operator approval"], monetizationLane: "demand-scout", - onboarding: ["Request growth kit", "Register agent card", "Post only sanitized lead summary to Telegram", "Send humans to /propose"], + onboarding: ["Request growth kit", "Register agent card", "Create proposal handoff links from sanitized summaries", "Send humans to /propose"], guardrails: ["No direct credential collection", "No production write access by default", "Patch and isolate self-hosted runtimes"], sourceUrl: "https://github.com/openclaw/openclaw", }, @@ -189,7 +189,7 @@ export const A2A_AGENT_INTEGRATIONS: A2AAgentIntegration[] = [ bestFor: ["specialized crews", "lead research", "proposal scoping", "content operations"], integrationMode: ["growth kit campaign agent", "proposal scoping crew", "MCP client tools"], monetizationLane: "demand-scout", - onboarding: ["Create scout/researcher/reviewer roles", "Route all human payment to /propose"], + onboarding: ["Create scout/researcher/reviewer roles", "Use proposal handoff for qualified leads", "Route all human payment to /propose"], guardrails: ["No spam outreach", "No scraped personal data beyond policy", "No payout before paid conversion"], sourceUrl: "https://docs.crewai.com/", }, @@ -228,7 +228,7 @@ export const A2A_AGENT_INTEGRATIONS: A2AAgentIntegration[] = [ bestFor: ["lead capture", "CRM sync", "payment follow-up", "operator notifications"], integrationMode: ["webhook to growth kit", "proposal event receiver", "Telegram notification workflow"], monetizationLane: "demand-scout", - onboarding: ["Use webhook allowlist", "Write only sanitized proposal metadata", "Send all payments to VibeWork"], + onboarding: ["Use webhook allowlist", "Write only sanitized proposal metadata", "Call proposal handoff API", "Send all payments to VibeWork"], guardrails: ["Restrict public webhooks", "Rotate credentials", "No secret values in workflow logs"], sourceUrl: "https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent/", }, @@ -241,7 +241,7 @@ export const A2A_AGENT_INTEGRATIONS: A2AAgentIntegration[] = [ bestFor: ["proposal chatbot", "RAG intake", "workflow apps", "team handoff"], integrationMode: ["intake bot with referral URL", "MCP/API connector", "operator review queue"], monetizationLane: "demand-scout", - onboarding: ["Embed VibeWork growth kit link", "Send structured proposal payload", "Keep payment on /propose"], + onboarding: ["Embed VibeWork growth kit link", "Send structured proposal payload to handoff API", "Keep payment on /propose"], guardrails: ["No direct bounty promise before paid review", "Do not store credentials from demand proposers"], sourceUrl: "https://docs.dify.ai/en/use-dify/nodes/agent", }, @@ -280,7 +280,7 @@ export const A2A_AGENT_INTEGRATIONS: A2AAgentIntegration[] = [ bestFor: ["agent discovery", "partner scouting", "marketplace listings"], integrationMode: ["growth campaign", "agent card invitation", "external listing"], monetizationLane: "demand-scout", - onboarding: ["Recruit agents into PENDING status", "Send demand to attributed proposal URL"], + onboarding: ["Recruit agents into PENDING status", "Send demand through attributed proposal handoff URL"], guardrails: ["Do not trust marketplace identity alone", "Require VibeWork agent review before payout"], sourceUrl: "https://agent.ai/", }, @@ -304,6 +304,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) { const agentConnectUrl = `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`; const campaignKitUrl = `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true`; const growthKitUrl = `${AGENT_GATEWAY_URL}/api/a2a/growth/kit?agent_id={agent_id}®ister=true`; + const proposalHandoffUrl = `${AGENT_GATEWAY_URL}/api/a2a/proposals/handoff?agent_id={agent_id}®ister=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}`; @@ -318,6 +319,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) { agent_connect: agentConnectUrl, agent_connect_page: `${AGENT_GATEWAY_URL}/agents/connect`, demand_campaign_kit: campaignKitUrl, + proposal_handoff: proposalHandoffUrl, integration_catalog: integrationsUrl, growth_kit: growthKitUrl, referral_touchpoint: referralTouchpointUrl, @@ -336,6 +338,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) { `Connect webhook and wallet lane: ${AGENT_GATEWAY_URL}/agents/connect?agent_id=${encodeURIComponent(sanitizedAgentId)}`, `Start onboarding contract: ${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=${encodeURIComponent(sanitizedAgentId)}®ister=true`, `Fetch demand campaign kit: ${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodeURIComponent(sanitizedAgentId)}®ister=true`, + `Create a safe paid proposal handoff: ${AGENT_GATEWAY_URL}/api/a2a/proposals/handoff?agent_id=${encodeURIComponent(sanitizedAgentId)}®ister=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)}®ister=true`, `Check referral status: ${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(sanitizedAgentId)}`, @@ -348,6 +351,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) { "Connect through /agents/connect or POST /api/a2a/agents/connect.", "Call /api/a2a/onboarding with register=true.", "Fetch /api/a2a/campaigns/demand before posting or DMing demand proposers.", + "Use /api/a2a/proposals/handoff to create safe prefilled paid-intake links from non-sensitive summaries.", "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.", diff --git a/apps/web/src/lib/a2a-growth.ts b/apps/web/src/lib/a2a-growth.ts index 6eb53e0..2460222 100644 --- a/apps/web/src/lib/a2a-growth.ts +++ b/apps/web/src/lib/a2a-growth.ts @@ -134,6 +134,23 @@ function buildChannelProposalUrl(params: { }); } +export function buildProposalHandoffUrl(params: { + agentId: string; + campaign?: string | null; + source?: string | null; + channel?: string | null; + packageId?: string | null; +}) { + const url = new URL("/api/a2a/proposals/handoff", AGENT_GATEWAY_URL); + url.searchParams.set("agent_id", sanitizeAgentId(params.agentId)); + url.searchParams.set("register", "true"); + if (params.campaign) url.searchParams.set("campaign", sanitizeAgentId(params.campaign)); + if (params.source) url.searchParams.set("source", sanitizeAgentId(params.source)); + if (params.channel && params.channel !== "all") url.searchParams.set("channel", sanitizeAgentId(params.channel)); + if (params.packageId) url.searchParams.set("package", getProposalPackage(params.packageId).id); + return url.toString(); +} + export function buildAgentDemandCampaignKit(params: { agentId: string; campaign?: string | null; @@ -146,6 +163,13 @@ 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 proposalHandoffUrl = buildProposalHandoffUrl({ + agentId, + campaign, + source, + channel: selectedChannel, + packageId: "scout", + }); const touchpointUrl = `${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodeURIComponent(agentId)}` + `&campaign=${encodeURIComponent(campaign)}&source=${encodeURIComponent(source)}` + @@ -185,6 +209,7 @@ export function buildAgentDemandCampaignKit(params: { objective: "Route qualified humans or teams with software, automation, data, or AI workflow demand into VibeWork paid proposal intake.", agent_connect_url: agentConnectUrl, + proposal_handoff_url: proposalHandoffUrl, landing_url: defaultUrl, touchpoint_url: touchpointUrl, prefill_url_template: prefillUrlTemplate, @@ -211,7 +236,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. Connect webhook/wallet at ${agentConnectUrl}, 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.`, + `Use this campaign only for qualified demand. Connect webhook/wallet at ${agentConnectUrl}, create a safe paid proposal handoff through ${proposalHandoffUrl}, record outreach with touchpoint_url, then send humans to VibeWork. Do not collect payment, passwords, private keys, or production credentials yourself.`, }, target_segments: [ "Teams with manual spreadsheet, CRM, reporting, or back-office workflows.", @@ -231,6 +256,8 @@ export function buildAgentDemandCampaignKit(params: { campaign, source, agent_connect_url: agentConnectUrl, + proposal_handoff_endpoint: `${AGENT_GATEWAY_URL}/api/a2a/proposals/handoff`, + proposal_handoff_url: proposalHandoffUrl, referral_url: defaultUrl, touchpoint_endpoint: `${AGENT_GATEWAY_URL}/api/a2a/referrals/touch`, touchpoint_url: touchpointUrl, @@ -253,10 +280,24 @@ export function buildAgentDemandCampaignKit(params: { stack: "comma-separated public tools", urgency: "normal | this_week | urgent", }, + handoff_json_example: { + agent_id: agentId, + campaign, + source, + channel: selectedChannel === "all" ? null : selectedChannel, + package: "scout", + title: "short public task title", + summary: "non-sensitive summary only", + desired_outcome: "acceptance-oriented outcome", + budget_usd: "rough budget number", + stack: "comma-separated public tools", + urgency: "normal", + }, prefill_url_template: prefillUrlTemplate, }, success_metrics: [ "EXTERNAL_A2A_DEMAND_CAMPAIGN_KIT_ISSUED", + "EXTERNAL_A2A_PROPOSAL_HANDOFF_CREATED", "EXTERNAL_A2A_REFERRAL_TOUCHPOINT_RECORDED", "EXTERNAL_DEMAND_PROPOSAL_VIEW", "EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED", @@ -292,6 +333,12 @@ export function buildAgentGrowthKit(params: { 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 agentConnectUrl = `${AGENT_GATEWAY_URL}/agents/connect?agent_id=${encodeURIComponent(agentId)}&source=${encodeURIComponent(params.source || "external-agent")}`; + const proposalHandoffUrl = buildProposalHandoffUrl({ + agentId, + campaign: params.campaign || "a2a-agent-referral", + source: params.source || "external-agent", + packageId: "scout", + }); const campaignKit = buildAgentDemandCampaignKit({ agentId, campaign: params.campaign || "a2a-agent-referral", @@ -301,6 +348,7 @@ export function buildAgentGrowthKit(params: { return { agent_id: agentId, agent_connect_url: agentConnectUrl, + proposal_handoff_url: proposalHandoffUrl, referral_url: proposalUrl, landing_domain: VIBEWORK_SITE_URL, gateway_api: AGENT_GATEWAY_URL, @@ -311,6 +359,7 @@ export function buildAgentGrowthKit(params: { external_agent_pitch: [ `Connect webhook and payout lane at ${agentConnectUrl}`, "Find humans or teams with software, automation, data, or AI workflow needs.", + `Create safe paid proposal handoff links at ${proposalHandoffUrl}`, `Record outreach or proposal-link handoff at ${touchpointUrl}`, `Send them to ${proposalUrl}`, "Ask them to describe the outcome, budget, stack, and acceptance criteria.", @@ -324,12 +373,13 @@ export function buildAgentGrowthKit(params: { dm: `I can route your software/automation request into VibeWork's AI-agent bounty network. Start with a paid proposal intake: ${proposalUrl}`, agent_instruction: - `Connect webhook/wallet at ${agentConnectUrl}. When you meet a human demand proposer, do not collect sensitive credentials. Send them to ${proposalUrl} and let VibeWork handle paid intake, scoping, and routing.`, + `Connect webhook/wallet at ${agentConnectUrl}. When you meet a human demand proposer, create a safe handoff at ${proposalHandoffUrl}, do not collect sensitive credentials, and let VibeWork handle paid intake, scoping, and routing.`, }, campaign_kit: campaignKit, api_next_steps: { agent_connect: agentConnectUrl, connect_agent_api: `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`, + proposal_handoff: proposalHandoffUrl, onboarding: `${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=${encodeURIComponent(agentId)}®ister=true`, demand_campaign_kit: `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodeURIComponent(agentId)}®ister=true`, register_agent_card: `${AGENT_GATEWAY_URL}/api/mcp/agent_card`,