Skip to content

Testing Tutorial

Testing x402 payments on Base Sepolia without spending real money

By Requestway · · 6 min read

You can test x402 payments on Base Sepolia end to end without spending a cent: the testnet USDC is free, the public facilitator is free, and the facilitator pays the gas. This guide sets up three layers of testing, from fake facilitators in your test suite to a real signed payment on chain, and lists the mistakes that usually make the first testnet payment fail.

What you need, and what it costs

Base Sepolia is the Base testnet. Its CAIP-2 id is eip155:84532, and test USDC lives at 0x036CbD53842c5426634e7929541eC2318f3dCF7e. Everything you need is free:

Piece Where it comes from
Test USDC faucet.circle.com: choose USDC and Base Sepolia
Facilitator https://x402.org/facilitator: free, no account, testnets only
Gas Paid by the facilitator, which submits the EIP-3009 transfer
Block explorer sepolia.basescan.org

The gas point surprises people. With the exact scheme on EVM chains, the payer signs an EIP-3009 transferWithAuthorization and the facilitator submits the transaction. Neither the paying client nor your server needs ETH, testnet or otherwise. The client wallet only needs test USDC.

Your server never needs a private key either. It needs a receiving address. If you want the background on why, the x402 guide explains the headers and the role of the facilitator.

Three layers of testing

Not every test needs a chain. Spread them across three layers:

Layer What it proves Speed Needs a wallet
Fake facilitator Your routes return 402, accept a valid payment, reject bad ones Milliseconds No
Real testnet payment Your requirements are signable and settle on chain Seconds Yes, with test USDC
External tester A client that is not yours can pay a deployed route Seconds No, it brings its own

Run the first layer in CI on every commit. Run the second when you change networks, assets, facilitators or prices. Run the third against staging before a release.

Layer 1: tests with a fake facilitator

In Laravel, requestway/laravel-x402 ships an in-memory facilitator. X402::fake() swaps it in, and the MakesX402Payments trait builds payment headers from a real 402 response:

use Requestway\X402\Facades\X402;
use Requestway\X402\Testing\MakesX402Payments;

uses(MakesX402Payments::class);

it('charges for the report', function () {
    X402::fake();

    $required = $this->getJson('/report')->assertStatus(402);

    $this->getJson('/report', $this->x402Payment($required))->assertOk();

    X402::assertSettledFor('/report', '0.01');
});

The failure paths matter more than the happy path, because those are what your users hit:

// Underpayment: rejected locally before the facilitator is called
$this->getJson('/report', $this->x402Payment($required, amount: '1'))->assertStatus(402);
X402::assertNotCharged();

// Facilitator says the payer has no funds
X402::fake()->rejectVerification(ErrorCode::InsufficientFunds);

// Settlement fails after verification passed
X402::fake()->rejectSettlement(ErrorCode::InvalidTransactionState);

// Settlement broadcast but not confirmed yet
X402::fake()->pendingSettlement('0xbroadcast');

Other assertions available: X402::assertSettled(), assertVerified() and assertNothingVerified().

From the command line, x402:test runs the full 402, pay, 200 exchange against a fake facilitator and prints both halves with the headers decoded. Nothing is charged:

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

It also takes --network=, --payer= and --header=, which helps when a route accepts several networks or has bypass rules keyed on a header. The full Laravel setup is in adding x402 payments to a Laravel API.

Layer 2: a real payment on Base Sepolia

A fake facilitator cannot catch a wrong asset address or wrong EIP-712 domain values: it accepts what you give it. For that you need a real signature checked by a real facilitator.

Configure the server for the testnet

In Laravel, a fresh install already targets the testnet. Base Sepolia is the default network and the x402.org facilitator is the default facilitator, so an unconfigured install cannot move real money. Set your receiving address and check the config:

X402_PAY_TO=0xYourTestWallet
X402_NETWORK=eip155:84532
X402_FACILITATOR_URL=https://x402.org/facilitator
php artisan x402:doctor

x402:doctor checks the config, wallet format, assets, whether the facilitator is reachable and lists your network in /supported, the replay cache and migrations.

In Node, register Base Sepolia on the resource server and use the same network in each route's accepts:

import { HTTPFacilitatorClient, x402ResourceServer } from '@x402/core/server'
import { ExactEvmScheme } from '@x402/evm/exact/server'

export const server = new x402ResourceServer(new HTTPFacilitatorClient({ url: 'https://x402.org/facilitator' }))
  .register('eip155:84532', new ExactEvmScheme())

// in the route config:
// accepts: [{ scheme: 'exact', price: '$0.01', network: 'eip155:84532', payTo: '0xYourTestWallet' }]

The Express, Next.js and Hono guide shows the full wiring for each framework.

Fund a client wallet and pay

