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#

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#

  1. 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."
  2. 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.
  3. 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.