grug402 — HTTP 402 reference catalog
A reference catalog of HTTP 402 (x402) payment endpoints — compliant flows alongside deliberate protocol quirks — for testing how your x402 client handles real verify and settle on the Radius testnet.
Network: eip155:72344 (chain 72344) · 84 endpoints
| Status | Method | Endpoint | Summary |
|---|---|---|---|
| Compliant | GET | Canonical GET (real verify and settle) | The textbook x402 flow: an unpaid GET gets a 402 with a PAYMENT-REQUIRED challenge, then the server runs a real /verify and /settle against the Radius testnet facilitator. |
| Compliant | POST | Canonical POST with body | The same compliant flow as the canonical GET, for a POST that carries a JSON body. |
| Compliant | GET | Minimal price (1 atomic unit) | A compliant endpoint priced at the smallest atomic unit, 0.000001 SBC. |
| Compliant | GET | Single testnet accepts entry | Advertises one testnet requirement that a correct client selects by matching its network. |
| Compliant | GET | Correct idempotency (cached retry) | Caches the result by payment id, so retrying the same payment returns the cached 200 without charging twice. |
| Compliant | GET | Weather API (pay-per-call) | A themed but fully compliant data endpoint, gated by a real settlement. |
| Compliant | GET | FX rate API (pay-per-call) | A themed compliant endpoint that returns an exchange rate after settlement. |
| Compliant | POST | Haiku generator (POST, paid) | A compliant POST endpoint that returns generated content after settlement. |
| Quirk | GET | Returns 401 instead of 402 | Gates the resource with 401 Unauthorized instead of 402 Payment Required. |
| Quirk | GET | Returns 403 instead of 402 | Gates the resource with 403 Forbidden instead of 402. |
| Quirk | GET | Returns 400 instead of 402 | Gates the resource with 400 Bad Request instead of 402. |
| Quirk | GET | Returns 200 with content (no gate) | Serves the protected content for free with a 200 and no payment gate. |
| Quirk | GET | 402 without PAYMENT-REQUIRED header | Returns a 402 but omits the PAYMENT-REQUIRED header entirely. |
| Quirk | GET | PAYMENT-REQUIRED is raw JSON (not base64) | Sends the PAYMENT-REQUIRED requirements as raw JSON instead of base64. |
| Quirk | GET | PAYMENT-REQUIRED is invalid base64 | The PAYMENT-REQUIRED header value is not valid base64. |
| Quirk | GET | Base64 decodes to non-JSON | The PAYMENT-REQUIRED header is valid base64 but decodes to bytes that are not JSON. |
| Quirk | GET | Atomic amount as a JSON number | Emits the atomic amount as a JSON number instead of a string. |
| Quirk | GET | Missing x402Version | The PAYMENT-REQUIRED body omits the x402Version field. |
| Quirk | GET | Legacy v1 headers (X-PAYMENT) | Emits the legacy v1 X-PAYMENT header instead of the v2 PAYMENT-REQUIRED challenge. |
| Quirk | GET | Empty accepts array | The PAYMENT-REQUIRED challenge advertises an empty accepts array. |
| Quirk | GET | Unsupported payment scheme | Advertises a payment scheme the facilitator does not support. |
| Quirk | GET | Advertises mainnet, settles on testnet | Advertises a mainnet network id while the facilitator runs on testnet. |
| Quirk | GET | Wrong token contract address | Advertises an asset contract address that is not SBC. |
| Quirk | GET | Wrong decimals (18 instead of 6) | Expresses the amount with 18 decimals for a token that has 6. |
| Quirk | GET | Wrong payTo address | Advertises a payTo address that is not the merchant's wallet. |
| Quirk | GET | Missing resource URL | The PAYMENT-REQUIRED challenge omits the resource object. |
| Quirk | GET | Already-expired quote (timeout 0) | Sets maxTimeoutSeconds to 0, so the quote is dead on arrival. |
| Quirk | GET | Overlong validity window (1 year) | Sets maxTimeoutSeconds to a full year, widening the replay window. |
| Quirk | GET | Price changes every request | Increases the advertised amount on every request. |
| Quirk | GET | Dollar-string price ($0.0001) | Advertises the amount as a human dollar string instead of atomic units. |
| Quirk | GET | Sensitive data in metadata | Leaks fake PII into the resource description before any payment. |
| Quirk | GET | Skips /verify, settles directly | Goes straight to /settle without a /verify pre-check. |
| Quirk | GET | Grants on /verify, never settles | Grants access once /verify succeeds and never calls /settle. |
| Quirk | GET | No verification at all | Grants access on the mere presence of a PAYMENT-SIGNATURE header. |
| Quirk | GET | Trusts client-supplied result | Believes a client-asserted payment result without checking the facilitator. |
| Quirk | GET | Fails open when the facilitator is down | Serves the resource anyway when the facilitator is unreachable. |
| Quirk | GET | Replay after success (no binding) | Once a payment settles, the same payload re-serves the resource forever with no re-settle. |
| Quirk | GET | Serves before settlement (verify/settle gap) | Returns 200 immediately and settles in the background. |
| Quirk | GET | CORS hides payment headers | Does not expose the PAYMENT-REQUIRED and PAYMENT-RESPONSE headers via CORS. |
| Quirk | GET | HEAD reveals paid metadata | A HEAD request returns 200 with a leak header and no payment. |
| Compliant | POST | upto: settles actual usage | Authorizes a maximum but settles only the actual usage (~60%). |
| Quirk | POST | upto: always charges the max | Settles the full authorized max even when usage is lower. |
| Quirk | POST | upto: override exceeds the max | Sets the settlement amount above the authorized maximum. |
| Quirk | POST | upto: settles zero (free) | A settlement override of 0 gives the service away by accident. |
| Quirk | GET | Dead facilitator URL | Points /verify and /settle at a facilitator host that does not exist. |
| Quirk | GET | Facilitator can't support the network | Advertises a network the facilitator does not handle. |
| Quirk | GET | Facilitator rate-limits /verify (429 → misclassified 402) | The facilitator returns 429 during /verify; the merchant misclassifies it as a payment failure and returns 402 instead of 503 + Retry-After. |
| Quirk | GET | Facilitator temporarily unavailable (503) | The facilitator is reachable but returns 503 Service Unavailable — it is not a dead URL, just temporarily down. |
| Quirk | GET | Settle returns 500 (ambiguous paid state) | /verify succeeds but /settle returns 500 Internal Server Error, leaving it unknown whether the payment landed on-chain. |
| Quirk | GET | Settle succeeds without txHash | /settle reports success: true but omits every transaction-hash field; the PAYMENT-RESPONSE receipt carries no on-chain proof. |
| Quirk | GET | Non-JSON facilitator response leaked in 402 | The facilitator returns an HTML maintenance page instead of JSON; the merchant forwards the raw HTML in the 402 response body, leaking internal infrastructure details. |
| Quirk | GET | Schema drift: valid vs isValid (loose check grants) | The facilitator returns { valid: true } instead of the spec's { isValid: true }; the merchant's loose field-name check grants access on the wrong key. |
| Quirk | GET | No timeout on /verify (hangs indefinitely) | The merchant's /verify fetch has no AbortController or timeout; when the facilitator is slow the handler ties up the connection forever. |
| Quirk | GET | Wrong payTo used at settle time | The merchant calls /settle with a payTo address different from the one advertised in the challenge; the facilitator rejects it because the payment was signed for a different recipient. |
| Quirk | GET | Grants access despite settle failure | When /settle returns a failure (e.g., already used), the merchant grants access anyway — a replay attacker can reuse any previously-rejected permit for free. |
| Quirk | GET | Concurrent settle race (second racer gets 402) | Two concurrent requests carry the same PAYMENT-SIGNATURE; without a per-permit lock the merchant lets both race to /settle. The facilitator accepts only one — the second racer is charged but gets a confusing 402. |
| Quirk | GET | Settles, then the handler fails | The payment settles on-chain, then the handler returns 500. |
| Quirk | GET | Success without PAYMENT-RESPONSE | Settles and returns 200 but omits the PAYMENT-RESPONSE header. |
| Compliant | GET | Joke API (pay-per-call) | A compliant GET that returns a joke after a real settlement. |
| Compliant | GET | Dictionary API (pay-per-call) | A compliant GET that returns a definition after settlement. |
| Compliant | GET | Quote API (pay-per-call) | A compliant GET that returns a quote after settlement. |
| Quirk | GET | Wrong asset transfer method | Sets extra.assetTransferMethod to something other than permit2, so the facilitator can't move funds. |
| Quirk | GET | Advertises a zero price but still gates | Advertises an amount of 0 yet still demands a payment header. |
| Compliant | POST | batch-settlement: deposit, then reconcile usage | Advertises the batch-settlement scheme: the client authorizes a 5x deposit, and the server settles only the actual (much lower) usage. |
| Quirk | POST | batch-settlement: settles beyond the deposit | Settles more than the deposit the client authorized under the batch scheme. |
| Quirk | POST | batch-settlement: never reconciles down | Holds the full authorized deposit and never settles the lower real usage. |
| Compliant | GET | Genuine multi-entry accepts (match by network) | Advertises two EVM entries — Base Sepolia first, Radius testnet second — so the client must match by network instead of taking accepts[0]. |
| Quirk | GET | SVM-only requirement (solana:*) | Advertises only a Solana (solana:*) requirement, which an EVM testnet client cannot satisfy. |
| Compliant | GET | Cross-VM accepts (EVM and SVM together) | Advertises a Solana entry and a Radius testnet EVM entry together, forcing the client to select the VM and network it can settle on. |
| Quirk | GET | Overpriced vs per-call cap | Advertises a price far above a sane per-call cap that an MCP policy gate would enforce. |
| Quirk | GET | Resource host mismatch | The advertised resource origin differs from the server actually being called. |
| Quirk | GET | Tool / operation mismatch | The requirement authorizes a different, more privileged tool than the one being requested. |
| Quirk | GET | Session price escalation | The price climbs call over call (1x, 2x, 3x...) to defeat a per-session spend limit. |
| Compliant | GET | Correct edge cache headers (compliant) | The 402 challenge is no-store; the paid 200 is private, no-store with Vary: PAYMENT-SIGNATURE — and it still does a real verify and settle. |
| Quirk | GET | Cacheable 402 challenge | Sends the 402 with Cache-Control: public, max-age, so a shared cache stores and replays a stale challenge. |
| Quirk | GET | Cacheable paid response (shared cache) | Sends the paid 200 publicly cacheable, so a shared edge serves paid content to unpaid clients on the same URL. |
| Quirk | GET | Missing Vary on payment | Caches the paid 200 but ignores the payment header in the cache key (no Vary), so one payer's response is served to another. |
| Quirk | GET | stale-while-revalidate leak | A stale-while-revalidate directive lets the edge serve cached protected content without payment during revalidation. |
| Quirk | GET | Oversized challenge header | Pads the PAYMENT-REQUIRED header beyond common proxy header-size caps, so an edge truncates, drops, or 431s it. |
| Quirk | GET | Conditional 304 replay | The paid resource carries an ETag, and a later conditional request is answered 304 from cache without re-payment. |
| Quirk | GET | Rate-limited before payment | Returns 429 (short Retry-After, no challenge) before the 402, so a willing payer can't tell a rate limit from a paywall. |
| Quirk | GET | WAF blocks the payment payload | The large base64 PAYMENT-SIGNATURE trips a managed WAF rule and is rejected with 403 before the payment logic runs. |
| Quirk | POST | Body too large (413) | A paid POST is rejected by a small edge body cap with 413 before reaching the payment logic. |
| Quirk | GET | Edge timeout during settle | Settlement is slow and the edge returns 504 while the charge may have completed on-chain — an ambiguous paid state. |