Quickstart: Pay-per-API-Call with x402
This guide walks you through creating an API endpoint that charges callers a small amount of MUSD or MJPY per request, using the x402 protocol on the Awaji testnet.
By the end you'll have:
- A running Express server that responds with
402 Payment Requiredto unpaid requests - A client script that pays automatically and retrieves the response
- A working end-to-end payment flow on Awaji you can build on Time to complete: ~30 minutes
You'll need MUSD or MJPY in your wallet to pay for requests. Use the faucet to fund a fresh testnet wallet before starting.
These examples are configured for the Awaji testnet. See x402 Contracts for all deployed x402 and stablecoin addresses.
Prerequisites
Before starting, make sure you have the following installed:
- Node.js v22+ — check with
node --version. If you need to install it, use nvm:nvm install 22 && nvm use 22 - A testnet wallet private key — use a fresh wallet with no real funds. You can create one in MetaMask or with
cast wallet newif you have Foundry installed - Testnet MUSD or MJPY — use the faucet to fund your wallet
Part 1: Seller — Create a Paid API Endpoint
1. Create a project folder
Open your terminal and create a new folder for the seller server:
mkdir x402-seller
cd x402-seller
2. Initialise the project
npm init -y
npm pkg set type=module
npm install @x402/express @x402/core @x402/evm express dotenv
3. Create a .env file
Create a file called .env in your project folder with your wallet address:
SELLER_ADDRESS=0xYourEvmAddress
Never commit your .env file. Add it to .gitignore:
echo ".env" >> .gitignore
4. Create the server
Create a file called server.js in your project folder with the following content:
import "dotenv/config";
import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
const app = express();
// Your Awaji testnet receiving address — loaded from .env
const payTo = process.env.SELLER_ADDRESS;
// Awaji facilitator — handles payment verification and settlement
const facilitatorClient = new HTTPFacilitatorClient({
url: "https://x402-production-6134.up.railway.app",
});
// Awaji testnet — chain ID 6497
const AWAJI_NETWORK = "eip155:6497";
// MUSD token on Awaji
// EIP-712 domain params (name/version) must live in `extra` — that's where
// the ExactEvmScheme reads them when building the permit signature.
const MUSD = {
address: "0xb9C49B527294E8472eD48B800E81b5FA69D0f72E",
extra: {
name: "MIZUHIKI USD",
version: "2",
},
decimals: 6,
};
// MJPY token on Awaji
const MJPY = {
address: "0x78f5f0Ac4EF201618b97638ded959b155c4f4B04",
extra: {
name: "Mizuhiki JPY",
version: "2",
},
decimals: 6,
};
app.use(
paymentMiddleware(
{
"GET /api/data": {
accepts: [
{
scheme: "exact",
network: AWAJI_NETWORK,
payTo,
price: {
amount: "1000", // 0.001 MUSD (6 decimals) - adjust as needed
asset: MUSD.address,
extra: MUSD.extra,
},
},
{
scheme: "exact",
network: AWAJI_NETWORK,
payTo,
price: {
amount: "150000", // adjust as needed
asset: MJPY.address,
extra: MJPY.extra,
},
},
],
description: "Paid data endpoint",
mimeType: "application/json",
unpaidResponseBody: function () {
return {
contentType: "application/json",
body: { message: "Payment required to access this resource" },
};
},
},
},
new x402ResourceServer(facilitatorClient).register(
AWAJI_NETWORK,
new ExactEvmScheme(),
),
),
);
// This handler only runs after payment is verified
app.get("/api/data", (req, res) => {
res.json({
message: "Payment verified. Here is your data.",
timestamp: Date.now(),
});
});
app.listen(4021, () => console.log("Server running on http://localhost:4021"));
This endpoint advertises both MUSD and MJPY in its accepts array, so callers can pay in either stablecoin. Drop either entry if you only want to accept one.
5. Start the server
node server.js
You should see:
Server running on http://localhost:4021
6. Test the 402 challenge
In a new terminal window, run:
curl http://localhost:4021/api/data
You should get a 402 Payment Required response with a PAYMENT-REQUIRED header. This confirms the server is correctly gating the endpoint. Leave this server running and move on to Part 2.
Part 2: Buyer — Pay for API Calls Automatically
Open a new terminal window for this part. Keep the seller server running in the other window.
1. Create a project folder
mkdir x402-buyer
cd x402-buyer
2. Initialise the project
npm init -y
npm install @x402/fetch @x402/core @x402/evm viem dotenv
Add "type": "module" to package.json as you did for the seller.
3. Create a .env file
Create a .env file with your buyer wallet's private key:
EVM_PRIVATE_KEY=0xYourPrivateKey
Use a fresh testnet wallet only. Never use a wallet with real funds.
4. Create the client
Create a file called client.js:
import "dotenv/config";
import { wrapFetchWithPayment } from "@x402/fetch";
import { x402Client } from "@x402/core/client";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";
// Load your testnet private key from .env
const signer = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
// Create x402 client and register the EVM payment scheme
const client = new x402Client();
client.register("eip155:*", new ExactEvmScheme(signer));
// Wrap fetch — payment is handled automatically on 402 responses
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
console.log("→ Sending request to paid endpoint...");
// Probe the endpoint first to log the payment details
const probe = await fetch("http://localhost:4021/api/data");
if (probe.status === 402) {
const raw = probe.headers.get("PAYMENT-REQUIRED");
const paymentRequired = JSON.parse(
Buffer.from(raw, "base64").toString("utf8"),
);
const option = paymentRequired.accepts?.[0];
console.log("← 402 Payment Required");
console.log(` Network: ${option?.network}`);
console.log(` Amount: ${option?.amount} (atomic units)`);
console.log(` Asset: ${option?.asset}`);
console.log(` Pay to: ${option?.payTo}`);
}
console.log("→ Signing and submitting payment on-chain...");
// This call handles the full 402 handshake automatically
const response = await fetchWithPayment("http://localhost:4021/api/data");
// Log the settlement receipt from the response header
const raw = response.headers.get("PAYMENT-RESPONSE");
const receipt = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
console.log(
"✓ Payment settled — tx hash:",
receipt?.txHash ?? receipt?.transactionHash ?? JSON.stringify(receipt),
);
const data = await response.json();
console.log("✓ Response received:", data);
5. Run the client
node client.js
You should see output like:
→ Sending request to paid endpoint...
← 402 Payment Required
Network: eip155:6497
Amount: 1000 (atomic units)
Asset: 0xb9C49B527294E8472eD48B800E81b5FA69D0f72E
Pay to: 0xYourSellerAddress
→ Signing and submitting payment on-chain...
✓ Payment settled — tx hash: 0x...
✓ Response received: { message: 'Payment verified. Here is your data.', timestamp: ... }
How It Works
Client x402 Middleware Your Handler
│ │ │
│── GET /api/data ──────────────────>│ │
│<── 402 + PAYMENT-REQUIRED header ──│ │
│ │ │
│ (client signs & submits │ │
│ payment on Awaji) ──────────────│ │
│ │ │
│── GET /api/data + PAYMENT-SIGNATURE│ │
│ ────────────────────────────────>│ │
│ verifies via facilitator │
│ │── request passes ────────>│
│<────────────────────────────────────────── 200 + data ─────────│
Your handler never runs unless payment is verified on-chain. The middleware owns the full 402 handshake.
Payment Schemes
The example above uses exact — a fixed price per request. x402 also supports:
upto— client authorises a maximum; seller charges only actual usage (e.g. by token count). EVM only.batch-settlement— buyer funds a channel once and signs off-chain vouchers per request; seller settles on-chain in batches. Good for high-frequency calls.
Next Steps
- x402 Contracts — x402 and stablecoin contract addresses, plus network details for agent development
- Predeployed Contracts — standard utility contracts on Awaji (Multicall3, Permit2, Create2 Factory, and more)