Most ways to charge for an API assume a human: a signup form, a dashboard, a card on file, a monthly invoice. An agent has none of those and wants none of them. x402 is the protocol that drops the assumption. It takes 402 Payment Required — the one HTTP status code the web reserved for payments and then never used — and makes it a real response your server can send and your agent can pay, in about the time it takes to read this.
Here's the whole flow before any code: a client requests a protected route with no payment. The server answers 402 with a small JSON body naming the price, the asset, and where to send it. The client's wallet signs a stablecoin authorization for exactly that amount and retries the same request with an X-PAYMENT header. A facilitator verifies the signature and settles the funds on-chain. Your handler runs. That's it — one extra round trip, no session, no account.
Two npm packages cover both sides. Let's build a paid endpoint and an agent that pays it.
Prerequisites#
- Node 18+ and a fresh folder.
- A wallet address to receive USDC (the server) and a funded wallet private key to spend from (the client). Use testnet for both to start.
- We'll run everything on base-sepolia, Base's testnet. Grab testnet USDC from a faucet; nothing here costs real money until you flip one string.
1. The server: one middleware#
Install the server package:
npm install express x402-express
paymentMiddleware takes three things: where the money lands, a map of which routes cost what, and a facilitator to settle through. It returns the 402 for you whenever a request arrives without valid payment.
import express from "express";
import { paymentMiddleware } from "x402-express";
const app = express();
app.use(paymentMiddleware(
"0xYourReceivingAddress", // where USDC lands
{
"GET /premium": {
price: "$0.10", // human price → USDC under the hood
network: "base-sepolia", // testnet first
},
},
{ url: "https://x402.org/facilitator" } // verifies + settles for you
));
// This only runs after payment clears:
app.get("/premium", (req, res) => {
res.json({ report: "the paid data your agent came for" });
});
app.listen(3000);
That's the entire paywall. The middleware watches for an X-PAYMENT header; if it's missing or invalid, the client never reaches your handler — they get a 402 instead.
2. What the 402 actually says#
The 402 isn't an error page, it's an instruction. Its body is machine-readable so the client's wrapper can act on it without a human:
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"x402Version": 1,
"accepts": [{
"scheme": "exact",
"network": "base-sepolia",
"maxAmountRequired": "100000",
"resource": "https://api.example.com/premium",
"payTo": "0xYourReceivingAddress",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"maxTimeoutSeconds": 60
}]
}
Two things worth reading twice. maxAmountRequired is "100000", not 0.10 — USDC has 6 decimals, so amounts travel as atomic-unit strings; $0.10 is 100000. And asset is the USDC contract on base-sepolia. On mainnet, both the network and the asset address change — which is exactly why you never hardcode a mainnet address while testing.
3. The client: one wrapper#
Install the client packages:
npm install x402-fetch viem
wrapFetchWithPayment takes the built-in fetch and an account, and hands you back a fetch that knows how to pay. When it sees a 402, it reads the accepts block, signs an EIP-3009 authorization for the amount, and retries with the X-PAYMENT header — all invisible to your agent code.
import { wrapFetchWithPayment } from "x402-fetch";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY);
const fetchWithPay = wrapFetchWithPayment(fetch, account);
// The agent just fetches. The 402 is caught, signed, and retried for it.
const res = await fetchWithPay("https://api.example.com/premium");
const data = await res.json();
console.log(data); // → { report: "the paid data your agent came for" }
From the agent's point of view, it called a URL and got JSON. Underneath, it paid ten cents. The response carries an X-PAYMENT-RESPONSE header with the settlement receipt if you want to log it.
4. Settle — testnet, then mainnet#
Run the server, run the client, and watch a real (testnet) USDC transfer clear on base-sepolia. The facilitator — Coinbase hosts the primary one, settling fee-free on Base — did the signature check and the on-chain settlement; neither your server nor your agent touched a chain directly.
To go live, change one string: network: "base-sepolia" → network: "base", and swap the testnet USDC address for mainnet USDC. That's the only code difference between a demo and money.
The three gotchas that bite#
- USDC only, by design. x402 rides EIP-3009's
transferWithAuthorization, which USDC implements and most tokens don't. That's a feature — it's what makes off-chain signing plus facilitator settlement possible — but it means "accept x402" means "accept USDC." - The balance is your real spend cap. Each request signs one narrowly scoped authorization, so a single call can't overspend the quote. But the agent's wallet balance is the ceiling you actually control — fund it small, keep the key in a secret store, rotate it. Don't hand an autonomous agent a wallet with your treasury in it.
- Test on base-sepolia first, always. The mainnet swap is one string precisely because everything else must be identical and proven before real value moves. Prove it on testnet.
That's a working, agent-payable endpoint. If you're deciding where x402 fits against the other agent-payment standards — Google's AP2 for authorization, OpenAI and Stripe's ACP for merchant checkout — start with the map: AP2 vs x402 vs ACP: the agent payment stack isn't a bake-off.



