x402 from zero

Lesson 1 is saved.

The status code that waited

Charged$0.00

Make a free account to keep going. Next is Your first paid endpoint, about 15 minutes.

Tick any you’d like. All start off.

No password and no card. We use your email to sign you in and for the choices you tick. Your lesson 1 progress moves to your account.

Not now

Your first paid endpoint

15 minutes. Checked 14 October.

In this lesson

  • A few lines of code make a server answer 402 with a price and hand over the data once paid.
  • On a test network the money has no value, so you can try a whole sale for free.
  • Never put real money in a test wallet, and never share a private key.
  • For real money, switch to Base, a production facilitator and a wallet you have backed up.
Go through it step by step

Your first paid endpoint

This is the hands-on lesson. You will run a small server that answers 402 Payment Required, then pay it from a test wallet, all on a test network where the money has no value. Every command is here to copy.

You are going to run a small web server with one free page and one page that costs $0.001 a request. Then you will pay for it from a second, test wallet. Everything runs on Base Sepolia, a test copy of the Base network where the money has no value, using the free test facilitator at x402.org.

Your first paid endpoint

What we ran, and what you run. On 2026-10-14 we ran steps 1 to 5 exactly as written, with Node.js 24.12.0, and got the 402 response shown below. We also ran the client in step 7 with an empty wallet: it signed a payment, and the test facilitator refused it because the wallet had no funds. We did not run the paid step, because the faucet in step 6 needs a person to pass a captcha. Steps 6 and 7, with real test funds, are yours.

You need Node.js 20.6 or newer (check with node --version) and a terminal. Allow about 15 minutes.

1. Make a project

mkdir paid-endpoint
cd paid-endpoint
npm init -y
npm pkg set type=module
npm install --save-exact @x402/express@2.28.0 @x402/core@2.28.0 @x402/evm@2.28.0 @x402/fetch@2.28.0 express@4.22.3 viem@2.57.2

The versions are pinned so that your result matches ours. Version 2.28.0 of the x402 packages was the latest on 2026-10-14.

2. Make two test wallets

Save this as make-test-wallet.mjs:

import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";

const key = generatePrivateKey();
console.log("Address:     " + privateKeyToAccount(key).address);
console.log("Private key: " + key);
console.log("Testnet only. Never send real money to this wallet.");

2. Make two test wallets

Run it twice:

node make-test-wallet.mjs
node make-test-wallet.mjs

The first wallet is the seller: you need only its address. The second is the buyer: keep its address and its private key.

2. Make two test wallets

Now create a file called .env (no other extension) with your own values:

PAY_TO=0xYourSellerAddress
FACILITATOR_URL=https://x402.org/facilitator
BUYER_PRIVATE_KEY=0xYourBuyerPrivateKey

Careful. A private key is full control of a wallet. These two are for the test network only. Never put real money in them, never reuse them, and never commit .env to GitHub or paste it anywhere public.

Your server’s 402 says amount “1000” for a price of $0.001. Why?

Your choice is checked as soon as you pick it. With a keyboard, move with the arrow keys and press Space to choose.

Right.

Not quite.

Amounts travel in the token’s smallest unit. One USDC is 1,000,000 units, so $0.001 is 1,000.

3. Write the server

Save this as server.mjs. It follows the official Express example from the x402 repository, with one paid route.

import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";

const payTo = process.env.PAY_TO;
const facilitatorUrl = process.env.FACILITATOR_URL;
if (!payTo || !facilitatorUrl) {
  console.error("Set PAY_TO and FACILITATOR_URL in your .env file");
  process.exit(1);
}

const facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl });
const resourceServer = new x402ResourceServer(facilitator)
  .register("eip155:84532", new ExactEvmScheme());

const app = express();

app.use(
  paymentMiddleware(
    {
      "GET /rainfall": {
        accepts: {
          scheme: "exact",
          price: "$0.001",
          network: "eip155:84532",
          payTo,
        },
        description: "Latest rainfall reading from one test gauge",
        mimeType: "application/json",
      },
    },
    resourceServer,
  ),
);

app.get("/rainfall", (req, res) => {
  res.json({ gauge: "test-gauge-1", rainfallMm: 2.4, at: new Date().toISOString() });
});

app.get("/", (req, res) => {
  res.send("Free page. Try /rainfall");
});

app.listen(4021, () => {
  console.log("Listening on http://localhost:4021");
});

3. Write the server

eip155:84532 is the ID for Base Sepolia. The server holds only your seller address, never a private key.

4. Start it

node --env-file=.env server.mjs

You should see Listening on http://localhost:4021. Leave this terminal open.

5. Ask for the paid page without paying

Open a second terminal in the same folder. On Windows, type curl.exe instead of curl.

curl -i http://localhost:4021/
curl -i http://localhost:4021/rainfall

The first answers 200 with the free page. The second is what an agent sees before it pays. This is what we got:

Captured from the server above on 14 October 2026. The PAYMENT-REQUIRED header is 568 characters and is shortened here.

Show the real messageHide the real message
GET /rainfall HTTP/1.1
Host: localhost:4021

The site answers:

HTTP/1.1 402 Payment Required
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQYXltZW50IHJlcXVpcmVkIiwicmVzb3VyY2UiOnsidXJsIjoiaHR0cDovL2xvY2FsaG9zdDo0MDIxL3JhaW5mYWxsIiwi…
Cache-Control: no-store

