Skip to content

Potential settlement-ordering issue: paid resource can be released without completed settlement #2

Description

@chenshj73

Potential settlement-ordering issue: paid resource can be released without completed settlement

Hi, I noticed a possible payment-flow issue while reviewing the current repository state. This is a conservative report based on the visible code path, and I may be missing deployment-specific guards outside this repository.

Reviewed HEAD: e38ecd00e2df

What I observed

In the visible payment path, I observed:

Facilitator routes parse paymentRequirements but core Facilitator.verify ignores requirements and calls standalone verifyPayment; settlement then transfers according to payment.authorization fields; resource middleware grants access after verify+settle so recipient/amount/asset requirements are not enforced in this path

Relevant code locations:

  • x402-facilitator/src/routes/verify.ts
  • x402-facilitator/src/routes/settle.ts
  • x402-facilitator/src/core/facilitator.ts
  • x402-facilitator/src/core/verifier.ts
  • x402-facilitator/src/middleware/x402-resource-server.ts

Relevant code excerpts

x402-facilitator/src/routes/verify.ts:45-59

  45 export function createVerifyRoute(facilitator: Facilitator): Hono {
  46   const route = new Hono();
  47 
  48   route.post('/', zValidator('json', verifyBodySchema), async (c) => {
  49     const { paymentPayload: rawPayment, paymentRequirements: rawReqs } = c.req.valid('json');
  50 
  51     const payment = toX402Payment(rawPayment);
  52     const requirements = toPaymentRequirements(rawReqs);
  53     const result = await facilitator.verify(payment, requirements);
  54 
  55     return c.json(result, result.isValid ? 200 : 400);
  56   });
  57 
  58   return route;
  59 }

x402-facilitator/src/routes/settle.ts:41-55

  41 export function createSettleRoute(facilitator: Facilitator): Hono {
  42   const route = new Hono();
  43 
  44   route.post('/', zValidator('json', settleBodySchema), async (c) => {
  45     const { paymentPayload: rawPayment, paymentRequirements: rawReqs } = c.req.valid('json');
  46 
  47     const payment = toX402Payment(rawPayment);
  48     const requirements = toPaymentRequirements(rawReqs);
  49     const result = await facilitator.settle(payment, requirements);
  50 
  51     return c.json(result, result.success ? 200 : 402);
  52   });
  53 
  54   return route;
  55 }

x402-facilitator/src/core/facilitator.ts:43-67

  43   const r = `0x${sig.slice(0, 64)}` as Hex;
  44   const s = `0x${sig.slice(64, 128)}` as Hex;
  45   let v = parseInt(sig.slice(128, 130), 16);
  46   if (v < 27) v += 27;
  47   return { v, r, s };
  48 }
  49 
  50 /**
  51  * Facilitator — orchestrates payment verification and on-chain settlement.
  52  */
  53 export class Facilitator {
  54   private walletClients: Map<SupportedChainId, WalletClient> = new Map();
  55   private publicClients: Map<SupportedChainId, PublicClient> = new Map();
  56   private readonly address: Address;
  57   private inflightNonces: Set<string> = new Set();
  58 
  59   constructor(config: FacilitatorConfig) {
  60     const account = privateKeyToAccount(config.privateKey);
  61     this.address = account.address;
  62 
  63     for (const chain of config.chains) {
  64       const viemChain = getViemChain(chain.chainId);
  65 
  66       this.walletClients.set(
  67         chain.chainId,

x402-facilitator/src/core/verifier.ts:54-78

  54  */
  55 export class PaymentVerifier {
  56   /**
  57    * Verify a payment against its requirements.
  58    *
  59    * Checks: chain match, asset match, recipient match, amount, timing,
  60    * requirements expiry, EIP-712 signature, and on-chain balance.
  61    */
  62   async verify(payment: X402Payment, requirements: PaymentRequirements): Promise<VerifyResult> {
  63     const { chainId, token, authorization, signature } = payment;
  64 
  65     // Chain ID match
  66     if (chainId !== requirements.chainId) {
  67       return { valid: false, reason: 'Chain ID mismatch' };
  68     }
  69 
  70     // Asset match (case-insensitive)
  71     if (token.toLowerCase() !== requirements.asset.toLowerCase()) {
  72       return { valid: false, reason: 'Asset mismatch' };
  73     }
  74 
  75     // Recipient match
  76     if (authorization.to.toLowerCase() !== requirements.payTo.toLowerCase()) {
  77       return { valid: false, reason: 'Recipient mismatch' };
  78     }

x402-facilitator/src/middleware/x402-resource-server.ts:67-89

  67         method: 'POST',
  68         headers: { 'Content-Type': 'application/json' },
  69         body: JSON.stringify({ payment, paymentRequirements }),
  70       });
  71 
  72       const settleData = (await settleRes.json()) as SettleResponse;
  73 
  74       if (!settleData.success) {
  75         return c.json({ error: 'Payment settlement failed', ...settleData }, 402);
  76       }
  77 
  78       // Attach settlement info to the request context for downstream handlers
  79       c.set('x402Settlement', settleData);
  80     } catch (err) {
  81       logger.error({
  82         error: err instanceof Error ? err.message : 'Unknown',
  83       }, 'x402 resource server middleware error');
  84       return c.json({ error: 'Payment processing error' }, 500);
  85     }
  86 
  87     await next();
  88   };
  89 }

Why this may matter

Payment verification should be rebound to server-trusted requirements such as payTo, amount, asset, network, nonce, and resource. If those values come from the client or are not rechecked, a different payment can satisfy the gate.

Suggested check

Consider making the paid-resource path depend on server-trusted payment requirements and a completed payment state. In particular, re-check recipient/payTo, amount, asset, network, nonce/idempotency, resource binding, and settlement result at the exact point where the protected API/tool/content is released.

Conservative caveat

I only reviewed the code visible in this repository at the HEAD above. If deployment-specific middleware or an upstream service enforces the missing binding/settlement invariant, this may already be mitigated there.

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