Core concepts
Omni402 has two sides and one settlement layer. Everything else is detail.
- The gateway (
x402ify) sits in front of an API you already have access to. Unpaid requests get an HTTP402naming the price. Paid requests are proxied upstream with your key injected server-side. - The buyer is a wallet. It answers the
402by signing an EIP-3009 authorization and retrying. No account, no API key. - The facilitator is Celo's hosted x402/MPP service. It verifies the signature, submits the transfer on-chain, and pays the gas. It never custodies funds: USDC moves buyer to seller inside the token contract.
Your upstream API key never leaves your machine, and no private key is needed on the server. The buyer signs; the facilitator settles.
The request lifecycle
1. agent → GET /pay/celo-token-prices (no payment)
2. server → 402 Payment Required + price in USDC
3. agent → signs EIP-3009 authorization (off-chain, no gas)
4. agent → retries with the credential attached
5. server → facilitator settles on Celo
6. server → 200 OK + data + Payment-Receipt (tx hash)
Install and configure
Node 20 or newer, and pnpm. Clone the repo and install:
git clone https://github.com/eddiemessiah/omni402
cd omni402
pnpm install
cp .env.example .env
To settle real payments you need two secrets in .env: a facilitator API key and an MPP server secret. Create the API key once at x402.celo.org (connect a wallet, click Create API key). Generate the server secret locally:
openssl rand -base64 32
The gateway still runs and serves a correct 402 challenge, but it cannot settle. That is challenge-only mode, which is useful for local development and tests.
Develop on Celo Sepolia first by setting X402_NETWORK=celo-sepolia. Testnet USDC comes from faucet.circle.com. Switch to celo only when you want real money to move.
Sell an API (for humans)
The fastest path. Point the wrapper at any API, set a price, and give it the wallet that should receive the money:
pnpm wrap https://api.coingecko.com/api/v3 \
--price 0.001 \
--wallet 0xYourReceivingWallet \
--sample "/simple/price?ids=celo&vs_currencies=usd"
If the upstream needs a key, pass it and the gateway injects it only after a payment settles. It never reaches the buyer:
# as a query parameter
pnpm wrap https://api.etherscan.io/v2/api --price 0.01 \
--wallet 0xYourWallet --query "apikey=$ETHERSCAN_KEY"
# or as a header
pnpm wrap https://api.example.com --price 0.01 \
--wallet 0xYourWallet --header "Api-Key: $SECRET"
Selling several APIs at once
Add an entry to lanes.json and the hub serves it at /pay/<slug> on one public port, alongside the dashboard. A lane whose key is missing is skipped with a warning rather than started, so a buyer is never charged for a call that cannot succeed.
{
"name": "Celo Token Prices",
"upstream": "https://api.coingecko.com/api/v3",
"price": "0.001",
"asset": "USDC",
"sample": "/simple/price?ids=celo&vs_currencies=usd",
"description": "Live CELO / cUSD / cEUR prices."
}
Then start everything with one command:
pnpm build # build the dashboard once
pnpm hub # hub + all lanes + dashboard on :4021
The startup banner tells you which mode you are in. Look for settlement MPP live before expecting real payments.
Buy as an agent
The buyer needs a wallet holding a little USDC and no native gas, because the facilitator sponsors settlement. From the command line:
BUYER_PRIVATE_KEY=0x… pnpm --filter @omni402/x402ify exec tsx bin/buy.ts \
"http://localhost:4021/pay/celo-token-prices/simple/price?ids=celo&vs_currencies=usd" \
--network celo --max 0.01
It prints the response, the settlement transaction hash, and a Celoscan receipt link. Programmatically, createBuyer patches fetch so any gated URL is paid transparently:
import { createBuyer } from "@omni402/x402ify";
const buyer = createBuyer({
privateKey: process.env.BUYER_PRIVATE_KEY,
network: "celo",
maxAmount: "1" // never pay more than 1 USDC per call
});
const { status, body, reference } = await buyer.pay("https://your-api/premium");
console.log(status, reference); // 200, 0x… settlement tx
The autonomous agent
The repo ships a buyer that runs on a loop, picking endpoints from the catalog and paying for them. Every tick is a real x402 settlement on Celo.
BUYER_PRIVATE_KEY=0x… X402_NETWORK=celo \
PUBLIC_BASE=https://your-app.up.railway.app \
pnpm agent
Keep the buyer and the seller separate. If both sides are the same address the payment is a self-transfer and proves nothing. Fund a throwaway buyer with a few dollars of USDC.
The MCP server
This is what makes any AI assistant a paying customer. The server speaks the Model Context Protocol over stdio and exposes two tools:
| Tool | What it does | Costs money |
|---|---|---|
list_paid_apis | Returns the catalog: name, price per call, network, and the exact URL to pay. | No, discovery is free |
paid_fetch | Pays the per-call price for an endpoint and returns the data plus the settlement transaction. | Yes, in USDC |
Wiring it into a client
The repo includes a .mcp.json that Claude Desktop, Claude Code, and other MCP clients can pick up. Point PUBLIC_BASE at wherever your hub is reachable:
{
"mcpServers": {
"omni402": {
"command": "node",
"args": ["--import", "tsx", "apps/mcp/src/server.ts"],
"env": {
"X402_NETWORK": "celo",
"PUBLIC_BASE": "https://your-app.up.railway.app",
"BUYER_PRIVATE_KEY": "0x…"
}
}
}
}
Or run it directly:
BUYER_PRIVATE_KEY=0x… X402_NETWORK=celo pnpm mcp
Talking to it
Once connected, ask the assistant something it cannot answer for free. It will discover the endpoint, pay for it, and show you the receipt:
- "What paid APIs are available on Omni402?"
- "Get me the current CELO price using omni402 and show the settlement transaction."
- "Buy the on-chain data endpoint but never spend more than 0.01 USDC."
paid_fetch accepts a maxPrice cap and refuses anything above it. Discovery costs nothing, so an assistant can browse the market before spending.
Agent identity (ERC-8004)
Payments answer "can this agent pay". Identity answers "who is this agent". Omni402 registers an agent on Celo's ERC-8004 Identity Registry, which mints a portable NFT identity that is browsable and reputation-ready.
Publish a registration file describing your agent (the repo's agent.json is a working example), then register it. The signing wallet owns the identity and needs a little CELO for gas:
AGENT_PRIVATE_KEY=0x… X402_NETWORK=celo \
pnpm register-8004 https://raw.githubusercontent.com/you/repo/main/agent.json
It prints your agent ID, the transaction, and an 8004scan link. The reference agent for this project is agent #9765.
| Registry | Network | Address |
|---|---|---|
| Identity | Celo mainnet | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 |
| Identity | Celo Sepolia | 0x8004A818BFB912233c491871b3d84c89A494BD9e |
Dashboard and endpoints
The hub serves everything on a single port, so local and production run the identical code path.
| Path | What it serves |
|---|---|
/ | The live dashboard: revenue, paid calls, per-API income, payments feed with explorer receipts, calls-by-hour. |
/pay | A public catalog of every payable endpoint currently running. |
/pay/<slug> | A payable API. Unpaid requests get a 402; paid requests get the upstream response. |
/api/state | JSON snapshot of all aggregated stats, which is what the dashboard renders. |
/api/health | Liveness probe used by the container healthcheck. |
/ws | WebSocket stream of payment and request events, pushed live to the dashboard. |
You can see the 402 for yourself without a wallet. This returns the payment requirements as JSON:
curl -i https://your-app.up.railway.app/pay/celo-token-prices
Environment reference
| Variable | Needed for | Meaning |
|---|---|---|
X402_NETWORK | always | celo for mainnet or celo-sepolia for testnet. |
SELLER_PAY_TO | live settlement | The wallet that receives the USDC. No private key required. |
MPP_SECRET_KEY | live settlement | Server secret that signs and verifies challenges and receipts. Keep it stable across redeploys. |
X402_API_KEY | live settlement | Facilitator credits key from x402.celo.org, sent as X-API-Key. |
PUBLIC_BASE | deployment | Your public URL, so the MCP catalog advertises payable URLs correctly. |
BUYER_PRIVATE_KEY | buying only | The agent wallet that pays. Never put this on the seller. |
PORT | hosts | Injected by Railway, Render, and Fly. Do not set it yourself. |
.env is gitignored. Never commit a private key, and never paste one into a chat or an issue.
Deploy it
One container serves the dashboard, the API, the WebSocket, and every payable endpoint. Build it from the repository root:
docker build -t omni402 .
docker run -p 4021:4021 --env-file .env omni402
On Railway, create one service from the repo with the root directory set to / so it uses the root Dockerfile. Set the same variables as .env plus PUBLIC_BASE, leave PORT alone, then generate a domain. The healthcheck at /api/health keeps traffic off a half-booted container.
Railway may offer to create a service per package. Decline. The MCP server is a stdio process rather than a web service, and the dashboard is served by the hub, so a single container is correct.
Troubleshooting
The gateway returns 402 forever
The buyer is not paying, or settlement is off. Check the startup banner: if it says challenge-only, then X402_API_KEY or MPP_SECRET_KEY is missing. Also confirm the payout address is a real address rather than the zero address.
Settlement fails with an insufficient balance
The buyer wallet needs USDC, not CELO. Gas is sponsored, but the payment itself is real money, so the balance has to cover the price.
A lane did not start
Its upstream key is missing. The hub skips such lanes deliberately, because a gateway with no upstream key would take the buyer's money and then fail.
USDT signatures are rejected
USDT has no on-chain version(), so its EIP-712 domain must be set explicitly to name Tether USD and version 1. Omni402 pins this per asset already; the error usually means a custom asset was configured by hand.
The unknown endpoint returns the landing page
An unrecognized /pay/<slug> returns a JSON 404 with a hint. If you get HTML instead, the lane router did not initialize, which usually means the hub failed to read lanes.json.