{}

5. Ask for the paid page without paying

The header is base64. To read yours, paste its value in place of PASTE_HEADER_HERE:

node -e "console.log(JSON.stringify(JSON.parse(Buffer.from(process.argv[1], 'base64').toString()), null, 2))" PASTE_HEADER_HERE

5. Ask for the paid page without paying

Ours decoded to this:

{
  "x402Version": 2,
  "error": "Payment required",
  "resource": {
    "url": "http://localhost:4021/rainfall",
    "description": "Latest rainfall reading from one test gauge",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:84532",
      "amount": "1000",
      "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "payTo": "0xC13d8A5Ecb934AC37E31C22A10752f0f7A2E3d76",
      "maxTimeoutSeconds": 300,
      "extra": { "name": "USDC", "version": "2" }
    }
  ]
}

5. Ask for the paid page without paying

We checked each field against the x402 version 2 spec. amount is 1,000 because USDC has six decimal places, so 1,000 units is $0.001. asset is Circle’s USDC contract on Base Sepolia. payTo will show your seller address. maxTimeoutSeconds gives the buyer five minutes to pay.

Does the buyer’s test wallet need test ETH to pay gas?

Your choice is checked as soon as you pick it. With a keyboard, move with the arrow keys and press Space to choose.

Right.

Not quite.

The exact scheme on EVM uses USDC’s transferWithAuthorization. The buyer only signs. The facilitator submits the transaction and pays the gas.

6. Get test USDC

Go to faucet.circle.com, choose Base Sepolia, paste your buyer address and pass the captcha. You get 20 test USDC, and you can ask again every two hours (14 Oct) source. No account is needed.

The buyer does not need test ETH for gas. With USDC, the buyer only signs an authorisation, and the facilitator submits the transaction and pays the gas.

7. Pay for it

Save this as client.mjs:

import { x402Client, x402HTTPClient, wrapFetchWithPayment } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";

const buyerKey = process.env.BUYER_PRIVATE_KEY;
if (!buyerKey) {
  console.error("Set BUYER_PRIVATE_KEY in your .env file");
  process.exit(1);
}

const client = new x402Client();
client.register("eip155:*", new ExactEvmScheme(privateKeyToAccount(buyerKey)));
client.setSpendControls({ maxAmountPerPayment: "$0.01" });

const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const response = await fetchWithPayment("http://localhost:4021/rainfall");
const result = await new x402HTTPClient(client).processResponse(response);
console.dir(result, { depth: null });

7. Pay for it

setSpendControls is the buyer’s safety limit: this client refuses any single payment over one cent. We tested it against a copy of the server priced at $0.02, and the client stopped with “All payment requirements were rejected by spendControls.maxAmountPerPayment” without signing anything. Without the line, the library’s default limit is $1 (14 Oct) source.

7. Pay for it

With the server still running, run:

node --env-file=.env client.mjs

The client asks for /rainfall, gets the 402, signs a payment for $0.001 and sends the request again with a PAYMENT-SIGNATURE header. The server passes the payment to the facilitator, which checks it and settles it on Base Sepolia. When it works, you should see status 200, the rainfall reading, and a decoded PAYMENT-RESPONSE containing success: true, a transaction hash, the network and the payer’s address. Paste the hash into sepolia.basescan.org to see the transfer.

7. Pay for it

If the buyer wallet has no test USDC, you get what we got: status 402 again, with the error invalid_exact_evm_insufficient_balance. Go back to step 6.

What you built, and what changes for real money

You built both sides of an x402 sale: a seller that answers with a price, and a buyer that pays it within a limit. Before you take real money, three things change, all covered in lesson 3:

  • the network becomes eip155:8453 (Base);
  • the facilitator becomes a production one, such as Coinbase’s, because the x402.org facilitator is for testing only;
  • PAY_TO becomes a wallet you control and have backed up.
What must you change before taking real money?

Your choice is checked as soon as you pick it. With a keyboard, move with the arrow keys and press Space to choose.

Right.

Not quite.

The x402.org facilitator is for testing only, and eip155:84532 is the test network. Lesson 3 covers the production route.

Lesson done.

Price: about 15 minutes of your attention.

You can now say

  • A few lines of code make a server answer 402 with a price and hand over the data once paid.
  • On a test network the money has no value, so you can try a whole sale for free.
  • Never put real money in a test wallet, and never share a private key.
  • For real money, switch to Base, a production facilitator and a wallet you have backed up.

Sources

Checked 14 October

  1. x402 repository: Express server example (identical at the 2.28.0 release) github.com
  2. x402 repository: fetch client example github.com
  3. x402 spec: HTTP transport, version 2 (PAYMENT-REQUIRED, PAYMENT-SIGNATURE, PAYMENT-RESPONSE) github.com
  4. x402 spec: exact scheme on EVM (the facilitator pays gas) github.com
  5. x402 docs: quickstart for sellers (test facilitator) docs.x402.org
  6. npm: @x402/express 2.28.0 (published 2026-09-29) npmjs.com
  7. Circle: testnet faucet faucet.circle.com
  8. Circle: USDC contract addresses (Base Sepolia) developers.circle.com
  9. Base docs: networks and block explorers docs.base.org