Developers
XGateway runs as a reverse proxy in front of your API. It answers unpaid requests with an x402 payment challenge, verifies the buyer's USDC payment on Base, and forwards paid requests to your API unchanged. Your API needs no billing code.
Building an agent? Give it this file.
A plain-text, machine-readable reference for paying an XGateway-protected API: parsing the 402, signing the payment, the headers, the networks and the error codes.
How a paid request flows
- 1 Request. The buyer calls your route on XGateway, without payment.
- 2 402 challenge. XGateway replies
402 Payment Requiredwith the price, the asset, the network and where to pay. - 3 Authorise. The buyer signs an EIP-3009 USDC transfer authorisation. No gas: the facilitator submits it.
- 4 Pay. The buyer resends the request with the signed payment in the
X-PAYMENTheader. - 5 Verify and forward. XGateway verifies and settles the payment, then forwards the request to your API.
- 6 Response. Your API's response goes back to the buyer. On fixed-price routes it also carries a session token in
X-Payment-Response.
Pricing modes
Fixed price
A fixed USDC price per call, sold in bundles. One payment buys a bundle of calls and returns a JWT (in X-Payment-Response); the buyer sends it as Authorization: Bearer <jwt> on later calls until the credits or the session time run out, then pays again.
Value-based
A percentage of the value your API returns. XGateway forwards the request first, reads the value from the JSON response field message.txValueUSD, and quotes the fee in the 402. The response is held in escrow and released once the buyer pays. One payment per request.
Routes can also be passthrough (free), for endpoints you want behind the same gateway without a charge.
Payments: USDC on Base
Buyers pay in USDC with EIP-3009 TransferWithAuthorization signatures, so they only need USDC, not ETH for gas. Amounts in the 402 are in atomic units (6 decimals: 50000 = $0.05).
| Network | CAIP-2 | USDC contract |
|---|---|---|
| Base (mainnet) | eip155:8453 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Base Sepolia (testnet) | eip155:84532 | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
The 402 challenge
What an unpaid request gets back (testnet example):
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"x402Version": 2,
"error": "X-PAYMENT header is required",
"accepts": [{
"scheme": "exact",
"network": "eip155:84532",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"amount": "50000",
"payTo": "0xMerchantWallet...",
"maxTimeoutSeconds": 120,
"extra": { "name": "USDC", "version": "2" }
}]
}
extra.name and extra.version are the USDC contract's EIP-712 domain; use them as given (they differ between networks).
Minimal example (Node.js, ethers v6)
Pay a 402 and call the route. Save as an ES module (agent.mjs); req is accepts[0] from the 402.
import { ethers } from "ethers";
const wallet = new ethers.Wallet(PRIVATE_KEY);
const chainId = Number(req.network.split(":")[1]);
const nonce = ethers.hexlify(ethers.randomBytes(32));
const validBefore = Math.floor(Date.now() / 1000) + 120; // compute once: signed and sent values must match
const signature = await wallet.signTypedData(
{ name: req.extra.name, version: req.extra.version, chainId, verifyingContract: req.asset },
{ TransferWithAuthorization: [
{ name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" },
]},
{ from: wallet.address, to: req.payTo, value: req.amount, validAfter: 0, validBefore, nonce }
);
const authorization = { from: wallet.address, to: req.payTo, value: req.amount,
validAfter: "0", validBefore: String(validBefore), nonce };
const header = btoa(JSON.stringify({ x402Version: 2, scheme: "exact", network: req.network, payload: {
x402Version: 2, payload: { authorization, signature },
accepted: { ...req, extra: { ...req.extra, assetTransferMethod: "eip3009" } } } }));
const resp = await fetch(URL, { headers: { "X-PAYMENT": header } });
const receipt = JSON.parse(resp.headers.get("X-Payment-Response")); // { token, transactionId }
Error codes
- 402
- Payment required: read
acceptsand pay. - 401
- The session JWT expired: pay again.
- 400
- Malformed
X-PAYMENTheader. - 403
- Insufficient USDC or an invalid signature.
See it work
Run a paid call against a sample endpoint on Base Sepolia.