How to put an HTTP endpoint behind a stablecoin paywall so any agent can pay per request with no accounts, API keys, or invoices.
What is x402?
x402 revives the long-dormant HTTP 402 Payment Required status code and turns it into a real payment protocol. The concept is simple: a client requests a protected resource, the server replies that payment is required with instructions on how to pay, the client signs a payment and retries, and the server returns the requested resource.
There are no signups, API keys, or monthly subscriptions. A single HTTP request becomes an instant paid transaction.
An autonomous agent can discover a priced endpoint, pay a fraction of a cent, and retrieve its data, all without a human needing to create an account first.
This guide walks you through building the server side on Celo, where transactions are fast, cheap, and stablecoins (such as USDT and USDC) are widely used. If you want to build the paying side, check out my companion guide, Building x402 Clients on Celo.
The Mental Model
An x402 server handles three main steps on any protected route:
-
Challenge: If a request arrives without a valid payment, respond with a
402status code and aPAYMENT-REQUIREDheader. The decoded JSON header contains anaccepts[]array listing the payment options you accept, including the network, asset, price, and your receiving wallet address. -
Verify & Settle: If the request includes a signed payment in the
X-PAYMENTheader, verify it and settle it through a facilitator (a service that submits the on-chain transfer and pays the network gas fee). On success, run your real route handler and return aPAYMENT-RESPONSEheader with the settlement transaction hash. -
Run it through middleware: You do not need to build this payment flow from scratch. The
@x402/expresspackage providespaymentMiddlewarethat handles challenge, verification, and settlement automatically. You simply provide a map of routes toaccepts[]options and a resource server that routes network requests to the correct facilitator.
Your server never holds the payer's private key and never pays gas fees. The facilitator handles all on-chain work, while your wallet simply acts as the
payTorecipient address.
Prerequisites
Before starting, make sure you have:
- Node.js 20 or higher running an Express app.
- A receiving wallet address on Celo (this is your
payToaddress that collects payments; it does not sign transactions). - Access to a Celo facilitator (an HTTP facilitator URL and API key). Celo's ecosystem provides these, or you can self-host
x402-rs.
Install the required packages:
npm install @x402/express @x402/evm @x402/core @coinbase/cdp-sdk
-
@x402/express: HandlespaymentMiddlewareandx402ResourceServer. -
@x402/evm: ProvidesExactEvmSchemefor exact-amount payments on EVM-compatible chains. -
@x402/core: ProvidesHTTPFacilitatorClientfor self-hosted or third-party facilitators. -
@coinbase/cdp-sdk: (Optional) Required only if you also want to accept Base USDC via Coinbase's hosted facilitator.
If you are using TypeScript and these packages lack built-in type definitions, add a small declaration file to keep your compiler happy:
// x402-shims.d.ts
declare module "@x402/express" { export const paymentMiddleware: any; export const x402ResourceServer: any; }
declare module "@x402/evm/exact/server" { export const ExactEvmScheme: any; }
declare module "@x402/core/server" { export const HTTPFacilitatorClient: any; }
declare module "@coinbase/cdp-sdk/x402" { export const createCdpFacilitatorClient: any; }
Step 1: Connect Your Facilitators
Each blockchain network requires a facilitator to verify and settle payments. Celo uses an HTTP facilitator configured via URL. If you also choose to accept USDC on Base, you can use Coinbase's hosted CDP facilitator.
import { HTTPFacilitatorClient } from "@x402/core/server";
import { createCdpFacilitatorClient } from "@coinbase/cdp-sdk/x402";
// Celo (eip155:42220): third-party or self-hosted HTTP facilitator using an API key
const celoFacilitatorClient = new HTTPFacilitatorClient({
url: process.env.X402_CELO_FACILITATOR_URL,
async createAuthHeaders() {
const headers = { "X-API-Key": process.env.CELO_FACILITATOR_API_KEY };
return { verify: headers, settle: headers, supported: headers };
},
});
// Base (eip155:8453): optional Coinbase-hosted facilitator (reads CDP creds from env)
const baseFacilitatorClient = createCdpFacilitatorClient();
Keep all sensitive credentials (API keys, CDP credentials, and wallet addresses) inside environment variables rather than your codebase. Additionally, wrap your payment setup in a configuration check so a misconfigured server returns a helpful error instead of crashing:
Go to x402.celo.org to get a Celo x402 facilitator URL and API key.
const payTo = process.env.X402_PAY_TO_ADDRESS;
if (!payTo || !process.env.X402_CELO_FACILITATOR_URL || !process.env.CELO_FACILITATOR_API_KEY) {
res.status(503).json({ error: "x402 payment endpoint is not configured on this server" });
return;
}
Step 2: Declare What You Accept
The core of your server setup is a mapping from route paths to an accepts[] array, paired with a resource server that routes each network to its corresponding facilitator.
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
// Dynamic price: compute atomic units for this request.
// All these stablecoins use 6 decimals, so $0.10 = 100,000 atomic units.
const units = getRequestedUnits(req); // your per-request quantity
const usd = units * PRICE_PER_UNIT_USD; // e.g., 0.10 * count
const atomic = String(Math.round(usd * 1e6)); // "100000"
return paymentMiddleware(
{
"GET /": {
accepts: [
// Base USDC: human-readable price strings work fine for the CDP facilitator
{ scheme: "exact", price: formatUsd(usd), network: "eip155:8453", payTo },
// Celo USDC: MUST use the flat wire shape (see Pitfall #1)
{
scheme: "exact",
price: { amount: atomic, asset: "0xcEBA9300f2b948710d2653dD7B07f33A8B32118C", extra: { name: "USDC", version: "2" } },
network: "eip155:42220",
payTo,
},
// Celo USDT: flat wire shape with the correct EIP-712 domain
{
scheme: "exact",
price: { amount: atomic, asset: "0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e", extra: { name: "Tether USD", version: "1" } },
network: "eip155:42220",
payTo,
},
],
description: "Your resource",
mimeType: "application/json",
},
},
new x402ResourceServer([baseFacilitatorClient, celoFacilitatorClient])
.register("eip155:8453", new ExactEvmScheme())
.register("eip155:42220", new ExactEvmScheme()),
)(req, res, next);
Offering multiple options in accepts[] allows callers to pay using whichever stablecoin they hold. The client chooses one option, and your resource server delegates settlement to the registered facilitator for that network.
Celo Token Constants
Copy these exact contract addresses and EIP-712 domain parameters:
-
Celo USDT:
0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e|extra: { name: "Tether USD", version: "1" } -
Celo USDC:
0xcEBA9300f2b948710d2653dD7B07f33A8B32118C|extra: { name: "USDC", version: "2" }
Important: The
extra.nameandextra.versionfields must match the token's on-chain EIP-712DOMAIN_SEPARATORexactly. If they differ, payment signatures will fail to recover, causing settlement errors. Note that Celo USDT is a proxy contract whose domain version is"1", not"2".
Two Pitfalls That Cost Me Hours
When I first implemented x402 on Celo, two subtle issues caused unexpected errors. Reviewing them now will save you debugging time.
Pitfall #1: The Celo facilitator requires a flat wire shape
The x402-rs facilitator expects the asset field to be a plain string address, with EIP-712 domain details placed in extra. Passing an object-style asset causes the facilitator to reject the transaction with an invalid_format error, even if the 402 challenge looks correct.
// ❌ REJECTED by the Celo facilitator (invalid_format)
price: { amount, asset: { address: "0x4806…", decimals: 6, eip712: { name: "Tether USD", version: "1" } } }
// ✅ ACCEPTED: flat string asset + domain in extra
price: { amount, asset: "0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e", extra: { name: "Tether USD", version: "1" } }
Declare the AssetAmount explicitly with a string amount in atomic units. Avoid passing raw price strings like "$0.10" directly for Celo; calculate the atomic integer value beforehand to ensure clarity.
Pitfall #2: Calculating price per request in atomic units
If your API charges by usage quantity (such as item counts or data size), calculate the price inside the request middleware rather than globally at startup:
function unitPriceUsd(): number {
const v = Number.parseFloat(process.env.X402_PRICE_USD_PER_UNIT ?? "");
return Number.isFinite(v) && v > 0 ? v : 0.1; // safe default
}
- Standard stablecoins like USDC and USDT on Celo use 6 decimals, meaning $1.00 equals 1,000,000 atomic units (
usd * 1e6). - Always configure prices via environment variables with a safe fallback value to prevent accidental zero-pricing.
- Pass the calculated atomic
amountinto every entry in youraccepts[]array.
Mount this combined logic on your Express routes after standard parameter validation:
app.use("/resource", validateParams, x402Middleware, resourceRouter);
Make Your Paywall Discoverable to AI Agents
Autonomous agents read API documentation to determine how to pay. You can make your server agent-friendly by publishing a GET /llms.txt file that outlines:
- The authentication flow (
402response -> decodePAYMENT-REQUIREDheader -> sign -> retry withX-PAYMENT). - The header requirement structure (noting that the response body remains empty
{}). - Supported networks, stablecoin assets, and atomic unit calculations.
- Your expected request parameters and response schemas.
Providing machine-readable documentation enables AI agents to pay and access your API automatically.
Test and Debug Your Endpoint
Once your server is running, test it using the x402 Inspector. This tool decodes your 402 challenge, displays the accepts[] array, and allows you to execute end-to-end test payments from a browser wallet:
[https://x402-inspector.vercel.app/?url=](https://x402-inspector.vercel.app/?url=)<your-endpoint-url>
For a working reference implementation, inspect and query the live Chess Puzzles API on Celo:
[https://x402-inspector.vercel.app/?url=https://api.chesspuzzles.xyz/puzzles?count=1](https://x402-inspector.vercel.app/?url=https://api.chesspuzzles.xyz/puzzles?count=1)
Skip the Trial and Error: Grab the Skills
To simplify integration, I packaged my Celo x402 learnings into reusable agent skills. If you use an AI coding assistant (such as Claude Code), you can install them directly:
GitHub Repository: https://github.com/Emmo00/agent-skills
npx skills add emmo00/agent-skills
This library provides pre-configured skills for building x402 servers and clients on Celo, ensuring your development environment automatically handles flat wire shapes, EIP-712 domains, and facilitator client setup.
Where to Go Next
- Build the paying client: Read my companion guide, Building x402 Clients on Celo.
- Inspect endpoints: Test your setup using the x402 Inspector.
- Reference code: Test against the live Chess Puzzles API on Celo.
x402 streamlines API monetization by replacing complex signup forms and key management with instant signed HTTP payments. Combined with Celo's fast block times and low transaction costs, it offers a seamless way to monetize services for human users and AI agents alike.

Top comments (0)