Skip to content

Potential paid-work-before-settlement issue in x402 tool routes #1

@chenshj73

Description

@chenshj73

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions