Hi, I noticed a possible payment-flow ordering issue while reviewing the current source.
In packages/js-sdk/src/handler/server/plugins/with-x402.ts, withX402 builds a payment requirement for the MCP tool resource, verifies the submitted payment, executes the wrapped callback, and only then settles.
The requirement is scoped to the tool resource and payee:
172: reqs.push({173: scheme: "exact"asconst,174: network,175: maxAmountRequired,176: payTo: normalizedPayTo,177: asset: normalizedAsset,178: maxTimeoutSeconds: 300,179: resource: `mcp://${name}`,180: mimeType: "application/json",181: description,182: extra,183:});The submitted payment is decoded and verified first:
272: // Decode & verify273: letdecoded: PaymentPayload;274: try{275: decoded=decodeX402Payment(token);276: decoded.x402Version=x402Version;277: }catch{278: returnpaymentRequired("INVALID_PAYMENT");279: }280:
281: constselected=findMatchingPaymentRequirements(accepts,decoded);286: constvr=awaitverify(decoded,selected);287: if(!vr.isValid){288: returnpaymentRequired(vr.invalidReason??"INVALID_PAYMENT",{289: payer: vr.payer,290:});291: }But the paid tool callback runs before settlement:
293: // Execute tool294: letresult: CallToolResult;295: letfailed=false;296: try{297: result=awaitcb(args,extra);
...
316: // Settle only on success317: if(!failed){318: try{319: consts=awaitsettle(decoded,selected);320: if(s.success){So the current order appears to be:
payment header -> decode/select requirement -> verify -> cb(args, extra) -> settle
Why this may matter:
- Verification is not always the same as completed settlement.
- A wrapped MCP tool callback can perform expensive computation, external calls, or state-changing work.
- If
settle(decoded, selected) fails after cb(args, extra) has already run, the wrapper can return a payment error but cannot undo the paid work.
A safer design would complete settlement before invoking externally effective callbacks, or clearly document that pre-settlement callbacks must be side-effect-free. A regression test where verify succeeds and settle fails after a callback would make the intended behavior explicit.
I am reporting this as a potential issue rather than a confirmed exploit, since the final impact depends on the callbacks users register with withX402.
Hi, I noticed a possible payment-flow ordering issue while reviewing the current source.
In
packages/js-sdk/src/handler/server/plugins/with-x402.ts,withX402builds a payment requirement for the MCP tool resource, verifies the submitted payment, executes the wrapped callback, and only then settles.The requirement is scoped to the tool resource and payee:
The submitted payment is decoded and verified first:
But the paid tool callback runs before settlement:
So the current order appears to be:
Why this may matter:
settle(decoded, selected)fails aftercb(args, extra)has already run, the wrapper can return a payment error but cannot undo the paid work.A safer design would complete settlement before invoking externally effective callbacks, or clearly document that pre-settlement callbacks must be side-effect-free. A regression test where
verifysucceeds andsettlefails after a callback would make the intended behavior explicit.I am reporting this as a potential issue rather than a confirmed exploit, since the final impact depends on the callbacks users register with
withX402.