# HTTP 402 and x402: three places a payment breaks

> Got an HTTP 402 and the x402 payment still fails? Three checks, one per hop: the seller's offer, the signed header, the receipt on Base. Real outputs inside.

Published 2026-10-16 · https://aiskills402.com/blog/http-402-x402-payment-checks

If an HTTP 402 answer arrived and your x402 payment still fails, the fault sits in one of three places, in this order: the seller's offer (the 402 answer itself), the signed `PAYMENT-SIGNATURE` header your client sends back, or the receipt on Base after settlement. Each hop can fail without any error message, and a plain `curl` shows only the first one. Below, we take the hops one at a time, show what goes wrong silently, and show what our three small paid checks return for each. We ran all of them on 16 October 2026.

People do look for this. In Bing's United States data for three months, "x402" had 287 searches, "http 402" 117 and "402 error" 110, while every phrase with "validator", "debug" or "checker" next to x402 had none. So the likely reader is someone holding a 402 and no idea what it means.

## What an HTTP 402 means in x402

Retrying a 402 gets you nowhere, because it is a quote rather than a fault: the price, the wallet, and a request to sign and return. The x402 version we use puts that offer in a `PAYMENT-REQUIRED` response header as well as in the body. The official client packages read the header and ignore the body. If the two disagree, or the header is missing, a program can see a perfectly readable page and still be unable to pay.

## Hop 1: the seller's offer

Most failures start here, and the buyer cannot fix them. Our check, `x402-offer-check`, takes a public https address and asks it from the edge of Cloudflare with three methods: GET, POST and OPTIONS. It then parses the 402 with the same library a buyer uses and says whether a standard version 2 client could pay. It costs 0.01 USDC per call. The free example, with the input schema and a recorded output, is at `https://api.aiskills402.com/v1/run/x402-offer-check/example`.

The strongest evidence we have is a snapshot of the ten storefronts that were listed in the CDP Bazaar catalog on 16 October 2026, one of them ours. All ten got the basics right: the header was present, `extra` was there to sign against, and the catalog metadata was complete. Our own storefront came out with no findings. The other nine listed storefronts are where the counts come from. We do not name them: the point is the pattern, not a ranking.

| What the check found | Storefronts out of nine |
|---|---|
| The 402 has no `cache-control: no-store` | 7 |
| The `PAYMENT-REQUIRED` header differs from the body | 5 |
| The other method answers 404 | 5 |
| The header is not exposed to browsers by CORS | 4 |
| The CORS preflight (OPTIONS) fails | 4 |
| Non-ASCII characters in the requirements | 4 |
| The other method answers with an HTML page | 2 |
| A price above 1 USD, which a default client refuses | 1 |

Counting all ten, nine could be paid by a standard version 2 client and none had a blocking finding. So what a catalog looks at was fine everywhere. The findings sit in what it does not look at. A browser agent that cannot read the header, a response cached at the edge, or a body that differs from the header are all things a buyer meets only at payment time, with nothing in the log.

In our own paid run against a listed foreign storefront, the answer was `reachable: true` and `payable_by_v2_client: true`. The probes returned GET 402, POST 404 and OPTIONS 204, and the findings were a warning for the 404 on the other method and an info note for the caching header. The check finished in 1,342 ms. Replaying that payment header with an identical body returned a `409 already_paid` with the receipt instead of a new, unpaid measurement, and the same header with a different body returned a 402 saying the payment was used for another purchase.

### The control that was broken by construction

One honest story. For a dead address we expected an answer saying it is unreachable. The first attempt returned `503 check_unavailable` and charged nothing. The reason was ours: the internal control that proves the checker itself has network access pointed at our own storefront, and in our test a Cloudflare Worker calling its own custom domain did not reach itself: the tail log showed zero incoming requests. We moved the control to a trace page on a foreign Cloudflare zone, counted as healthy only on a 200 with an `ip=` line. After that, the dead host returned `reachable: false` with a blocking finding, `unreachable_from_edge`, in 106 ms.

This is also the rule we want from any paid check: when the check itself cannot run, the buyer is not charged. The same holds for bad input. An `http://` address, a bare IP, our own host and a `workers.dev` address all returned a 400 with the sentence that nothing was settled or charged, and an unreadable payment header returned a 402 before any work began.

## Hop 2: the signed PAYMENT-SIGNATURE header

