Build an x402 Payment Server
Create an Express server that charges USDC for API access using the x402 payment protocol. Callers pay per-request in USDC on Base — no accounts, API keys, or subscriptions needed.
How It Works
x402 is an HTTP-native payment protocol. When a client hits a protected endpoint without paying, the server returns HTTP 402 with payment requirements. The client signs a USDC payment and retries with a payment header. The facilitator verifies and settles the payment, and the server returns the response.
Confirm wallet is initialized and authed
If the wallet is not authenticated, refer to the
skill.
Step 1: Get the Payment Address
Run this to get the wallet address that will receive payments:
Use this address as the
value.
Step 2: Set Up the Project
bash
mkdir x402-server && cd x402-server
npm init -y
npm install express x402-express
js
const express = require("express");
const { paymentMiddleware } = require("x402-express");
const app = express();
app.use(express.json());
const PAY_TO = "<address from step 1>";
// x402 payment middleware — protects routes below
const payment = paymentMiddleware(PAY_TO, {
"GET /api/example": {
price: "$0.01",
network: "base",
config: {
description: "Description of what this endpoint returns",
},
},
});
// Protected endpoint
app.get("/api/example", payment, (req, res) => {
res.json({ data: "This costs $0.01 per request" });
});
app.listen(3000, () => console.log("Server running on port 3000"));
Step 3: Run It
Test with curl — you should get a 402 response with payment requirements:
bash
curl -i http://localhost:3000/api/example
API Reference
paymentMiddleware(payTo, routes, facilitator?)
Creates Express middleware that enforces x402 payments.
| Parameter | Type | Description |
|---|
| string | Ethereum address (0x...) to receive USDC payments |
| object | Route config mapping route patterns to payment config |
| object? | Optional custom facilitator (defaults to x402.org) |
Route Config
Each key in the routes object is
. The value is either a price string or a config object:
js
// Simple — just a price
{ "GET /api/data": "$0.05" }
// Full config
{
"POST /api/query": {
price: "$0.25",
network: "base",
config: {
description: "Human-readable description of the endpoint",
inputSchema: {
bodyType: "json",
bodyFields: {
query: { type: "string", description: "The query to run" },
},
},
outputSchema: {
type: "object",
properties: {
result: { type: "string" },
},
},
},
},
}
Route Config Fields
| Field | Type | Description |
|---|
| string | USDC price (e.g. "$0.01", "$1.00") |
| string | Blockchain network: "base" or "base-sepolia" |
| string? | What this endpoint does (shown to clients) |
| object? | Expected request body/query schema |
| object? | Response body schema |
| number? | Max time for payment settlement |
Supported Networks
| Network | Description |
|---|
| Base mainnet (real USDC) |
| Base Sepolia testnet (test USDC) |
Patterns
Multiple endpoints with different prices
js
const payment = paymentMiddleware(PAY_TO, {
"GET /api/cheap": { price: "$0.001", network: "base" },
"GET /api/expensive": { price: "$1.00", network: "base" },
"POST /api/query": { price: "$0.25", network: "base" },
});
app.get("/api/cheap", payment, (req, res) => { /* ... */ });
app.get("/api/expensive", payment, (req, res) => { /* ... */ });
app.post("/api/query", payment, (req, res) => { /* ... */ });
Wildcard routes
js
const payment = paymentMiddleware(PAY_TO, {
"GET /api/*": { price: "$0.05", network: "base" },
});
app.use(payment);
app.get("/api/users", (req, res) => { /* ... */ });
app.get("/api/posts", (req, res) => { /* ... */ });
Health check (no payment)
Register free endpoints before the payment middleware:
js
app.get("/health", (req, res) => res.json({ status: "ok" }));
// Payment middleware only applies to routes registered after it
app.get("/api/data", payment, (req, res) => { /* ... */ });
POST with body schema
js
const payment = paymentMiddleware(PAY_TO, {
"POST /api/analyze": {
price: "$0.10",
network: "base",
config: {
description: "Analyze text sentiment",
inputSchema: {
bodyType: "json",
bodyFields: {
text: { type: "string", description: "Text to analyze" },
},
},
outputSchema: {
type: "object",
properties: {
sentiment: { type: "string" },
score: { type: "number" },
},
},
},
},
});
app.post("/api/analyze", payment, (req, res) => {
const { text } = req.body;
// ... your logic
res.json({ sentiment: "positive", score: 0.95 });
});
Using the CDP facilitator (authenticated)
For production use with the Coinbase facilitator (supports mainnet):
bash
npm install @coinbase/x402
js
const { facilitator } = require("@coinbase/x402");
const payment = paymentMiddleware(PAY_TO, routes, facilitator);
This requires
and
environment variables. Get these from
https://portal.cdp.coinbase.com.
Testing with the pay-for-service Skill
Once the server is running, use the
skill to test payments:
bash
# Check the endpoint's payment requirements
npx awal@latest x402 details http://localhost:3000/api/example
# Make a paid request
npx awal@latest x402 pay http://localhost:3000/api/example
Pricing Guidelines
| Use Case | Suggested Price |
|---|
| Simple data lookup | $0.001 - $0.01 |
| API proxy / enrichment | $0.01 - $0.10 |
| Compute-heavy query | $0.10 - $0.50 |
| AI inference | $0.05 - $1.00 |
Checklist