tp tate@programs browser 402
tate@programs ~/notes/x402-cors-fix browser payment headers

x402 cors / may 2026

Preflight is not enough for browser-readable x402.

A browser client has to pass two gates before it can pay: the OPTIONS preflight must allow the payment header, and the actual 402 challenge response must expose the payment requirements. If the second response drops CORS, wallets and browser agents cannot read the amount, rail, payee, or resource they are supposed to authorize.

failure
blind 402
header
X-PAYMENT
result
readable challenge

fix order

Make the challenge readable before you promote the endpoint.

1. Answer OPTIONS on every paid route

Return 204 or 200 with the intended origin, the route method, OPTIONS, and the payment headers the client will send. For x402, that usually means x-payment plus content-type. For MPP or mixed surfaces, include the documented payment-signature and receipt headers too.

2. Add CORS to the actual 402 response

The most common miss is fixing preflight while the payment-required response still has no Access-Control-Allow-Origin. That leaves browser clients unable to read the challenge payload. Add the same origin policy to the 402 path that short-circuits before application handlers.

3. Expose payment requirement headers

If your challenge is carried in a header, expose it. Common names are payment-required, x-payment-required, payment-response, and x-payment-response. Without Access-Control-Expose-Headers, browser code may receive the 402 but still be unable to inspect the fields it needs.

4. Put CORS outside payment middleware

In middleware stacks where the x402 layer returns early, CORS has to wrap that layer. If the payment middleware is outermost, it can emit a 402 before the response passes through CORS. The fast test is simple: hit a paid route without payment and inspect the actual 402 headers, not just OPTIONS.

5. Echo the resource on each accept leg

Keep the top-level resource.url, then repeat the charged URL in accepts[N].resource or accepts[N].extra.resource. That makes spend maps, wallet prompts, receipts, and rail selection easier to reconcile.

6. Keep paid responses out of shared caches

Protected responses and payment challenges should not be publicly cacheable. Use Cache-Control: no-store, private, vary on payment headers, and bypass shared proxy caches for paid content.

commands

Test both gates from the shell.

npm package
no-payment check
npx --yes x402-surface-check@latest \
  --endpoint --method POST https://api.example.com/paid \
  --origin https://app.example.com \
  --body '{"sample":true}'

curl -si -X OPTIONS https://api.example.com/paid \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: x-payment,content-type'

curl -si -X POST https://api.example.com/paid \
  -H 'Origin: https://app.example.com' \
  -H 'Content-Type: application/json' \
  -d '{"sample":true}'

response checklist

The headers to look for.

OPTIONS

Preflight

Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers should appear on the preflight response.

402

Challenge response

The actual payment-required response should also include Access-Control-Allow-Origin, not only the challenge payload.

expose

Readable headers

Access-Control-Expose-Headers should include whichever header carries settlement or payment requirements.

cache

Cache policy

Cache-Control: no-store, private plus Vary on payment headers keeps paid responses out of shared cache reuse.

resource

Route binding

Resource URLs should be canonical HTTPS values, and accept legs should echo the exact URL being purchased.

implementation notes

Framework fixes are usually small.

FastAPI / Starlette

Register CORS so it wraps the x402 middleware path. Then confirm that no-payment 402 responses, not only OPTIONS responses, contain the allow-origin and expose-headers values.

app.add_middleware(X402Middleware, ...)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_methods=["GET", "POST", "OPTIONS"],
    allow_headers=["content-type", "x-payment"],
    expose_headers=["payment-required", "x-payment-response"],
)

Express

Put the CORS layer before paid-route middleware or set headers on the 402 response directly before returning.

app.use(cors({
  origin: "https://app.example.com",
  methods: ["GET", "POST", "OPTIONS"],
  allowedHeaders: ["content-type", "x-payment"],
  exposedHeaders: ["payment-required", "x-payment-response"],
}));

app.options("/paid", cors());
app.post("/paid", x402Middleware, handler);

Next.js / Vercel routes

If a route handler returns 402 before normal response helpers run, attach the headers to that returned response. Also implement an explicit OPTIONS export for the same path.

const corsHeaders = {
  "Access-Control-Allow-Origin": "https://app.example.com",
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
  "Access-Control-Allow-Headers": "content-type, x-payment",
  "Access-Control-Expose-Headers": "payment-required, x-payment-response",
};

export async function OPTIONS() {
  return new Response(null, { status: 204, headers: corsHeaders });
}

field proof

This has already changed live x402 surfaces.

The pattern is not theoretical. Recent no-payment checks caught it on public pay-skills launches, and follow-up patches made the payment requirements readable.

fixed

anchor-x402

Middleware order was changed so actual 402 responses pass through CORS and expose the payment headers.

fix verification

partial

Blocksize market data

Preflight and resource echo were fixed; the latest note isolated actual-402 ACAO as the remaining browser-agent blocker.

follow-up note

partial

x402watch

Paid endpoint metadata and POST preflight improved; actual 402 responses still need browser-readable CORS.

follow-up note

partial

x402jp Japan data

Weather routes now return structured 402 challenges; browser CORS and resource echo remain the highest-value polish.

fix verification

paid review

Need this checked before a launch?

Send one manifest, OpenAPI file, direct endpoint, or registry PR. The fixed-scope review returns the no-payment evidence, spend-map gaps, and patch order.