Skip to content

Potential verify-only payment gate: SDK handlers run after verify without settlement #111

Description

@chenshj73

Hi, I noticed a possible verify-only payment gate in the SDK HTTP wrappers.

In packages/sdk/src/serve.ts, the helper posts to the facilitator /verify endpoint:

145: async function verifyWithFacilitator(
146:   paymentPayload: Record<string, unknown>,
147:   paymentRequirements: Record<string, unknown>,
148:   facilitatorUrl: string,
150:   const response = await fetch(`${facilitatorUrl}/verify`, {
153:     body: JSON.stringify({
154:       x402Version: paymentPayload.x402Version ?? 1,
155:       paymentPayload,
156:       paymentRequirements,
157:     }),
165:   return (await response.json()) as {
166:     isValid: boolean

The Express wrapper runs the handler after isValid:

259:     try {
260:       const result = await verifyWithFacilitator(payload, requirements, facilitatorUrl)
261:       if (!result.isValid) {
262:         res.status(402).json({
267:       }
275:
276:     return handler(req, res, next)

The Hono wrapper does the same:

352:     try {
353:       const result = await verifyWithFacilitator(payload, requirements, facilitatorUrl)
354:       if (!result.isValid) {
355:         return c.json(
360:           402,
361:         )
362:       }
372:
373:     return handler(c, next)

And the Next-style wrapper also runs the handler after verify:

458:     try {
459:       const result = await verifyWithFacilitator(payload, requirements, facilitatorUrl)
460:       if (!result.isValid) {
461:         return new Response(
467:         ) as unknown as T
468:       }
478:
479:     return handler(req)

The API documentation describes /facilitator/verify as a soft gate rather than settlement:

1062:   /facilitator/verify:
1065:       operationId: verifyPayment
1066:       summary: Verify an x402 payment signature
1067:       description: |
1068:         Checks that a payment signature is valid without settling on-chain.
1069:         Use this for soft-gating - verify intent before committing.
1070:         Does not prevent replay (use `/facilitator/settle` for that).

The effective wrapper lifecycle appears to be:

payment payload -> /verify isValid -> protected HTTP handler

I did not see a /settle call or replay/nonce consumption in these SDK wrapper paths before the protected handler runs.

A safer design would either settle before executing non-reversible handlers, or clearly document these wrappers as verify-only soft gates and provide a settle-before-handler variant. I am reporting this as a potential issue rather than a confirmed exploit, since other parts of the repository may use stronger merchant flows.

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