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" as const,
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 & verify
273: let decoded: PaymentPayload;
274: try {
275: decoded = decodeX402Payment(token);
276: decoded.x402Version = x402Version;
277: } catch {
278: return paymentRequired("INVALID_PAYMENT");
279: }
280:
281: const selected = findMatchingPaymentRequirements(accepts, decoded);
286: const vr = await verify(decoded, selected);
287: if (!vr.isValid) {
288: return paymentRequired(vr.invalidReason ?? "INVALID_PAYMENT", {
289: payer: vr.payer,
290: });
291: }
But the paid tool callback runs before settlement:
293: // Execute tool
294: let result: CallToolResult;
295: let failed = false;
296: try {
297: result = await cb(args, extra);
...
316: // Settle only on success
317: if (!failed) {
318: try {
319: const s = await settle(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.