Skip to content

Troubleshooting

Why x402 payments fail: the common errors and how to fix them

By Requestway · · 9 min read

When x402 payments fail, the protocol tells you why: every rejection carries a reason code, and every code points at a specific mistake on either the paying client or the server. This guide walks through the stages where a payment can fail, the error codes you will actually see, and the fix for each one on both sides of the request.

The four stages where a payment can fail

An x402 request goes 402, sign, retry, verify, settle. Failures happen at four points, and the stage matters as much as the reason because it decides what the client received.

Stage What happened HTTP status Was the resource served?
decode The payment header could not be decoded (bad base64 or JSON) 400 No
verify Local checks or the facilitator's /verify rejected the payment 402 No
settle The facilitator's /settle failed; the response is discarded 402 No
settle_after_response Settlement failed after the response was already sent 200 already sent Yes

There is also a fifth outcome that is not a rejection at all: the facilitator could not be reached. The Laravel package reports this as facilitator_unavailable and answers 502.

The first three are safe in the sense that nobody lost anything: no funds moved and nothing was served. The fourth only happens if you opted into after_response settlement, and it is the one case that needs a human.

Read the reason before you guess

Most debugging time goes on guessing. Get the reason code first.

In Laravel, every real payment attempt that was rejected is stored in the payments table with its failure_reason, once you have run the migration. Group the last day's failures to see what dominates:

use Requestway\X402\Models\X402Payment;

X402Payment::query()
    ->failed()
    ->between(now()->subDay(), now())
    ->get()
    ->groupBy('failure_reason')
    ->map->count()
    ->sortDesc();

Payloads that could not be decoded at all are logged but not stored, so random noise cannot grow the table. While you are debugging a client that cannot form a valid payment, set X402_RECORD_MALFORMED=true to keep those too.

In Node, the resource server exposes failure hooks. The reason is on the error object:

const reasonOf = (e: Error & { errorReason?: string; invalidReason?: string }) =>
  e.errorReason ?? e.invalidReason ?? e.name

server
  .onVerifyFailure(async (ctx) => {
    console.warn('x402 verify failed', reasonOf(ctx.error), ctx.requirements.network)
  })
  .onSettleFailure(async (ctx) => {
    console.error('x402 settle failed', reasonOf(ctx.error), ctx.requirements.network)
  })

On the client, a rejected payment comes back as another 402 rather than an exception, so check the status after the paid call instead of assuming success:

const response = await fetchWithPayment('https://api.example.com/premium-data')
if (response.status === 402) {
  console.error('Payment rejected:', await response.text())
}

Decode failures (HTTP 400)

A 400 means the server could not even read the PAYMENT-SIGNATURE header as base64-encoded JSON. Nothing was verified and nothing was charged.

Client fix: use an x402 client library rather than assembling the header by hand. The usual hand-rolled mistakes are URL-safe base64 instead of standard base64, a JSON object that was stringified twice, and a header copied from logs with whitespace in it.

Server fix: usually none. If a known client keeps sending 400s, turn on X402_RECORD_MALFORMED temporarily, look at what arrives, then turn it off again.

Verify failures (HTTP 402)

Verification runs before your controller. The Laravel package checks a lot locally (amount, asset, payTo, network, resource, validity window, replay) before it calls the facilitator's /verify, so many of these codes never cost a network round trip.

insufficient_funds

Cause: the paying wallet does not hold enough of the required asset on the network it is paying on. The classic version: the wallet holds USDC on Base, but the server advertised Base Sepolia, or the reverse.

Client fix: fund the wallet with the right token on the right network. For Base Sepolia, test USDC comes from https://faucet.circle.com (choose USDC and Base Sepolia). The agent does not need ETH; the facilitator pays gas.

Server fix: check which network you are advertising. The Laravel package defaults to Base Sepolia so an unconfigured install cannot move real money, which also means a production deploy that never set X402_NETWORK=eip155:8453 asks mainnet-funded agents to pay on a testnet where they hold nothing, and the ones that try fail with this code. If your callers hold funds on several chains, advertise more than one with X402_ADDITIONAL_NETWORKS.