Create a separate wallet for the client. Do not reuse a wallet that holds anything on mainnet: treat any key you put in an environment variable as a test key. Get test USDC from the Circle faucet, choosing USDC and Base Sepolia, then call the route with @x402/fetch:

import { wrapFetchWithPaymentFromConfig, decodePaymentResponseHeader } from '@x402/fetch'
import { ExactEvmScheme } from '@x402/evm'
import { privateKeyToAccount } from 'viem/accounts'

const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`)
const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
  schemes: [{ network: 'eip155:84532', client: new ExactEvmScheme(account) }],
})

const response = await fetchWithPayment('http://localhost:3000/premium-data')
const receipt = response.headers.get('PAYMENT-RESPONSE')
if (receipt) console.log(decodePaymentResponseHeader(receipt))

The client reads the 402, signs a payment for the amount in accepts, and retries with a PAYMENT-SIGNATURE header. Only your server talks to the facilitator; the client talks only to your server, so localhost works for this step.

Check the receipt and the chain

A successful call returns 200 with a PAYMENT-RESPONSE header. Decoded, it holds success, the transaction hash, the payer and the network. Then confirm three things:

  1. The transaction exists on sepolia.basescan.org and shows a USDC transfer.
  2. The recipient is your payTo address, character for character.
  3. The amount matches your price in atomic units. USDC has 6 decimals, so a $0.01 route moves 10000, and $1.00 moves 1000000.

In Laravel, the same payment is a row in x402_payments if you ran the migration:

use Requestway\X402\Models\X402Payment;

X402Payment::query()->settled()->latest()->take(5)->get();

Try the replay case too: send the same PAYMENT-SIGNATURE header a second time. It should be rejected. Each authorization carries a unique nonce and the token contract refuses to execute the same one twice; the Laravel package also claims each payload in its cache before verifying, and reports the replay as invalid_payload ("This payment has already been used. Sign a new one.").

Layer 3: the Requestway endpoint tester

Your own client shares your assumptions. The endpoint tester is an independent client. Give it the URL of a paid route from a Requestway project and it:

  1. Calls the route without paying and expects 402.
  2. Checks the requirements a client needs: scheme, network, asset, payTo, and the EIP-712 name and version in extra.
  3. Signs an EIP-3009 payment with test USDC from a Requestway wallet and retries.
  4. Reads the PAYMENT-RESPONSE receipt and links the transaction on the block explorer.
  5. Waits up to 20 seconds for your app's own payment.settled report to arrive.

It only pays on testnets, and each test is capped at 1 test USDC. On a route that only accepts mainnet it validates the 402 and stops, so it never spends real money. It calls public https URLs only and never follows redirects, so point it at a staging deployment that accepts Base Sepolia rather than at localhost. Requests identify themselves as RequestwayEndpointTester/1.0, which helps if you need to find them in your logs.

Step 5 only passes if your app reports payments. In Laravel that means X402_PLATFORM_KEY plus a running queue worker; in Node, @requestway/reporter with the transaction included in rw.settled(). Testnet payments show up under Testnet in the dashboard, apart from real earnings.

When the first testnet payment fails

Most first-payment failures come down to a handful of protocol error codes:

Code Usual cause on testnet
insufficient_funds The client wallet has no test USDC on Base Sepolia, or it was funded on a different network
invalid_network The client signed for one network and the route accepts another
invalid_exact_evm_payload_signature Wrong token domain values, or a payload modified after signing
invalid_exact_evm_payload_recipient_mismatch The signed recipient does not match your payTo
invalid_exact_evm_payload_authorization_valid_before The authorization expired, often after a slow retry or clock drift
invalid_payload Also used for a replayed payment
facilitator_unavailable The facilitator could not be reached (Laravel answers 502)

A rejected payment returns 402, and a payload that could not be decoded returns 400. In Laravel, X402Payment::query()->failed()->latest()->take(20)->get() shows recent failures with their failure_reason. Why x402 payments fail covers each code and its fix.

FAQ

Do I need Base Sepolia ETH to test x402 payments?

No. With the exact scheme the facilitator submits the EIP-3009 transfer and pays the gas. The paying wallet only needs test USDC, and your server needs only a receiving address.

Where do I get test USDC for Base Sepolia?

From the Circle faucet at faucet.circle.com. Choose USDC and Base Sepolia, and send it to the wallet your client signs with.

Can I use the x402.org facilitator for mainnet?

No. It is free and needs no account, but it is for testnets only and will not settle mainnet payments. Use a production facilitator when you switch to eip155:8453.

How do I test x402 payments in CI without a blockchain?

Use a fake facilitator. In Laravel, X402::fake() with the MakesX402Payments trait runs the whole exchange in memory, and php artisan x402:test does the same from the command line with nothing charged.

Why does the endpoint tester stop after the 402 on my route?

Your route only accepts mainnet. The tester never spends real money, so it validates the 402 and stops. Accept Base Sepolia on a staging deployment to run the full paid test.