feat: record A2A referral touchpoints
All checks were successful
CI and Production Smoke / smoke (push) Successful in 6s
All checks were successful
CI and Production Smoke / smoke (push) Successful in 6s
This commit is contained in:
@@ -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}®ister=true",
|
||||
"demandCampaignKit": "https://agent.wooo.work/api/a2a/campaigns/demand?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}",
|
||||
"integrationCatalog": "https://agent.wooo.work/api/a2a/integrations",
|
||||
"paidProposalIntake": "https://vibework.wooo.work/propose",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"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",
|
||||
"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}",
|
||||
"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",
|
||||
|
||||
@@ -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>®ister=true`.
|
||||
2. Fetch approved campaign copy from `https://agent.wooo.work/api/a2a/campaigns/demand?agent_id=<YOUR_AGENT_ID>®ister=true`.
|
||||
3. Request a growth kit from `https://agent.wooo.work/api/a2a/growth/kit?agent_id=<YOUR_AGENT_ID>®ister=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>®ister=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.
|
||||
|
||||
|
||||
@@ -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>"
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -13,6 +13,7 @@ export async function GET() {
|
||||
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",
|
||||
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}",
|
||||
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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -36,6 +36,7 @@ function buildEndpointTemplates(agentId: string | null) {
|
||||
demand_campaign_kit: `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?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`,
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
258
apps/web/src/app/api/a2a/referrals/touch/route.ts
Normal file
258
apps/web/src/app/api/a2a/referrals/touch/route.ts
Normal 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);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -304,6 +304,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) {
|
||||
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 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)}®ister=true`,
|
||||
`Fetch demand campaign kit: ${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?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)}`,
|
||||
"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.",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user