You’re offline. Your progress is saved on this phone and will sync when you’re back.
Lesson 1 is saved.
The status code that waited
Make a free account to keep going. Next is Your first paid endpoint, about 15 minutes.
Google sign-in doesn’t work inside TikTok. Use an email link, or open this page in your browser.
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.
Check your email.
We sent a sign-in link to . It works for 15 minutes.
You can send it again in 30 seconds
Saved. Your progress is on your account 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.
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.2The 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.mjsThe 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=0xYourBuyerPrivateKeyCareful. 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.
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.mjsYou 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/rainfallThe 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_HERE5. 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.
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.mjsThe 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_TObecomes a wallet you control and have backed up.
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.
x402 from zero
Your receipt on this device
Part 2: Build or sell
0 of 6 doneWhat could sell, how to price it, and your first paid endpoint on a free test network.
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.
- Read the whole lesson on one page
- Was this clear?
Thanks.
Sources
Checked 14 October
- x402 repository: Express server example (identical at the 2.28.0 release) github.com
- x402 repository: fetch client example github.com
- x402 spec: HTTP transport, version 2 (PAYMENT-REQUIRED, PAYMENT-SIGNATURE, PAYMENT-RESPONSE) github.com
- x402 spec: exact scheme on EVM (the facilitator pays gas) github.com
- x402 docs: quickstart for sellers (test facilitator) docs.x402.org
- npm: @x402/express 2.28.0 (published 2026-09-29) npmjs.com
- Circle: testnet faucet faucet.circle.com
- Circle: USDC contract addresses (Base Sepolia) developers.circle.com
- Base docs: networks and block explorers docs.base.org
Sources
Checked 14 October
- x402 repository: Express server example (identical at the 2.28.0 release) github.com
- x402 repository: fetch client example github.com
- x402 spec: HTTP transport, version 2 (PAYMENT-REQUIRED, PAYMENT-SIGNATURE, PAYMENT-RESPONSE) github.com
- x402 spec: exact scheme on EVM (the facilitator pays gas) github.com
- x402 docs: quickstart for sellers (test facilitator) docs.x402.org
- npm: @x402/express 2.28.0 (published 2026-09-29) npmjs.com
- Circle: testnet faucet faucet.circle.com
- Circle: USDC contract addresses (Base Sepolia) developers.circle.com
- Base docs: networks and block explorers docs.base.org