invalid_exact_evm_payload_authorization_value_mismatch

Cause: underpayment. The signed amount is below what the route requires. Amounts are atomic units: for USDC, 10000 is $0.01 and 1000000 is $1.00. A client that signs 1 meaning one cent has offered a millionth of a dollar.

Client fix: take the amount from the accepts entry in the current 402, as a string, and do not convert it. Do not cache requirements across requests; prices can change.

Server fix: if you use dynamic pricing, make sure the same request produces the same price on the unpaid call and the paid retry. A closure passed to X402::priceUsing() that depends on the time, a counter or a random value will advertise one price and then demand another.

invalid_exact_evm_payload_recipient_mismatch

Cause: the authorization pays an address other than the payTo the server expects.

Client fix: sign to the payTo from the 402 you just received. Never hard-code a recipient.

Server fix: look for configuration drift. If one web node has a different X402_PAY_TO from another, one node advertises address A and the next one verifies against address B. The same happens briefly after you change a route's pay_to= option while clients still hold old requirements.

invalid_exact_evm_payload_signature

Cause: the signature does not match the data it claims to sign. For the exact scheme the payer signs an EIP-3009 transferWithAuthorization with EIP-712, and the EIP-712 domain includes the token's name and version from the extra field plus the chain and token contract. Get any of these wrong and the signature is valid for something else.

Client fix: use the official scheme implementation (ExactEvmScheme from @x402/evm) instead of building typed data yourself, and sign with the same account that appears as the payer.

Server fix: make sure extra carries the correct name and version for the token on each network you advertise. If you have overridden asset configuration, this is the first place to look. The Requestway endpoint tester checks exactly these fields.

Validity window: valid_after and valid_before

invalid_exact_evm_payload_authorization_valid_after means the authorization is not valid yet. invalid_exact_evm_payload_authorization_valid_before means it has expired.

Client fix: sign immediately before sending, not when the task is planned. Set validAfter slightly in the past and keep the machine clock synchronised. Never retry an old payment header after a long pause.

Server fix: check the server clock too; the package compares against it with a small skew tolerance. If slow clients routinely expire, raise the advertised window per route with timeout=, bearing in mind X402_MAX_TIMEOUT_SECONDS (default 60) caps it.

invalid_network, invalid_scheme, unsupported_scheme

Cause: the client paid on a network or with a scheme the server or facilitator does not accept.

Client fix: choose an entry from accepts and register a scheme client for its network (network: 'eip155:*' covers all EVM chains with @x402/fetch).

Server fix: the facilitator's /supported must list your scheme and network. https://x402.org/facilitator is testnet only, so advertising eip155:8453 with it fails. php artisan x402:doctor checks /supported for you.

invalid_x402_version

Cause: the client speaks a protocol version the server does not accept. Version 1 put the payment in X-PAYMENT; version 2 uses PAYMENT-SIGNATURE.

Fix: move the client to the current v2 packages. On the server, check which versions your middleware accepts.

invalid_payload, including replays

Cause: a structurally broken payload, or a payment that has already been used. The Laravel package answers a replay with "This payment has already been used. Sign a new one." Replay protection claims each payload's fingerprint in the cache before verification, so a replay never reaches the facilitator.

Client fix: sign a new payment for every request. Watch out for generic HTTP retry logic that resends the same headers after a timeout.

Server fix: put the replay cache in a store shared by every web node (X402_REPLAY_STORE). With per-node caches, a replay sent to a different node is only caught later, by the token contract refusing to execute the same authorization twice.

invalid_payment_requirements and unexpected_verify_error

invalid_payment_requirements points at the terms rather than the signature: the requirements the client echoed back do not match what was advertised, or the facilitator does not accept them. Clients should return the chosen accepts entry unchanged; servers should run x402:doctor, which validates assets and wallet format. unexpected_verify_error means the facilitator itself failed; check its URL and credentials (X402_FACILITATOR_TOKEN, or X402_FACILITATOR_AUTH for signed-header facilitators such as CDP).

Settle failures

