Guide · updated 2026-10-08 · about 6 minutes

How to test an x402 endpoint with curl

Quick answer: request the paid URL with curl -i. You should get status 402 and a PAYMENT-REQUIRED header that base64-decodes to JSON with x402Version: 2 and an accepts list. Check that the amount is in the token’s smallest units, that the network and asset are the ones you mean, that CORS exposes the header, and that HEAD returns 402 too.

Every command below runs against this store’s own paywall, so you can try them right now and compare with your endpoint. Set the URL once:

URL=https://x402-seller-kit.soloearn.workers.dev/download

1. The status is 402

curl -s -o /dev/null -w '%{http_code}\n' "$URL"      # 402

A 200 means the file is unprotected. A 401 or 403 means something else is in front of it, such as an auth layer or a firewall rule.

2. The PAYMENT-REQUIRED header is there and decodes

The x402 v2 HTTP transport spec puts the payment terms, base64-encoded, in this header. Clients read the header, not the body.

curl -s -D - -o /dev/null "$URL" | grep -i '^payment-required:' \
  | cut -d' ' -f2 | tr -d '\r' | base64 -d | jq .

You should see x402Version, resource and an accepts array. If base64 -d fails, check for URL-safe characters or a missing btoa on the server.

3. Browsers can read it

curl -s -D - -o /dev/null "$URL" | grep -i '^access-control-expose-headers'
# access-control-expose-headers: PAYMENT-REQUIRED

Without this, a browser-based x402 client on another origin gets the 402 but can’t see the terms. Expose PAYMENT-RESPONSE as well on the paid response.

4. The amount is in the right units

curl -s "$URL" | jq -r '.accepts[0].amount'          # 29000000

Amounts are strings in the token’s smallest unit. USDC has 6 decimals, so 29000000 is 29 USDC. A common mistake is writing "29", which asks for 0.000029 USDC.

5. Network, asset and payTo are what you meant

curl -s "$URL" | jq '.accepts[0] | {network, asset, payTo, maxTimeoutSeconds}'

6. HEAD returns 402 as well

curl -s -I "$URL" | head -1                         # HTTP/2 402

Some link checkers and directory crawlers send HEAD first. On this store, an early version answered HEAD with 404 while GET returned 402, and we only noticed it in our request logs. Route both methods to the same handler.

7. Discovery metadata is present

curl -s "$URL" | jq '{desc: .resource.description, mime: .resource.mimeType, bazaar: (.extensions.bazaar != null)}'

A clear description and MIME type, plus Bazaar discovery metadata in extensions, help directories and agents list and price your endpoint. Keep the description specific: what the buyer gets, in one line.

A one-shot check script

#!/usr/bin/env bash
# usage: ./x402-check.sh https://your.host/paid-path
URL=$1
H=$(curl -s -D - -o /dev/null "$URL")
echo "$H" | head -1 | grep -q ' 402' && echo "ok   402 on GET" || echo "FAIL GET is not 402"
curl -s -I "$URL" | head -1 | grep -q ' 402' && echo "ok   402 on HEAD" || echo "FAIL HEAD is not 402"
T=$(echo "$H" | grep -i '^payment-required:' | cut -d' ' -f2 | tr -d '\r' | base64 -d 2>/dev/null)
[ -n "$T" ] && echo "ok   PAYMENT-REQUIRED decodes" || echo "FAIL no PAYMENT-REQUIRED header"
echo "$H" | grep -qi '^access-control-expose-headers:.*payment-required' && echo "ok   header exposed to browsers" || echo "WARN header not in Access-Control-Expose-Headers"
echo "$T" | jq -r '.accepts[] | "info \(.network) \(.asset) amount=\(.amount) payTo=\(.payTo)"'

This covers what a client and a directory see before payment. To test the paid path, use an x402 client library with a test wallet, ideally on Base Sepolia first.

FAQ

Which headers does x402 v2 use?

Three, all base64-encoded JSON: PAYMENT-REQUIRED (server to client, sent with the 402), PAYMENT-SIGNATURE (client to server, carrying the signed payment) and PAYMENT-RESPONSE (server to client, carrying the settlement result).

Is the JSON body required?

The x402 v2 HTTP transport spec says protocol information travels in the headers and that the response body is up to the server. Sending the same terms in the body as well makes the endpoint easier for people and simple crawlers to read.

Why does my browser client not see PAYMENT-REQUIRED?

Browsers hide non-standard response headers from cross-origin scripts unless the server lists them in Access-Control-Expose-Headers. Add PAYMENT-REQUIRED and PAYMENT-RESPONSE there.

Can curl test the paid path?

Not on its own. A real PAYMENT-SIGNATURE needs a wallet signature, so use an x402 client library with a funded test wallet, or a testnet network, for that part. curl is for checking everything a client and a directory see before payment.

Source for header names: the x402 v2 HTTP transport spec in the coinbase/x402 repository. Building the endpoint from scratch? Read how to sell a digital file for USDC with x402 on Cloudflare Workers.

Want a store that already passes these checks? The x402 Seller Kit is a tested Cloudflare Worker template with an order-code checkout, one-time claims and a smoke test. The README is free to read.

Get the kit · $29