Now the buyer's side. The client signs a USDC authorization and sends it in `PAYMENT-SIGNATURE`: base64 text wrapping UTF-8 JSON. Three silent failures live here. The receiving server decodes the text with a bare `atob` instead of as UTF-8, so one non-ASCII character in the echoed description breaks the whole payload. The authorization window is longer than it should be. Or the signed amounts do not match the requirements.

`payment-header-check` reads a header you are about to send, or one you received, and never settles anything. It decodes it, tells you whether base64 and UTF-8 were fine, checks the validity window against the clock, recovers the signer from the signature and, when you also pass the seller's `PAYMENT-REQUIRED` value, compares recipient, amount, network and asset. It costs 0.005 USDC. Free example: `https://api.aiskills402.com/v1/run/payment-header-check/example`.

We ran it on the public test vector from that example, a header whose recipient is the zero address and which cannot be paid. The signature lane was `a`, which means the signature recovers to the `from` address. The clock state was `valid`, and the one finding was `expiry_window_long`: the window ran far beyond 300 seconds, which leaves a signed authorization exposed to replay for longer than needed.

```json
{
  "decoding": { "base64": "standard", "utf8": true, "json": true, "non_ascii": false },
  "signature": { "bytes": 65, "lane": "a" },
  "clock": { "state": "valid" }
}
```

That block is trimmed from the recorded example output. Treat the header as a secret until it is spent: the check reads it on our side and sends it nowhere else, but you should still prefer test vectors when you are only learning.

## Hop 3: the receipt on Base

After settlement you hold a transaction hash. A block explorer such as Basescan is the usual place to look, and it shows the transaction faithfully. One thing surprises people: the From field shows the relayer that submitted the transaction, not the wallet that paid, because the buyer signs and the facilitator sends. The payer appears only in the USDC `Transfer` and `AuthorizationUsed` logs. That is how the chain works, not a flaw of any explorer.

`base-receipt-verify` takes a transaction hash plus the expected recipient and amount, and optionally the expected payer and nonce. It asks a Base RPC node directly, not an explorer, and returns a verdict. It costs 0.005 USDC. Free example: `https://api.aiskills402.com/v1/run/base-receipt-verify/example`.

We ran it on the transaction of our own earlier test purchase. The verdict was `confirmed`, with 2,975 confirmations, recipient and amount both matching (3,000 micro-units, which is 0.003 USDC), answered by one node in 88 ms. In the receipt the sender was the relayer.

The most useful sentence came from a second call. We sent a made-up hash together with the `from` and `nonce` of that real payment. The verdict was `not_found`, the nonce state was `used`, and two findings came back, one of them saying that the authorization is spent, under another hash. Trimmed to the codes, the answer looked like this:

```json
{
  "verdict": "not_found",
  "nonce_state": "used",
  "findings": ["authorization_used_under_another_tx", "single_node_answer"]
}
```

Why it matters: a "not found" for a hash, taken alone, reads as "you were not charged". With the payer and nonce in hand, it can mean the opposite, that the money moved in a transaction you are not looking at. The second finding is a reminder that only one node answered; the call took 213 ms.

## A path through all three

1. Ask the address without paying, and read the `PAYMENT-REQUIRED` header, not only the body.
2. Run the offer check if the seller is not yours. A blocking finding means a standard client cannot pay, and you should stop there.
3. Run the header check on what your client is about to send, with the seller's header attached for the cross-check.
4. After settlement, run the receipt check with recipient, amount, payer and nonce, so a wrong hash is told apart from an unpaid call.

The live list of all our endpoints and their current prices is at `https://api.aiskills402.com/v1/run`; the price in the 402 you receive is the one that counts.

## When this does not apply

Everything above comes from a single evening, so weigh these limits before you generalise.

- **Base mainnet only.** We did not look at other chains, tokens or payment schemes.
- **Our wallets only.** Every payment in this record went from our wallet to our wallet, so no foreign client is in this record and the net amount moved was zero.
- **Listed storefronts only.** The ten storefronts came from one catalog. Unlisted and private sellers were not measured, and ours was counted separately from the nine.
- **One moment.** The snapshot is from 16 October 2026 at about 17:00 UTC. Sellers change, and a finding may be gone tomorrow.
- **One viewpoint.** The probes ran from Cloudflare's edge. An agent running elsewhere can meet different answers, for example different caching or a different network block.
- **Five paid calls.** The outputs above are single runs, not a rate of success.
- **A check is not a guarantee.** A clean result says what we saw in that moment; it does not promise that a seller will deliver.