Settlement happens after your controller has run. In the default before_response mode a failed settlement discards the response and the client gets a 402, so it never receives something it did not pay for.

  • invalid_transaction_state: the settlement transaction failed on chain. One example cause is an agent firing many paid requests in parallel from a small balance: each one can pass verification, then they compete for the same funds at settlement. Clients should size the wallet for their concurrency; servers have nothing to fix.
  • unexpected_settle_error: the facilitator failed while settling. A timed-out /settle is indeterminate, since the transfer may have gone through, which is why the package never retries settle unless you set X402_FACILITATOR_SETTLE_RETRIES. Check the block explorer before charging again.
  • settlement_pending: the transaction was broadcast but not confirmed. It is not terminal. The package retries once, then records the payment as pending. Look the hash up on https://basescan.org (or https://sepolia.basescan.org) before treating it as lost.

With X402_SETTLEMENT_MODE=after_response, settlement runs after the response is sent. A failure there is recorded as failed and fires PaymentFailed with servedWithoutPayment() returning true. Alert on it:

Event::listen(PaymentFailed::class, function (PaymentFailed $event): void {
    if ($event->servedWithoutPayment()) {
        Log::critical('x402: served without payment', [
            'route' => $event->context->route->label(),
            'amount_usd' => $event->context->amountUsd(),
        ]);
    }
});

When the facilitator is down (HTTP 502)

facilitator_unavailable means the package could not reach the facilitator. Connection failures are retried (X402_FACILITATOR_RETRIES, default 2), then the request gets a 502 and the resource is not served. If you would rather give the resource away during an outage, set X402_ON_FACILITATOR_ERROR=fail_open.

Clients should treat 502 as an outage, not a rejection: back off and retry later with a freshly signed payment.

Reproduce failures before your users do

Every code above can be triggered in a test. With the fake facilitator:

use Requestway\X402\Core\Errors\ErrorCode;

X402::fake()->rejectVerification(ErrorCode::InsufficientFunds);
X402::fake()->rejectSettlement(ErrorCode::InvalidTransactionState);
X402::fake()->pendingSettlement('0xbroadcast');

From the command line, without a wallet or a chain:

php artisan x402:test /report --underpay
php artisan x402:test /report --fail-settlement
php artisan x402:doctor

For a real end-to-end check on Base Sepolia, the endpoint tester pays your route with test USDC and shows which step breaks. The Base Sepolia testing guide covers the rest.

See failures grouped in a dashboard

A single failed payment tells you about one client. Failures grouped by stage and reason tell you about your configuration. Requestway's dashboard does that grouping for you, most frequent first, next to settled and observed counts per day, with mainnet and testnet kept separate.

In Laravel, set X402_PLATFORM_KEY=rw_live_… and run a queue worker; reporting never blocks or fails a request. In Node, call rw.failed({ ...details(ctx), stage: 'verify', reason: reasonOf(ctx.error) }) from the hooks shown earlier. A sudden spike in one reason after a deploy is usually the fastest way to find a bad config change. Create a free account, or read the install guide first.

FAQ

Why does my x402 client get a 402 again after paying?

The payment was rejected at verification or settlement, and the server answered with a new 402 instead of the resource. Read the reason code: underpayment, an expired authorization and a wrong network are the most common causes.

Was I charged if an x402 payment failed?

Not if it failed at decode or verify, because no transaction was submitted. A settle failure in before_response mode means no transfer completed, except for settlement_pending and timed-out settlements, which may still confirm on chain, so check the explorer.

What does invalid_exact_evm_payload_signature mean in x402?

The signature does not match the typed data it claims to sign. It is usually a wrong EIP-712 domain: the token name or version from extra, the chain, or the token contract.

How do I fix "This payment has already been used. Sign a new one."?

Your client resent a payment header that was already used, often through automatic HTTP retries. Sign a fresh payment for every request; the token contract will never execute the same authorization twice.

Do I need a private key on my server to accept x402 payments?

No. The server needs a receiving address only. The payer signs, and the facilitator submits the transfer. See what x402 is for the full flow.