feat: track A2A proposal funnel events
All checks were successful
CI and Production Smoke / smoke (push) Successful in 9s

This commit is contained in:
OG T
2026-06-11 14:59:19 +08:00
parent 5abf6be991
commit d9ac59e3dd
8 changed files with 439 additions and 5 deletions

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { buildAgentGrowthKit, sanitizeAgentId } from "@/lib/a2a-growth";
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
export const dynamic = "force-dynamic";
@@ -52,6 +53,23 @@ export async function GET(request: NextRequest) {
},
});
await logA2aTrafficEvent({
headers: request.headers,
fallbackAgentId: agentId,
action: "EXTERNAL_A2A_GROWTH_KIT_ISSUED",
surface: "a2a/growth-kit",
entityId: "a2a-growth-kit",
reason: "external_agent_growth_kit_issued",
metadata: {
campaign,
source,
registered_pending_agent: shouldRegister,
referral_url: kit.referral_url,
response_status: 200,
response_summary: "a2a_growth_kit_issued",
},
});
return NextResponse.json({
success: true,
kit,

View File

@@ -1,12 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import { buildA2aIntegrationCatalog } from "@/lib/a2a-agent-integrations";
import { buildAgentGrowthKit, sanitizeAgentId } from "@/lib/a2a-growth";
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
export const dynamic = "force-dynamic";
export async function GET(request: NextRequest) {
const agentId = sanitizeAgentId(request.nextUrl.searchParams.get("agent_id"));
const catalog = buildA2aIntegrationCatalog(agentId || null);
const action = agentId
? "EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"
: "INTERNAL_A2A_INTEGRATION_CATALOG_VIEW";
void logA2aTrafficEvent({
headers: request.headers,
fallbackAgentId: agentId || null,
action,
surface: "a2a/integrations",
entityId: "a2a-integration-catalog",
reason: agentId ? "external_agent_catalog_discovery" : "catalog_discovery",
metadata: {
agent_id: agentId || null,
integration_count: catalog.integrations.length,
telegram_roles: catalog.telegram_control_plane.length,
response_status: 200,
response_summary: `a2a_integrations_ok:${catalog.integrations.length}`,
},
}).catch((error) => {
console.warn("[a2a/integrations] traffic audit failed", error);
});
return NextResponse.json({
success: true,

View File

@@ -503,8 +503,15 @@ export async function GET(request: NextRequest) {
const discoveryEvents =
(actionSummary["EXTERNAL_LIST_OPEN_TASKS"] || 0) +
(actionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0);
(actionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0) +
(actionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
(actionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 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;
const proposalWalletPendingEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING"] || 0;
const proposalPaidEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED"] || 0;
const claimEvents = actionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
const submitEvents = actionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
const judgePassEvents = judgeCompleteRows.filter((row) => {
@@ -523,6 +530,11 @@ export async function GET(request: NextRequest) {
const externalFunnel = {
discovery_events: discoveryEvents,
proposal_view_events: proposalViewEvents,
proposal_created_events: proposalCreatedEvents,
proposal_checkout_events: proposalCheckoutEvents,
proposal_wallet_pending_events: proposalWalletPendingEvents,
proposal_paid_events: proposalPaidEvents,
claim_events: claimEvents,
submit_events: submitEvents,
judge_pass_events: judgePassEvents,
@@ -533,6 +545,9 @@ export async function GET(request: NextRequest) {
};
const conversionRates = {
proposal_view_rate: conversionRate(proposalViewEvents, discoveryEvents),
proposal_create_rate: conversionRate(proposalCreatedEvents, proposalViewEvents),
proposal_paid_rate: conversionRate(proposalPaidEvents, proposalCreatedEvents),
claim_rate: conversionRate(claimEvents, discoveryEvents),
submit_rate: conversionRate(submitEvents, claimEvents),
pass_rate: conversionRate(judgePassEvents, submitEvents),

View File

@@ -4,6 +4,7 @@ import Stripe from "stripe";
import { TaskStatus } from "@agent-bounty/contracts";
import { broadcastFomoEvent } from "@/lib/x-broadcaster";
import { sanitizeAgentId } from "@/lib/a2a-growth";
import { resolveA2aTrafficContext } from "@/lib/a2a-traffic";
const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2026-05-27.dahlia",
@@ -14,13 +15,16 @@ function getStripeObjectId(value: string | Stripe.PaymentIntent | Stripe.SetupIn
return value?.id || null;
}
async function handleDemandProposalFee(session: Stripe.Checkout.Session) {
async function handleDemandProposalFee(session: Stripe.Checkout.Session, requestHeaders: Headers) {
const metadata = session.metadata || {};
const taskId = metadata.task_id;
const referralAgent = sanitizeAgentId(metadata.referral_agent);
const proposalFeeCents = Number(metadata.proposal_fee_cents || session.amount_total || 0);
const paymentIntentId = getStripeObjectId(session.payment_intent);
const idempotencyKey = `proposal-fee:${session.id}`;
const trafficContext = referralAgent
? resolveA2aTrafficContext(requestHeaders, referralAgent)
: null;
const task = await prisma.task.findFirst({
where: taskId
@@ -131,6 +135,37 @@ async function handleDemandProposalFee(session: Stripe.Checkout.Session) {
},
},
});
if (referralAgent && trafficContext) {
await tx.auditEvent.create({
data: {
actorType: trafficContext.actor_type,
actorId: trafficContext.actor_id,
action: "EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED",
entityType: "TASK",
entityId: task.id,
reason: "referred_demand_proposal_payment_captured",
metadata: {
stripe_session_id: session.id,
stripe_payment_intent_id: paymentIntentId,
fee_cents: proposalFeeCents,
package_id: metadata.package_id || null,
referral_agent: referralAgent,
affiliate_fee_cents: Math.floor(proposalFeeCents * 0.1),
source: metadata.source || null,
campaign: metadata.campaign || null,
request_id: trafficContext.request_id,
source_ip: trafficContext.source_ip,
user_agent: trafficContext.user_agent,
is_public_ip: trafficContext.is_public_ip,
surface: "stripe/webhook",
request_actor_headers: trafficContext.request_actor_headers,
response_status: 200,
response_summary: "demand_proposal_fee_captured",
},
},
});
}
});
console.log(`[Webhook] Proposal fee captured for task ${task.id}. Payment Intent: ${paymentIntentId}`);
@@ -164,7 +199,7 @@ export async function POST(request: NextRequest) {
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
if (session.metadata?.intent === "DEMAND_PROPOSAL_FEE") {
await handleDemandProposalFee(session);
await handleDemandProposalFee(session, request.headers);
return NextResponse.json({ received: true });
}

View File

@@ -1,6 +1,7 @@
"use server";
import { prisma } from "@/lib/prisma";
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
import {
getProposalPackage,
sanitizeAgentId,
@@ -9,6 +10,7 @@ import {
VIBEWORK_SITE_URL,
} from "@/lib/a2a-growth";
import { TaskDifficulty, TaskStatus } from "@agent-bounty/contracts";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import Stripe from "stripe";
@@ -68,6 +70,7 @@ function buildCancelUrl(referralAgent: string, packageId: string) {
}
export async function createDemandProposal(formData: FormData) {
const requestHeaders = await headers();
const packageId = getString(formData, "packageId");
const proposalPackage = getProposalPackage(packageId);
const referralAgent = sanitizeAgentId(getString(formData, "refAgent"));
@@ -178,6 +181,29 @@ export async function createDemandProposal(formData: FormData) {
},
});
if (referralAgent) {
await logA2aTrafficEvent({
headers: requestHeaders,
fallbackAgentId: referralAgent,
action: "EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED",
surface: "propose/action",
entityType: "TASK",
entityId: task.id,
reason: "referred_demand_proposal_submitted",
metadata: {
package_id: proposalPackage.id,
proposal_fee_cents: proposalPackage.feeCents,
budget_cents: budgetCents,
source,
campaign,
referral_agent: referralAgent,
payment_method: paymentMethod,
response_status: 200,
response_summary: "demand_proposal_intake_created",
},
});
}
if (paymentMethod === "wallet" || !stripe) {
await prisma.auditEvent.create({
data: {
@@ -195,6 +221,25 @@ export async function createDemandProposal(formData: FormData) {
},
});
if (referralAgent) {
await logA2aTrafficEvent({
headers: requestHeaders,
fallbackAgentId: referralAgent,
action: "EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING",
surface: "propose/action",
entityType: "TASK",
entityId: task.id,
reason: "wallet_payment_instructions_issued",
metadata: {
package_id: proposalPackage.id,
amount_cents: proposalPackage.feeCents,
referral_agent: referralAgent,
response_status: 200,
response_summary: "wallet_payment_instructions_issued",
},
});
}
redirect(buildSuccessUrl(task.id, {
payment: "wallet",
package: proposalPackage.id,
@@ -244,5 +289,25 @@ export async function createDemandProposal(formData: FormData) {
throw new Error("Stripe checkout session URL is missing.");
}
if (referralAgent) {
await logA2aTrafficEvent({
headers: requestHeaders,
fallbackAgentId: referralAgent,
action: "EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED",
surface: "propose/action",
entityType: "TASK",
entityId: task.id,
reason: "stripe_checkout_started",
metadata: {
stripe_session_id: session.id,
package_id: proposalPackage.id,
amount_cents: proposalPackage.feeCents,
referral_agent: referralAgent,
response_status: 303,
response_summary: "stripe_checkout_started",
},
});
}
redirect(session.url);
}

View File

@@ -1,6 +1,8 @@
import { createDemandProposal } from "@/app/propose/actions";
import { buildAgentGrowthKit, PROPOSAL_PACKAGES, sanitizeAgentId } from "@/lib/a2a-growth";
import { logA2aTrafficEvent } from "@/lib/a2a-traffic";
import { ArrowRight, Bot, CreditCard, Network, Users, Wallet } from "lucide-react";
import { headers } from "next/headers";
import Link from "next/link";
export const dynamic = "force-dynamic";
@@ -23,6 +25,30 @@ export default async function ProposePage({ searchParams }: { searchParams?: Sea
? buildAgentGrowthKit({ agentId: referralAgent, campaign, source })
: null;
if (referralAgent) {
const requestHeaders = await headers();
await logA2aTrafficEvent({
headers: requestHeaders,
fallbackAgentId: referralAgent,
action: "EXTERNAL_DEMAND_PROPOSAL_VIEW",
surface: "propose",
entityType: "SYSTEM",
entityId: "demand-proposal-intake",
reason: "referred_demand_proposer_landed",
metadata: {
referral_agent: referralAgent,
campaign,
source,
package_id: packageId,
cancelled,
response_status: 200,
response_summary: "demand_proposal_view",
},
}).catch((error) => {
console.warn("[propose] traffic audit failed", error);
});
}
return (
<main className="min-h-screen bg-zinc-950 text-zinc-100">
<div className="mx-auto grid min-h-screen max-w-7xl gap-8 px-5 py-6 lg:grid-cols-[1.5fr_0.9fr] lg:px-8">

View File

@@ -9,6 +9,13 @@ const MONITOR_TOKEN = process.env.TRAFFIC_MONITOR_TOKEN;
const EVENT_LABELS: Record<string, string> = {
EXTERNAL_LIST_OPEN_TASKS: "外部公開流量頁讀取 open tasks",
EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW: "外部 Agent 讀取 A2A 整合目錄",
EXTERNAL_A2A_GROWTH_KIT_ISSUED: "外部 Agent 領取 growth kit",
EXTERNAL_DEMAND_PROPOSAL_VIEW: "外部導流需求方查看提案頁",
EXTERNAL_DEMAND_PROPOSAL_INTAKE_CREATED: "外部導流需求方建立提案",
EXTERNAL_DEMAND_PROPOSAL_CHECKOUT_STARTED: "外部導流需求方開始 Stripe 結帳",
EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING: "外部導流需求方取得錢包付款指示",
EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED: "外部導流提案費付款成功",
EXTERNAL_LIST_OPEN_TASKS_SURGE: "外部公開流量突增告警",
EXTERNAL_LIST_OPEN_TASKS_MCP: "外部 MCP 入口讀取 open tasks",
EXTERNAL_LIST_OPEN_TASKS_MCP_SURGE: "外部 MCP 流量突增告警",
@@ -377,7 +384,14 @@ async function getTrafficSummary(minutes: number) {
const discoveryEvents =
(actionSummary["EXTERNAL_LIST_OPEN_TASKS"] || 0) +
(actionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0);
(actionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0) +
(actionSummary["EXTERNAL_A2A_INTEGRATION_CATALOG_VIEW"] || 0) +
(actionSummary["EXTERNAL_A2A_GROWTH_KIT_ISSUED"] || 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;
const proposalWalletPendingEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_WALLET_PAYMENT_PENDING"] || 0;
const proposalPaidEvents = actionSummary["EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED"] || 0;
const claimEvents = actionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
const submitEvents = actionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
const judgePassEvents = judgeCompleteRows.filter((row) => {
@@ -391,6 +405,11 @@ async function getTrafficSummary(minutes: number) {
const conversionSummary = {
discovery_events: discoveryEvents,
proposal_view_events: proposalViewEvents,
proposal_created_events: proposalCreatedEvents,
proposal_checkout_events: proposalCheckoutEvents,
proposal_wallet_pending_events: proposalWalletPendingEvents,
proposal_paid_events: proposalPaidEvents,
claim_events: claimEvents,
submit_events: submitEvents,
judge_pass_events: judgePassEvents,
@@ -400,6 +419,9 @@ async function getTrafficSummary(minutes: number) {
};
const conversionRates = {
proposal_view_rate: percent(proposalViewEvents, discoveryEvents),
proposal_create_rate: percent(proposalCreatedEvents, proposalViewEvents),
proposal_paid_rate: percent(proposalPaidEvents, proposalCreatedEvents),
claim_rate: percent(claimEvents, discoveryEvents),
submit_rate: percent(submitEvents, claimEvents),
pass_rate: percent(judgePassEvents, submitEvents),
@@ -557,12 +579,20 @@ function toLocalTime(value: Date) {
}
function buildConversionTips(summary: {
proposal_view_rate: number;
proposal_create_rate: number;
proposal_paid_rate: number;
claim_rate: number;
submit_rate: number;
pass_rate: number;
payout_rate: number;
}, conversionSummary: {
discovery_events: number;
proposal_view_events: number;
proposal_created_events: number;
proposal_checkout_events: number;
proposal_wallet_pending_events: number;
proposal_paid_events: number;
claim_events: number;
submit_events: number;
judge_pass_events: number;
@@ -572,6 +602,18 @@ function buildConversionTips(summary: {
}) {
const steps: string[] = [];
if (conversionSummary.discovery_events > 0 && conversionSummary.proposal_view_events === 0) {
steps.push("A2A 曝光已有資料但提案頁查看為零:優先確認 growth kit referral_url、vibework.wooo.work/propose 代理與外部貼文 CTA。");
}
if (conversionSummary.proposal_view_events > 0 && conversionSummary.proposal_created_events === 0) {
steps.push("有提案頁查看但無送出:優先檢查表單文案、套裝價格、付款方式與 mobile 可用性。");
}
if (conversionSummary.proposal_created_events > 0 && conversionSummary.proposal_paid_events === 0) {
steps.push("已有提案但尚未付款成功:分流檢查 Stripe checkout 與 USDC wallet receipt verification。");
}
if (conversionSummary.discovery_events > 0 && conversionSummary.claim_events === 0) {
steps.push("曝光高但接單為零:請先檢查 open-tasks 回傳任務文案是否足夠明確,是否包含 npx 指令與標準格式。");
}
@@ -657,9 +699,21 @@ export default async function TrafficDashboard({
<div className="text-3xl font-bold mt-2 text-emerald-300">{summary.channelSummary.external}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm">OPEN_TASKS</div>
<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"></div>
<div className="text-3xl font-bold mt-2 text-sky-300">{conversionSummary.proposal_view_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-violet-300">{conversionSummary.proposal_created_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-emerald-300">{conversionSummary.proposal_paid_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-blue-300">{conversionSummary.claim_events}</div>
@@ -703,6 +757,36 @@ 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曝光</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"
style={{ width: `${Math.min(conversionRates.proposal_view_rate, 100)}%` }}
/>
</div>
<div className="flex justify-between">
<span></span>
<span className="text-emerald-300">{fmtPercent(conversionRates.proposal_create_rate)}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-violet-400"
style={{ width: `${Math.min(conversionRates.proposal_create_rate, 100)}%` }}
/>
</div>
<div className="flex justify-between">
<span></span>
<span className="text-emerald-300">{fmtPercent(conversionRates.proposal_paid_rate)}</span>
</div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-emerald-400"
style={{ width: `${Math.min(conversionRates.proposal_paid_rate, 100)}%` }}
/>
</div>
<div className="flex justify-between">
<span></span>
<span className="text-emerald-300">{fmtPercent(conversionRates.claim_rate)}</span>
@@ -745,6 +829,8 @@ export default async function TrafficDashboard({
</div>
</div>
<div className="mt-4 text-sm text-gray-300 space-y-1">
<div className="flex justify-between"><span>Stripe checkout </span><span>{conversionSummary.proposal_checkout_events}</span></div>
<div className="flex justify-between"><span></span><span>{conversionSummary.proposal_wallet_pending_events}</span></div>
<div className="flex justify-between"><span>PASS </span><span>{conversionSummary.judge_pass_events}</span></div>
<div className="flex justify-between"><span>FAIL </span><span>{conversionSummary.judge_fail_events}</span></div>
<div className="flex justify-between"><span></span><span>{conversionSummary.payout_captured}</span></div>

View File

@@ -0,0 +1,167 @@
import { randomUUID } from "node:crypto";
import { isIP } from "node:net";
import { prisma } from "@/lib/prisma";
type HeaderReader = {
get(name: string): string | null;
};
const REQUEST_ID_HEADERS = ["x-request-id", "x-correlation-id", "x-trace-id"];
const ACTOR_HEADERS = ["x-agent-id", "x-agent-name", "x-ai-agent-id", "x-ai-id"];
const AI_USER_AGENT_HINTS = [
"gpt",
"chatgpt",
"openai",
"anthropic",
"claude",
"perplexity",
"llm",
"mcp",
"autogpt",
"agent",
"assistant",
"gemini",
"cursor",
"copilot",
"aider",
"openhands",
"openclaw",
"hermes",
];
export function normalizeA2aActorId(value: string | null | undefined, fallback = "agent") {
const normalized = (value || fallback)
.trim()
.toLowerCase()
.replace(/[^a-z0-9._:-]+/g, "_")
.replace(/_+/g, "_")
.slice(0, 80);
return normalized || fallback;
}
export function asA2aAgentActorId(value: string | null | undefined, fallback = "agent") {
const normalized = normalizeA2aActorId(value, fallback);
return normalized.startsWith("agent:") ? normalized : `agent:${normalized}`;
}
function sanitizeIpAddress(value: string | null | undefined) {
if (!value) return "unknown";
const first = value.split(",")[0]?.trim();
if (!first) return "unknown";
const bracketedMatch = first.match(/^\[(.+)\]:(\d+)$/);
if (bracketedMatch?.[1]) return bracketedMatch[1].toLowerCase();
const ipv4WithPortMatch = first.match(/^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/);
if (ipv4WithPortMatch?.[1]) return ipv4WithPortMatch[1];
return first.toLowerCase();
}
export function resolveA2aSourceIp(headers: HeaderReader) {
return sanitizeIpAddress(
headers.get("x-forwarded-for") ??
headers.get("x-real-ip") ??
headers.get("cf-connecting-ip") ??
headers.get("true-client-ip") ??
headers.get("x-client-ip") ??
"unknown"
);
}
export function isPrivateA2aSourceIp(ip: string | undefined) {
if (!ip) return true;
const normalized = ip.trim().toLowerCase();
if (!normalized || normalized === "unknown" || normalized === "localhost") return true;
if (normalized === "::1" || normalized.startsWith("fe80")) return true;
if (isIP(normalized) === 6 && (normalized.startsWith("fc") || normalized.startsWith("fd"))) return true;
if (isIP(normalized) === 0) return false;
if (normalized.startsWith("127.") || normalized.startsWith("10.") || normalized.startsWith("192.168.")) return true;
if (normalized.startsWith("172.")) {
const secondOctet = Number(normalized.split(".")[1]);
return secondOctet >= 16 && secondOctet <= 31;
}
return false;
}
function resolveRequestId(headers: HeaderReader) {
for (const headerName of REQUEST_ID_HEADERS) {
const value = headers.get(headerName);
if (value) return value;
}
return randomUUID();
}
function resolveHeaderActor(headers: HeaderReader) {
for (const headerName of ACTOR_HEADERS) {
const value = headers.get(headerName);
if (value) return asA2aAgentActorId(value);
}
return null;
}
function isLikelyAiUserAgent(userAgent: string) {
const normalized = userAgent.toLowerCase();
return AI_USER_AGENT_HINTS.some((token) => normalized.includes(token));
}
export function resolveA2aTrafficContext(headers: HeaderReader, fallbackAgentId?: string | null) {
const sourceIp = resolveA2aSourceIp(headers);
const userAgent = headers.get("user-agent") || "unknown";
const headerActorId = resolveHeaderActor(headers);
const actorId =
fallbackAgentId
? asA2aAgentActorId(fallbackAgentId)
: headerActorId || (isLikelyAiUserAgent(userAgent) ? asA2aAgentActorId(userAgent) : `external:${sourceIp}`);
return {
request_id: resolveRequestId(headers),
source_ip: sourceIp,
user_agent: userAgent,
is_public_ip: !isPrivateA2aSourceIp(sourceIp),
actor_type: actorId.startsWith("agent:") ? ("AGENT" as const) : ("USER" as const),
actor_id: actorId,
request_actor_headers: {
x_agent_id: headers.get("x-agent-id"),
x_agent_name: headers.get("x-agent-name"),
x_ai_agent_id: headers.get("x-ai-agent-id"),
x_ai_id: headers.get("x-ai-id"),
x_request_id: headers.get("x-request-id"),
origin: headers.get("origin"),
referer: headers.get("referer"),
},
};
}
export async function logA2aTrafficEvent(params: {
headers: HeaderReader;
fallbackAgentId?: string | null;
action: string;
surface: string;
entityType?: "TASK" | "CLAIM" | "SUBMISSION" | "JUDGE_RESULT" | "SYSTEM";
entityId: string;
reason?: string;
metadata?: Record<string, unknown>;
}) {
const context = resolveA2aTrafficContext(params.headers, params.fallbackAgentId);
return prisma.auditEvent.create({
data: {
actorType: context.actor_type,
actorId: context.actor_id,
action: params.action,
entityType: params.entityType || "SYSTEM",
entityId: params.entityId,
reason: params.reason,
metadata: {
...params.metadata,
request_id: context.request_id,
source_ip: context.source_ip,
user_agent: context.user_agent,
is_public_ip: context.is_public_ip,
surface: params.surface,
request_actor_headers: context.request_actor_headers,
},
},
});
}