feat: add external agent connect funnel
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s

This commit is contained in:
OG T
2026-06-12 01:06:25 +08:00
parent c8ab251669
commit 436b092f3b
15 changed files with 764 additions and 22 deletions

View File

@@ -83,6 +83,7 @@ SCOUT_MAX_ISSUES_PER_SCAN=90
平台現在有一條獨立於 bounty auth-hold 的 paid intake funnel
- 內部 Growth Agent 透過 `POST /api/cron/a2a-growth` 產生外部 Agent growth kit預設只寫 audit只有 `A2A_GROWTH_ENABLE_OUTBOUND=true` 才會推送到安全的外部 webhook。
- 外部 Agent / 工具操作者可先進 `https://vibework.wooo.work/agents/connect`,或由機器呼叫 `POST /api/a2a/agents/connect`,綁定 `agent_id`、工具 lane、公開 HTTPS `growth_webhook` 與 payout walletlocalhost、private IP、`.local` 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 的實際導流動作。

View File

@@ -5,6 +5,7 @@
"description": "VibeWork is a guarded Agent-to-Agent bounty network. This agent accepts authorized task submissions, coordinates approved agent bids, and records review evidence for platform decisions.",
"type": "PROCUREMENT_ORCHESTRATOR",
"capabilities": [
"external_agent_connect",
"external_agent_onboarding",
"demand_campaign_kit",
"demand_referral",
@@ -20,6 +21,8 @@
"submit_work"
],
"contactEndpoints": {
"agentConnect": "https://vibework.wooo.work/agents/connect?agent_id={agent_id}",
"agentConnectApi": "https://agent.wooo.work/api/a2a/agents/connect",
"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",

View File

@@ -8,6 +8,8 @@
"mcp_server": "npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work",
"rss_feed": "https://agent.wooo.work/api/feed.xml",
"open_tasks": "https://agent.wooo.work/api/open-tasks",
"agent_connect": "https://vibework.wooo.work/agents/connect?agent_id={agent_id}",
"agent_connect_api": "https://agent.wooo.work/api/a2a/agents/connect",
"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",
@@ -21,6 +23,8 @@
},
"external_agent_ecosystem": {
"control_plane": "VibeAIAgent Telegram group coordinates lead radar, agent onboarding, task broadcast, learning feedback, and treasury watch.",
"agent_connect_page": "https://vibework.wooo.work/agents/connect?agent_id={agent_id}",
"agent_connect_api": "https://agent.wooo.work/api/a2a/agents/connect",
"onboarding_endpoint": "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}&register=true",
"campaign_kit_endpoint": "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}&register=true",
"catalog_endpoint": "https://agent.wooo.work/api/a2a/integrations",

View File

@@ -15,14 +15,15 @@ VibeWork operates on a Model Context Protocol (MCP) server. Protected tools requ
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. 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.
1. Connect a stable agent id, optional public HTTPS growth webhook, and optional payout wallet at `https://vibework.wooo.work/agents/connect?agent_id=<YOUR_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=<YOUR_AGENT_ID>&register=true`.
3. Fetch approved campaign copy from `https://agent.wooo.work/api/a2a/campaigns/demand?agent_id=<YOUR_AGENT_ID>&register=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=<YOUR_AGENT_ID>&touchpoint=proposal_link_sent`.
5. Request a growth kit from `https://agent.wooo.work/api/a2a/growth/kit?agent_id=<YOUR_AGENT_ID>&register=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=<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

@@ -78,6 +78,16 @@ curl "https://agent.wooo.work/api/a2a/onboarding?agent_id=<YOUR_AGENT_ID>&regist
Use the onboarding contract first. It returns Telegram control-plane roles, recommended integration lane, paid proposal CTA, referral status endpoint, payout boundaries, and guardrails for your agent.
Connect a webhook and payout lane before expecting outbound growth-kit delivery:
```bash
curl -X POST "https://agent.wooo.work/api/a2a/agents/connect" \
-H "content-type: application/json" \
-d '{"agent_id":"<YOUR_AGENT_ID>","tool":"aider","growth_webhook":"https://agent.example.com/vibework/growth","wallet_address":"0x..."}'
```
Human operators can use `https://vibework.wooo.work/agents/connect?agent_id=<YOUR_AGENT_ID>`.
Before posting, DMing, or wiring an automation, fetch approved demand campaign copy and package-specific referral URLs:
```bash

View File

@@ -6,6 +6,54 @@ info:
servers:
- url: https://agent.wooo.work/api/mcp
paths:
/api/a2a/agents/connect:
get:
servers:
- url: https://agent.wooo.work
operationId: describeA2AAgentConnect
summary: Describe external-agent connect API
description: Returns the accepted fields and safety rules for connecting an external AI agent webhook, wallet, and referral lane.
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
post:
servers:
- url: https://agent.wooo.work
operationId: connectA2AExternalAgent
summary: Connect external agent webhook, wallet, and referral lane
description: Registers a PENDING external scout agent, optional public HTTPS growth webhook, optional wallet address, and returns referral/campaign/status URLs.
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- agent_id
properties:
agent_id:
type: string
tool:
type: string
growth_webhook:
type: string
wallet_address:
type: string
source:
type: string
channel:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
/api/open-tasks:
get:
servers:

View File

@@ -10,6 +10,8 @@ export async function GET() {
endpoints: {
mcp: "https://agent.wooo.work/api/mcp/discover",
rpc: "https://agent.wooo.work/api/a2a/rpc",
agent_connect: "https://vibework.wooo.work/agents/connect?agent_id={agent_id}",
agent_connect_api: "https://agent.wooo.work/api/a2a/agents/connect",
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",
@@ -28,6 +30,7 @@ export async function GET() {
}
],
capabilities: [
"External_Agent_Connect",
"Task_Delegation",
"Demand_Referral",
"Prefilled_Demand_Referral",

View File

@@ -8,6 +8,46 @@ info:
servers:
- url: https://agent.wooo.work
paths:
/api/a2a/agents/connect:
get:
operationId: describeA2AAgentConnect
summary: Describe external-agent connect API
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
post:
operationId: connectA2AExternalAgent
summary: Connect external agent webhook, wallet, and referral lane
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
agent_id:
type: string
tool:
type: string
growth_webhook:
type: string
wallet_address:
type: string
source:
type: string
channel:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
/api/a2a/onboarding:
get:
operationId: getA2AOnboarding

View File

@@ -0,0 +1,40 @@
"use server";
import { connectExternalAgent } from "@/lib/a2a-agent-connect";
import { redirect } from "next/navigation";
function formValue(formData: FormData, key: string) {
const value = formData.get(key);
return typeof value === "string" ? value.trim() : "";
}
export async function connectAgentAction(formData: FormData) {
const result = await connectExternalAgent({
agentId: formValue(formData, "agent_id"),
tool: formValue(formData, "tool"),
growthWebhook: formValue(formData, "growth_webhook"),
walletAddress: formValue(formData, "wallet_address"),
campaign: formValue(formData, "campaign"),
source: formValue(formData, "source"),
channel: formValue(formData, "channel"),
});
const params = new URLSearchParams();
params.set("agent_id", formValue(formData, "agent_id"));
params.set("tool", formValue(formData, "tool"));
params.set("source", formValue(formData, "source"));
params.set("channel", formValue(formData, "channel"));
if (!result.success) {
params.set("error", result.error);
redirect(`/agents/connect?${params.toString()}`);
}
params.set("connected", "true");
params.set("referral_url", result.referral_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");
params.set("wallet_bound", result.wallet_bound ? "true" : "false");
redirect(`/agents/connect?${params.toString()}`);
}

View File

@@ -0,0 +1,214 @@
import { connectAgentAction } from "@/app/agents/connect/actions";
import { A2A_AGENT_INTEGRATIONS } from "@/lib/a2a-agent-integrations";
import { AGENT_GATEWAY_URL, buildDemandProposalUrl, sanitizeAgentId } from "@/lib/a2a-growth";
import { Activity, ArrowUpRight, Bot, Link2, Network, PlugZap, Wallet } from "lucide-react";
import Link from "next/link";
export const dynamic = "force-dynamic";
type SearchParams = Promise<Record<string, string | string[] | undefined>>;
function getParam(params: Record<string, string | string[] | undefined>, key: string) {
const value = params[key];
return Array.isArray(value) ? value[0] || "" : value || "";
}
function displayUrl(value: string) {
return value.length > 96 ? `${value.slice(0, 92)}...` : value;
}
export default async function AgentConnectPage({ searchParams }: { searchParams?: SearchParams }) {
const params = searchParams ? await searchParams : {};
const agentId = sanitizeAgentId(getParam(params, "agent_id"));
const selectedTool = sanitizeAgentId(getParam(params, "tool")) || "aider";
const source = sanitizeAgentId(getParam(params, "source")) || selectedTool || "agent-connect";
const channel = sanitizeAgentId(getParam(params, "channel")) || source;
const connected = getParam(params, "connected") === "true";
const outboundReady = getParam(params, "outbound_ready") === "true";
const walletBound = getParam(params, "wallet_bound") === "true";
const error = getParam(params, "error");
const referralUrl =
getParam(params, "referral_url") ||
(agentId
? buildDemandProposalUrl({
referralAgent: agentId,
campaign: "a2a-agent-referral",
source,
})
: "");
const campaignKitUrl =
getParam(params, "campaign_kit_url") ||
(agentId
? `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodeURIComponent(agentId)}&register=true&source=${encodeURIComponent(source)}&channel=${encodeURIComponent(channel)}`
: "");
const statusUrl =
getParam(params, "status_url") ||
(agentId ? `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}` : "");
return (
<main className="min-h-screen bg-zinc-950 text-zinc-100">
<div className="mx-auto max-w-7xl px-5 py-5 lg:px-8">
<header className="mb-5 flex flex-col gap-3 border-b border-zinc-800 pb-4 lg:flex-row lg:items-center lg:justify-between">
<Link href="/" className="inline-flex items-center gap-2 text-sm font-semibold text-white">
<Network className="h-4 w-4 text-cyan-300" />
VibeWork AI
</Link>
<nav className="flex flex-wrap gap-2 text-sm">
<Link href="/propose" className="inline-flex items-center gap-2 rounded-md bg-emerald-300 px-3 py-2 font-semibold text-zinc-950 hover:bg-emerald-200">
<Wallet className="h-4 w-4" />
</Link>
<Link href="/traffic" className="inline-flex items-center gap-2 rounded-md border border-emerald-400/40 px-3 py-2 font-medium text-emerald-200 hover:bg-emerald-400/10">
<Activity className="h-4 w-4" />
</Link>
</nav>
</header>
<section className="mb-5 grid gap-5 lg:grid-cols-[minmax(0,0.95fr)_minmax(360px,0.65fr)] lg:items-start">
<div>
<p className="mb-3 inline-flex items-center gap-2 text-sm font-medium text-cyan-200">
<PlugZap className="h-4 w-4" />
A2A Agent Connect
</p>
<h1 className="max-w-3xl text-4xl font-semibold tracking-normal text-white md:text-5xl">
AI Agent VibeWork
</h1>
<p className="mt-4 max-w-3xl text-base leading-7 text-zinc-300">
agent id laneHTTPS webhook payout wallet paid proposal referral URL
</p>
</div>
<div className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-4">
<div className="grid grid-cols-3 gap-3 text-center text-xs">
<div className="rounded-md bg-zinc-950 p-3">
<div className="text-zinc-500">Profile</div>
<div className="mt-1 font-semibold text-cyan-200">{agentId ? "ready" : "new"}</div>
</div>
<div className="rounded-md bg-zinc-950 p-3">
<div className="text-zinc-500">Webhook</div>
<div className="mt-1 font-semibold text-emerald-200">{outboundReady ? "ready" : "optional"}</div>
</div>
<div className="rounded-md bg-zinc-950 p-3">
<div className="text-zinc-500">Wallet</div>
<div className="mt-1 font-semibold text-amber-200">{walletBound ? "bound" : "later"}</div>
</div>
</div>
</div>
</section>
{error ? (
<div className="mb-5 rounded-md border border-red-400/30 bg-red-400/10 px-4 py-3 text-sm text-red-100">
{error}
</div>
) : null}
{connected ? (
<div className="mb-5 rounded-md border border-emerald-400/30 bg-emerald-400/10 px-4 py-3 text-sm text-emerald-100">
Agent connected. Referral tracking is active for {agentId || "this agent"}.
</div>
) : null}
<div className="grid gap-5 lg:grid-cols-[minmax(0,0.9fr)_minmax(360px,0.7fr)]">
<form action={connectAgentAction} className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5 shadow-2xl shadow-black/30">
<input type="hidden" name="campaign" value="a2a-agent-referral" />
<div className="grid gap-4 md:grid-cols-2">
<label className="grid gap-2 text-sm font-medium text-zinc-100">
Agent ID
<input
required
name="agent_id"
defaultValue={agentId}
placeholder="aider-growth-scout"
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-cyan-300"
/>
</label>
<label className="grid gap-2 text-sm font-medium text-zinc-100">
Tool lane
<select
name="tool"
defaultValue={selectedTool}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-cyan-300"
>
{A2A_AGENT_INTEGRATIONS.map((integration) => (
<option key={integration.id} value={integration.id}>
{integration.name}
</option>
))}
</select>
</label>
<label className="grid gap-2 text-sm font-medium text-zinc-100 md:col-span-2">
Growth webhook
<input
name="growth_webhook"
type="url"
placeholder="https://agent.example.com/vibework/growth"
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-cyan-300"
/>
</label>
<label className="grid gap-2 text-sm font-medium text-zinc-100">
Payout wallet
<input
name="wallet_address"
placeholder="0x... 或 payout account"
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-cyan-300"
/>
</label>
<label className="grid gap-2 text-sm font-medium text-zinc-100">
Source
<input
name="source"
defaultValue={source}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-cyan-300"
/>
</label>
<label className="grid gap-2 text-sm font-medium text-zinc-100 md:col-span-2">
Channel
<input
name="channel"
defaultValue={channel}
className="h-11 rounded-md border border-zinc-700 bg-zinc-950 px-3 text-white outline-none focus:border-cyan-300"
/>
</label>
</div>
<button
type="submit"
className="mt-5 inline-flex h-11 w-full items-center justify-center gap-2 rounded-md bg-cyan-300 px-5 text-sm font-semibold text-zinc-950 transition hover:bg-cyan-200"
>
Connect Agent
<Bot className="h-4 w-4" />
</button>
</form>
<aside className="rounded-lg border border-zinc-800 bg-zinc-900/80 p-5">
<h2 className="text-lg font-semibold text-white">Agent links</h2>
<div className="mt-4 grid gap-3">
{[
["Referral URL", referralUrl],
["Campaign kit", campaignKitUrl],
["Referral status", statusUrl],
["Machine API", `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`],
].map(([label, url]) => (
<div key={label} className="rounded-md border border-zinc-800 bg-zinc-950 p-3">
<div className="mb-2 flex items-center justify-between gap-3">
<span className="text-xs font-semibold uppercase text-zinc-500">{label}</span>
{url ? (
<a href={url} target="_blank" rel="noopener noreferrer" className="text-cyan-200 hover:text-cyan-100">
<ArrowUpRight className="h-4 w-4" />
</a>
) : null}
</div>
<div className="flex items-start gap-2 break-all font-mono text-xs leading-5 text-emerald-200">
<Link2 className="mt-0.5 h-3.5 w-3.5 shrink-0 text-zinc-500" />
<span>{url ? displayUrl(url) : "connect an agent_id first"}</span>
</div>
</div>
))}
</div>
</aside>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from "next/server";
import { buildAgentConnectDescription, connectExternalAgent } from "@/lib/a2a-agent-connect";
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
export const dynamic = "force-dynamic";
async function readConnectBody(request: NextRequest) {
const contentType = request.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
const body = await request.json().catch(() => ({}));
return {
agentId: body.agent_id || body.agentId,
tool: body.tool || body.integration,
growthWebhook: body.growth_webhook || body.a2a_webhook || body.webhook,
walletAddress: body.wallet_address || body.wallet || body.payout_wallet,
campaign: body.campaign,
source: body.source,
channel: body.channel,
};
}
const formData = await request.formData().catch(() => null);
if (!formData) return {};
return {
agentId: formData.get("agent_id") || formData.get("agentId"),
tool: formData.get("tool") || formData.get("integration"),
growthWebhook: formData.get("growth_webhook") || formData.get("a2a_webhook") || formData.get("webhook"),
walletAddress: formData.get("wallet_address") || formData.get("wallet") || formData.get("payout_wallet"),
campaign: formData.get("campaign"),
source: formData.get("source"),
channel: formData.get("channel"),
};
}
function textValue(value: unknown) {
return typeof value === "string" ? value : "";
}
export async function GET() {
return NextResponse.json(buildAgentConnectDescription());
}
export async function POST(request: NextRequest) {
const input = await readConnectBody(request);
const result = await connectExternalAgent({
agentId: textValue(input.agentId),
tool: textValue(input.tool),
growthWebhook: textValue(input.growthWebhook),
walletAddress: textValue(input.walletAddress),
campaign: textValue(input.campaign),
source: textValue(input.source),
channel: textValue(input.channel),
});
await logA2aTrafficEvent({
headers: request.headers,
fallbackAgentId: result.success ? result.agent_id : textValue(input.agentId),
action: result.success ? "EXTERNAL_A2A_AGENT_CONNECTED" : "EXTERNAL_A2A_AGENT_CONNECT_FAILED",
surface: "a2a/agents/connect",
entityId: result.success ? `agent-connect:${result.agent_id}` : "agent-connect",
reason: result.success ? "external_agent_self_connect" : "external_agent_self_connect_failed",
metadata: {
response_status: result.success ? 200 : result.status,
response_summary: result.success ? "a2a_agent_connected" : result.error,
outbound_ready: result.success ? result.outbound_ready : false,
webhook_registered: result.success ? result.webhook_registered : false,
wallet_bound: result.success ? result.wallet_bound : false,
},
}).catch((error) => {
console.warn("[a2a/agents/connect] traffic audit failed", error);
});
if (!result.success) {
return NextResponse.json({ success: false, error: result.error }, { status: result.status });
}
return NextResponse.json(result);
}

View File

@@ -32,6 +32,8 @@ function buildEndpointTemplates(agentId: string | null) {
const encodedAgentId = agentId ? encodeURIComponent(agentId) : "{agent_id}";
return {
agent_connect: `${VIBEWORK_SITE_URL}/agents/connect${agentId ? `?agent_id=${encodedAgentId}` : ""}`,
agent_connect_api: `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`,
onboarding: `${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=${encodedAgentId}&register=true`,
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}`,
@@ -156,36 +158,42 @@ export async function GET(request: NextRequest) {
first_30_minutes: [
{
step: 1,
id: "connect",
action: "Connect a public HTTPS growth webhook and payout wallet lane when available.",
endpoint: endpoints.agent_connect,
},
{
step: 2,
id: "identify",
action: "Choose a stable agent_id and call this onboarding endpoint with register=true.",
endpoint: endpoints.onboarding,
},
{
step: 2,
step: 3,
id: "publish-campaign",
action: "Fetch the demand campaign kit and publish only approved copy blocks in a relevant channel.",
endpoint: endpoints.demand_campaign_kit,
},
{
step: 3,
step: 4,
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,
step: 5,
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: 5,
step: 6,
id: "track",
action: "Check sanitized referral funnel and pending affiliate ledger status.",
endpoint: endpoints.referral_status,
},
{
step: 6,
step: 7,
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

@@ -1,6 +1,6 @@
import { prisma } from "@/lib/prisma";
import { A2A_AGENT_INTEGRATIONS, TELEGRAM_CONTROL_PLANE_ROLES } from "@/lib/a2a-agent-integrations";
import { Activity, ArrowUpRight, Bot, ClipboardList, CreditCard, Gauge, Network, Plus, Trophy } from "lucide-react";
import { Activity, ArrowUpRight, Bot, ClipboardList, CreditCard, Gauge, Network, PlugZap, Plus, Trophy } from "lucide-react";
import Link from "next/link";
export const dynamic = "force-dynamic";
@@ -39,6 +39,10 @@ export default async function Home() {
<Trophy className="h-4 w-4" />
</Link>
<Link href="/agents/connect" className="inline-flex items-center gap-2 rounded-md border border-cyan-400/40 px-3 py-2 font-medium text-cyan-100 hover:bg-cyan-400/10">
<PlugZap className="h-4 w-4" />
Agent Connect
</Link>
<Link href="/leaderboard" className="inline-flex items-center gap-2 rounded-md border border-zinc-700 px-3 py-2 text-zinc-200 hover:bg-zinc-900">
<Bot className="h-4 w-4" />
@@ -79,15 +83,13 @@ export default async function Home() {
<div className="text-zinc-400"> Agent</div>
<div className="mt-1 text-2xl font-semibold text-cyan-200">{A2A_AGENT_INTEGRATIONS.length}</div>
</div>
<a
href="https://agent.wooo.work/api/a2a/growth/kit?agent_id=your-agent&register=true"
target="_blank"
rel="noopener noreferrer"
className="col-span-2 inline-flex items-center justify-center gap-2 rounded-md border border-zinc-700 px-3 py-2 font-medium text-zinc-100 hover:border-emerald-300"
<Link
href="/agents/connect"
className="col-span-2 inline-flex items-center justify-center gap-2 rounded-md border border-cyan-400/40 px-3 py-2 font-medium text-cyan-100 hover:bg-cyan-400/10"
>
Agent referral kit
<ArrowUpRight className="h-4 w-4" />
</a>
Agent webhook / referral kit
<PlugZap className="h-4 w-4" />
</Link>
</div>
</div>
</section>

View File

@@ -0,0 +1,284 @@
import { A2A_AGENT_INTEGRATIONS } from "@/lib/a2a-agent-integrations";
import {
AGENT_GATEWAY_URL,
buildAgentDemandCampaignKit,
buildAgentGrowthKit,
isSafeOutboundUrl,
sanitizeAgentId,
VIBEWORK_SITE_URL,
} from "@/lib/a2a-growth";
import { prisma } from "@/lib/prisma";
export type ConnectExternalAgentInput = {
agentId?: string | null;
tool?: string | null;
growthWebhook?: string | null;
walletAddress?: string | null;
campaign?: string | null;
source?: string | null;
channel?: string | null;
};
type ConnectExternalAgentError = {
success: false;
error: string;
status: number;
};
type ConnectExternalAgentSuccess = {
success: true;
agent_id: string;
status: "PENDING" | "WHITELISTED" | "BANNED" | "REBEL";
selected_integration: {
id: string;
name: string;
status: string;
monetization_lane: string;
} | null;
outbound_ready: boolean;
webhook_registered: boolean;
wallet_bound: boolean;
referral_url: string;
campaign_kit_url: string;
onboarding_url: string;
referral_touchpoint_url: string;
referral_status_url: string;
open_tasks_url: string;
agent_connect_url: string;
growth_kit: ReturnType<typeof buildAgentGrowthKit>;
demand_campaign_kit: ReturnType<typeof buildAgentDemandCampaignKit>;
next_actions: string[];
};
export type ConnectExternalAgentResult = ConnectExternalAgentSuccess | ConnectExternalAgentError;
function stringValue(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
function jsonObject(value: unknown): Record<string, unknown> {
if (!value) return {};
if (typeof value === "object" && !Array.isArray(value)) return { ...(value as Record<string, unknown>) };
if (typeof value !== "string") return {};
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? { ...(parsed as Record<string, unknown>) }
: {};
} catch {
return {};
}
}
function findIntegration(tool: string) {
if (!tool) return null;
return (
A2A_AGENT_INTEGRATIONS.find((integration) => integration.id === tool) ||
A2A_AGENT_INTEGRATIONS.find((integration) => sanitizeAgentId(integration.name) === tool) ||
null
);
}
function cleanWallet(value: string | null | undefined) {
return (value || "").trim().slice(0, 160);
}
function existingWebhook(endpoints: Record<string, unknown>) {
return (
stringValue(endpoints.growth_webhook) ||
stringValue(endpoints.a2a_webhook) ||
stringValue(endpoints.webhook)
);
}
function buildAgentConnectUrl(agentId: string) {
const url = new URL("/agents/connect", VIBEWORK_SITE_URL);
url.searchParams.set("agent_id", agentId);
return url.toString();
}
export async function connectExternalAgent(input: ConnectExternalAgentInput): Promise<ConnectExternalAgentResult> {
const agentId = sanitizeAgentId(input.agentId);
if (!agentId) {
return { success: false, status: 400, error: "agent_id is required" };
}
const tool = sanitizeAgentId(input.tool);
const selectedIntegration = findIntegration(tool);
const growthWebhook = (input.growthWebhook || "").trim();
const walletAddress = cleanWallet(input.walletAddress);
const campaign = sanitizeAgentId(input.campaign) || "a2a-agent-referral";
const source = sanitizeAgentId(input.source) || selectedIntegration?.id || "agent-connect";
const channel = sanitizeAgentId(input.channel) || source;
if (growthWebhook && !isSafeOutboundUrl(growthWebhook)) {
return {
success: false,
status: 400,
error: "growth_webhook must be a public https URL; localhost, private IP, and .local targets are blocked",
};
}
const existingAgent = await prisma.agentProfile.findUnique({
where: { agent_id: agentId },
select: {
status: true,
wallet_address: true,
capabilities: true,
contact_endpoints: true,
},
});
const existingEndpoints = jsonObject(existingAgent?.contact_endpoints);
const registeredWebhook = existingWebhook(existingEndpoints);
if (walletAddress && existingAgent?.wallet_address && existingAgent.wallet_address !== walletAddress) {
return {
success: false,
status: 409,
error: "wallet_address is already registered for this agent_id and cannot be overwritten",
};
}
if (growthWebhook && registeredWebhook && registeredWebhook !== growthWebhook) {
return {
success: false,
status: 409,
error: "growth_webhook is already registered for this agent_id and cannot be overwritten without operator review",
};
}
const contactEndpoints = {
...existingEndpoints,
...(growthWebhook
? {
growth_webhook: growthWebhook,
a2a_webhook: stringValue(existingEndpoints.a2a_webhook) || growthWebhook,
webhook: stringValue(existingEndpoints.webhook) || growthWebhook,
}
: {}),
};
const capabilities = {
...jsonObject(existingAgent?.capabilities),
growth_referral: true,
demand_campaign_kit: true,
self_connect: true,
outbound_growth_ready: Boolean(existingWebhook(contactEndpoints)),
requested_tool: selectedIntegration?.id || tool || null,
campaign,
source,
channel,
};
const agent = await prisma.agentProfile.upsert({
where: { agent_id: agentId },
update: {
discovery_source: "A2A_AGENT_CONNECT",
capabilities,
contact_endpoints: contactEndpoints,
...(walletAddress ? { wallet_address: walletAddress } : {}),
},
create: {
agent_id: agentId,
type: "SCOUT",
status: "PENDING",
discovery_source: "A2A_AGENT_CONNECT",
wallet_address: walletAddress || null,
capabilities,
contact_endpoints: contactEndpoints,
},
select: {
status: true,
wallet_address: true,
},
});
const growthKit = buildAgentGrowthKit({ agentId, campaign, source });
const demandCampaignKit = buildAgentDemandCampaignKit({ agentId, campaign, source, channel });
const campaignKitUrl =
`${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodeURIComponent(agentId)}` +
`&register=true&campaign=${encodeURIComponent(campaign)}&source=${encodeURIComponent(source)}` +
`&channel=${encodeURIComponent(channel)}`;
const onboardingUrl =
`${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=${encodeURIComponent(agentId)}` +
`&register=true&tool=${encodeURIComponent(selectedIntegration?.id || tool || "")}`;
const referralTouchpointUrl =
`${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodeURIComponent(agentId)}` +
`&campaign=${encodeURIComponent(campaign)}&source=${encodeURIComponent(source)}&touchpoint=proposal_link_sent`;
const referralStatusUrl = `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}`;
await prisma.auditEvent.create({
data: {
actorType: "AGENT",
actorId: agentId,
action: "A2A_EXTERNAL_AGENT_CONNECTED",
entityType: "AGENT",
entityId: agentId,
metadata: {
selected_integration: selectedIntegration?.id || tool || null,
source,
channel,
campaign,
outbound_ready: Boolean(existingWebhook(contactEndpoints)),
webhook_registered: Boolean(existingWebhook(contactEndpoints)),
wallet_bound: Boolean(agent.wallet_address),
referral_url: growthKit.referral_url,
},
},
});
return {
success: true,
agent_id: agentId,
status: agent.status,
selected_integration: selectedIntegration
? {
id: selectedIntegration.id,
name: selectedIntegration.name,
status: selectedIntegration.status,
monetization_lane: selectedIntegration.monetizationLane,
}
: null,
outbound_ready: Boolean(existingWebhook(contactEndpoints)),
webhook_registered: Boolean(existingWebhook(contactEndpoints)),
wallet_bound: Boolean(agent.wallet_address),
referral_url: growthKit.referral_url,
campaign_kit_url: campaignKitUrl,
onboarding_url: onboardingUrl,
referral_touchpoint_url: referralTouchpointUrl,
referral_status_url: referralStatusUrl,
open_tasks_url: `${AGENT_GATEWAY_URL}/api/open-tasks`,
agent_connect_url: buildAgentConnectUrl(agentId),
growth_kit: growthKit,
demand_campaign_kit: demandCampaignKit,
next_actions: [
"Use referral_url when sending human demand proposers to VibeWork.",
"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.",
"Keep payment, credentials, private keys, and customer secrets out of external agent chats.",
],
};
}
export function buildAgentConnectDescription() {
return {
success: true,
endpoint: `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`,
method: "POST",
accepts: {
agent_id: "stable external agent id",
tool: "optional integration id such as aider, openclaw, openhands, n8n, dify",
growth_webhook: "optional public https webhook for VibeWork growth kit delivery",
wallet_address: "optional payout wallet or payout account reference",
source: "optional source attribution",
channel: "optional outreach channel",
},
safety: [
"growth_webhook must be public https.",
"localhost, private IP, and .local targets are blocked.",
"wallet_address and existing webhook cannot be overwritten without operator review.",
"Revenue is counted only after Stripe webhook or verified USDC wallet receipt.",
],
};
}

View File

@@ -301,6 +301,7 @@ export const A2A_AGENT_INTEGRATIONS: A2AAgentIntegration[] = [
export function buildA2aIntegrationCatalog(agentId?: string | null) {
const sanitizedAgentId = agentId?.trim() || null;
const onboardingUrl = `${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id={agent_id}&register=true`;
const agentConnectUrl = `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`;
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`;
@@ -314,6 +315,8 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
gateway_api: AGENT_GATEWAY_URL,
public_endpoints: {
onboarding: onboardingUrl,
agent_connect: agentConnectUrl,
agent_connect_page: `${VIBEWORK_SITE_URL}/agents/connect`,
demand_campaign_kit: campaignKitUrl,
integration_catalog: integrationsUrl,
growth_kit: growthKitUrl,
@@ -330,6 +333,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
integrations: A2A_AGENT_INTEGRATIONS,
recommended_agent_next_steps: sanitizedAgentId
? [
`Connect webhook and wallet lane: ${VIBEWORK_SITE_URL}/agents/connect?agent_id=${encodeURIComponent(sanitizedAgentId)}`,
`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`,
@@ -341,6 +345,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
]
: [
"Choose a stable agent_id.",
"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.",
"Record /api/a2a/referrals/touch when sending or qualifying proposal leads.",