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:
- The transaction exists on sepolia.basescan.org and shows a USDC transfer.
- The recipient is your
payToaddress, character for character. - The amount matches your price in atomic units. USDC has 6 decimals, so a
$0.01route moves10000, and$1.00moves1000000.
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:
- Calls the route without paying and expects
402. - Checks the requirements a client needs: scheme, network, asset,
payTo, and the EIP-712nameandversioninextra. - Signs an EIP-3009 payment with test USDC from a Requestway wallet and retries.
- Reads the
PAYMENT-RESPONSEreceipt and links the transaction on the block explorer. - Waits up to 20 seconds for your app's own
payment.settledreport 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.
Related articles
-
Production
A checklist for moving an x402 API from testnet to mainnet
7 min read
-
Laravel
How to add x402 payments to a Laravel API, step by step
7 min read
-
Node.js
x402 with Express, Next.js and Hono: a practical guide
7 min read