Hi, I noticed a possible paid-work-before-settlement issue in the current x402 tool routes.
The route configuration in apps/web/lib/paywall/x402.ts defines the expected payment requirements for each paid tool:
60: const ROUTES: Record<X402Route, X402RouteConfig> = {
61: "GET /api/tools/search": {
62: resource: "/api/tools/search",
73: payTo: RECEIVER_ADDRESS,
74: price: { asset: USDC_TESTNET_ADDRESS, amount: TOOL_PRICES_TOKEN_UNITS.search },
115: "GET /api/tools/weather": {
116: resource: "/api/tools/weather",
127: payTo: RECEIVER_ADDRESS,
128: price: { asset: USDC_TESTNET_ADDRESS, amount: TOOL_PRICES_TOKEN_UNITS.weather },
The shared helper has a clean settlement routine:
86: export async function settlePaidToolJsonWithProofs(
93: const settled = await settlePaymentForTool(
94: vr.paymentPayload,
95: vr.paymentRequirements,
96: { tool: toolName }
97: );
111: return NextResponse.json(
123: {
124: status: 200,
125: headers: settled.headers,
The ordering in the concrete tool routes is what looks worth checking. In the weather endpoint, the external paid work runs before settlement is called:
17: export async function GET(req: NextRequest): Promise<NextResponse> {
18: const verify = await verifyPaidOrReturn402(req);
19: const early = paidX402EarlyResponse(verify);
20: if (early) return early;
47: // ON-CHAIN REVENUE SPLITTING
50: const policy = await authorizeSplitSpendingPolicyForVerifiedPayment(
58: const result = await getWeather(location);
59: return settlePaidToolJsonWithProofs(vr, "weather", "$0.05", result as Record<string, unknown>, policy);
The search endpoint has the same shape:
26: export async function GET(req: NextRequest): Promise<NextResponse> {
27: const verify = await verifyPaidOrReturn402(req);
28: const early = paidX402EarlyResponse(verify);
29: if (early) return early;
56: // ON-CHAIN REVENUE SPLITTING
61: const policy = await authorizeSplitSpendingPolicyForVerifiedPayment(
70: const result = await search(q);
76: return settlePaidToolJsonWithProofs(vr, "search", "$0.01", result as Record<string, unknown>, policy);
The effective order appears to be:
source: x402 receipt/header
transform: verifyPaidOrReturn402 + policy authorization
sink: getWeather/search paid provider call
later sink: settlePaymentForTool
If verifyPaidOrReturn402 only establishes that the payment is acceptable but settlement can still fail later, the route may spend provider resources or compute paid output before settlement success is known. This is especially relevant for endpoints that call external APIs or incur provider cost.
Possible hardening directions:
- Settle the payment before invoking externally costly tool work, or buffer output until settlement succeeds.
- Add a regression test where verification succeeds but
settlePaymentForTool fails; the test should assert that getWeather or search is not called.
- If this ordering is intentional, document that tool providers may run before final settlement and should only use reversible or low-cost work in this path.
I am reporting this as a potential issue rather than a confirmed exploit, since verifyPaidOrReturn402 and the facilitator may already provide stronger guarantees than are visible from this route-level ordering.
Hi, I noticed a possible paid-work-before-settlement issue in the current x402 tool routes.
The route configuration in
apps/web/lib/paywall/x402.tsdefines the expected payment requirements for each paid tool:The shared helper has a clean settlement routine:
The ordering in the concrete tool routes is what looks worth checking. In the weather endpoint, the external paid work runs before settlement is called:
The search endpoint has the same shape:
The effective order appears to be:
If
verifyPaidOrReturn402only establishes that the payment is acceptable but settlement can still fail later, the route may spend provider resources or compute paid output before settlement success is known. This is especially relevant for endpoints that call external APIs or incur provider cost.Possible hardening directions:
settlePaymentForToolfails; the test should assert thatgetWeatherorsearchis not called.I am reporting this as a potential issue rather than a confirmed exploit, since
verifyPaidOrReturn402and the facilitator may already provide stronger guarantees than are visible from this route-level ordering.