x402
| ID | Check Name | Spec Reference | What It Checks | Pass Criteria |
|---|---|---|---|---|
X402-01 | 402 Response Status | x402 spec, HTTP semantics | An unpaid request must be answered with status code 402 | Response status is exactly 402, not 401/403/other |
X402-02 | Payment Header Present | x402 built-on-stellar guide | The 402 response must include a payment header | Either PAYMENT-REQUIRED or X-Payment header is present (both are checked — the spec itself is not yet consistent, see note in README). An x402 v1 challenge in the response body fails this check, and X402-03–05 still inspect it; see the note on v1 below |
X402-03 | Header Payload Decodable | x402 spec §payment-required-object | The header value must be valid base64 that decodes to JSON | atob() + JSON.parse() succeed without error |
X402-04 | Required Fields Present | x402 spec §payment-required-object | The payload must include the core payment terms, under the field names its own advertised version requires | Checked in every payment option in accepts, each reported by its index when the challenge offers more than one; a challenge with no options fails. In each option, every field the advertised x402Version requires is present: for v2 scheme, network, amount, asset, payTo and maxTimeoutSeconds (spec 5.1.2); for v1 scheme, network, maxAmountRequired, asset, payTo, resource, description and maxTimeoutSeconds (v1 spec 5.1). Each is a non-empty string, except maxTimeoutSeconds, a positive number of seconds; a field present with the wrong type is reported as such rather than as missing. The price field follows the version: maxAmountRequired for v1, amount for v2 (renamed in v2, which also moves the resource out of the option). The version is read from the challenge rather than accepting whichever name happens to appear, because a service advertising x402Version: 2 while emitting the v1 field name is not conformant to the version it claims — and reporting that as a merely absent price would hide the actual defect. An unrecognised version fails: the field names cannot be checked against a version whose schema is unknown. |
X402-05 | Network Identifier Valid | x402 v2 spec §11.1; CAIP-2 | Every advertised network id is CAIP-2 | Every option's network is a CAIP-2 identifier, namespace:reference with a 3 to 8 character lowercase namespace and a reference of at most 32 characters, as x402 v2 requires. Where the namespace's own CAIP-2 definition fixes the reference, that is checked too: stellar is testnet or pubnet; eip155 is the chain id in base 10 (eip155:84532, not eip155:0x14a34); solana is the first 32 characters of the base58 genesis hash. A well-formed id in another namespace passes, and the result says only the format was checked there: x402 v2 asks for CAIP-2 and nothing more, so failing it would report Wasit's own lack of rules as the target's defect. An option without a network is left to X402-04. |
X402-06 | Signature Resubmit Accepted | x402 spec §payment-flow | A resubmitted request carrying a valid signature must be accepted | Response is no longer 402; a 2xx returns the original resource. The challenge is re-read immediately before signing, so the payment answers a challenge the target issued just now rather than a stale one. Settles a real payment — see the cost note below. A 2xx alone does not pass. The settlement the response reports in its PAYMENT-RESPONSE header (x402 v2 HTTP transport) must name a Stellar transaction hash, and that transaction is then looked up on Stellar RPC and held to the advertised terms exactly as MPP-01 does: it must have succeeded and emitted exactly one transfer event, from this run's payer, to the advertised payTo, for the advertised amount of the advertised asset. A missing header, a reported failure, or a hash that does not match fails. A settlement_pending response, which the spec defines as broadcast but unconfirmed, is reconciled on chain rather than failed. Uses the same RPC wait as MPP-01. On Base Sepolia (eip155:84532) the payment uses the transfer method the target advertises: EIP-3009 by default, or Permit2, whose one-time approval the client signs as an EIP-2612 permit when the target offers the eip2612GasSponsoring extension. A Permit2 target without it, where this run's payer has not approved Permit2, gives no verdict (ERROR (setup)): the missing approval is the payer's, not the target's. The reference is an EVM transaction hash, and the receipt's ERC-20 Transfer log is held to the same terms (a Permit2 settlement also logs the permit's Approval, which is not a transfer): the transaction succeeded and logged exactly one token transfer, from this run's payer, to payTo, for amount of asset (an ERC-721 transfer, which shares the event signature, is not counted). The receipt is awaited for 30 blocks before the transaction counts as missing; an RPC that stops advancing gives no verdict. On Solana devnet (solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1) the client signs a transaction with one TransferChecked to the payee's associated token account, and the sponsor named in extra.feePayer signs as fee payer and submits it, so the payer needs no SOL. The reference is a transaction signature, and the confirmed transaction's token balances are held to the same terms: it succeeded, and exactly one transfer moved (one account debited and one credited, by the same amount of the same mint), for amount of asset, to an account payTo owns, from this run's payer. The transaction is awaited for 150 slots before it counts as missing; an RPC that stops advancing gives no verdict. |
X402-07 | Invalid Signature Rejected (negative) | x402 spec §payment-flow; exact scheme on Stellar | A payment whose authorization signature is wrong must be REJECTED | The target answers with a non-2xx status. The payment is built exactly as for X402-06, then only the client's authorization signature is corrupted: in the exact scheme on Stellar the client signs a Soroban authorization entry rather than the envelope, and one byte of that entry's signature is flipped. The transaction still decodes and carries the same amount, payer and recipient, so a target can refuse it only by verifying the signature. Measured against the x402.org facilitator on 2026-09-30, the rejection is invalid_exact_stellar_payload_simulation_failed, reached at the Soroban simulation that checks authorization; the pre-0.6.0 corruption, which overwrote the base64 tail and broke XDR decoding, drew invalid_exact_stellar_payload_malformed instead, and a target that decoded the envelope without verifying the signature passed it. Rejection is established only by an answer: a target that cannot be reached, or whose challenge cannot be read, produces no verdict and is reported as ERROR or SKIP, and a payload with no authorization signature to corrupt reports ERROR (setup). On Base Sepolia the client signs an EIP-3009 transferWithAuthorization, or a Permit2 transfer, off-chain; the first byte of that signature is flipped and the authorization left intact, so the signer it recovers to is no longer the payer and only signature verification can refuse it. Measured against the x402.org facilitator on 2026-10-05: refused with 402. On Solana devnet the transaction carries one signature before the sponsor signs, the payer's; its first byte is flipped, and the message, transfer and blockhash are left intact. Measured against the x402.org facilitator on 2026-10-05: invalid_exact_svm_payload_signature_invalid. |
X402-08 | Payment Replay Rejected (negative) | x402 v2 spec §10.1; exact scheme, one-time use | A payment that was already accepted must not be accepted again | The headers X402-06's accepted payment was sent with are sent again, byte for byte, and the target answers with a non-2xx status. Each authorization is single-use: once the payment settles, its EIP-3009 nonce, or its Soroban auth entry's nonce, is spent, or on Solana the transaction has already executed, and the facilitator's verify refuses it. A 2xx means one payment bought the resource twice. Nothing can settle twice, so this costs nothing. Skipped when the challenge advertises the payment-identifier extension, under which a server may legitimately answer a repeated payment with its cached response. |
X402-09 | Underpayment Rejected (negative) | exact scheme verification: amount | A validly signed payment for less than the advertised amount must be rejected | The payment is signed for half the advertised amount, while its accepted still claims the advertised terms, so the signature is valid and only the amount is wrong; the target answers with a non-2xx status. On Stellar the facilitator must hold the transfer to requirements.amount exactly; on EVM, verification step 3 holds the authorization's value to it; on Solana the scheme forbids a transfer below it (§1.4), and the reference facilitator's static path requires the exact amount (§3.1). Skipped when the price is 1 base unit, since nothing smaller can be offered. A target that accepts may settle the lower amount. |
X402-10 | Expired Authorization Rejected (negative) | x402 v2 spec §10.1 (time constraints); exact scheme verification: validity window | A payment whose authorization has expired must be rejected | The payment is signed with a one-second lifetime, which both SDK clients derive from maxTimeoutSeconds (EVM validBefore, the Stellar auth entry's expiration ledger), held until that has passed (5 seconds on Base Sepolia, 20 on Stellar, three or more ledgers), and sent claiming the advertised terms. On Solana a payment's lifetime is its recent blockhash, which the SDK client does not take from maxTimeoutSeconds: the payment is built, through the client's extra.recentBlockhash hint, on a blockhash 300 slots old that the RPC confirms is no longer valid, and sent at once. The target answers with a non-2xx status. An expired authorization cannot settle, so a target that serves it serves for nothing. |
Note on the x402 payment checks' cost (Week 2). X402-06 and X402-07
are not free. X402-06 settles a real payment against the target, and X402-07
attempts one with a corrupted signature; both move or risk moving testnet funds
from the payer key, and repeated runs spend repeatedly. Like MPP-01 this is
inherent rather than an implementation choice — a payment flow that was never
exercised cannot be verified. X402-01 through X402-05 read the challenge
only and cost nothing; --read-only (CLI) or readOnly: true (MCP) restricts a
run to those. The payment checks are also skipped entirely when no payer key is
present, so the default posture is the cheap one.
Note on the negative payment checks (0.7.0). X402-07 through X402-10 each send a
payment the target must refuse, and pass when it does. A refusal only means something
from a target that accepts a valid payment: one that refuses everything would pass them
all. So they run only after X402-06's valid payment was answered with a 2xx, and are
skipped otherwise, with that reason. They run in catalogue order after X402-06.
Note on settlement timing (0.7.0). There is no check that a target settles before it
serves, because the protocol does not require it. x402 v2's default authorization flow
is verify, run the resource, settle, then respond (spec §6.1); only a flow declared as
upfront or escrow in extra.paymentFlow settles first. What the client can observe,
that the response arrives with a settlement that happened, X402-06 already requires.
Note on cascading failures (Week 2). The read-only checks inspect
progressively deeper parts of one challenge: the status, then the header, then
its payload, then the fields inside it. When one fails, the checks after it have
nothing left to inspect, and they are skipped rather than failed. A target
answering 404 produces one finding, not five. The same applies across the
payment checks: when X402-06 cannot exercise the payment flow at all,
X402-07 is skipped rather than credited with a rejection it never observed.
Note on x402 v1 challenges (0.5.0). x402 v1 signals payment in the 402
response body, as a PaymentRequirementsResponse with x402Version: 1
(transports-v1/http.md).
v2 moved it into the PAYMENT-REQUIRED header
(transports-v2/http.md),
and the exact scheme on Stellar is defined for v2 only, with CAIP-2 network
identifiers (scheme_exact_stellar.md:
"❌ v1 - we don't plan to support v1 for now"). So a v1 challenge from a
Stellar service fails X402-02, and the failure says a v1 challenge was found
in the body. Its terms are still worth reading, so X402-03–05 inspect the
body instead of being skipped: X402-04 applies the v1 field names, and
X402-05 reports whether the network is a CAIP-2 identifier (v1 used plain
names such as base-sepolia, and the failure says so). X402-06 and
X402-07 are skipped: Wasit pays through the v2 exact scheme, so no
payment is built or sent, and neither check has a verdict. The same holds for
any challenge the payment client cannot read. Before 0.5.0 both reported FAIL
in that case, contradicting the X402-07 row above, and a v1 challenge left
X402-03–05 skipped.
Note on networks (0.7.0). X402-01–05 read the challenge only, so they
apply to an x402 service on any chain. The payment checks pay through the
exact scheme on the network the run names: stellar:testnet (the default),
stellar:pubnet, eip155:84532 (Base Sepolia, with the EIP-3009 or Permit2
method, where the facilitator pays the gas), or
solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 (Solana devnet, where the facilitator signs as
fee payer). When a challenge offers several options, the payment is built for the option
on that network; a challenge with no such option, or only one in another scheme, gets
X402-06–10 skipped with the networks it does offer, since nothing was paid and nothing refused. Asking for any other network, for pubnet without an RPC endpoint, or paying
with a key for the wrong chain, stops the run before any payment, as no
settlement could be verified.