A production-ready, self-hosted payment gateway that implements the x402 protocol — HTTP-native micropayments for APIs. Accept stablecoin payments (USDC) per-request with no API keys, no subscriptions, and no intermediaries.
Forked from azep-ninja's x402-gateway-template to run as a microservice: Bun instead of Express; lmdb instead of Upstash Redis; Typescript and full test suite.
Fork this repo, configure your backend, and start accepting crypto micropayments in minutes.
x402 uses the HTTP 402 Payment Required status code to create a machine-readable payment flow:
1. Agent/client calls your API
2. Gateway returns 402 with payment requirements (chain, amount, token)
3. Agent signs a USDC transfer authorization
4. Agent retries with signed payment in header
5. Gateway verifies signature, settles on-chain, proxies to your backend
6. Client gets the API response + payment receipt
No wallets to integrate. No payment pages. Just HTTP headers.
- Multi-chain support — Accept USDC on Base, Ethereum, Arbitrum, Optimism, Polygon, Avalanche, Unichain, Linea, and Solana out of the box
- MegaETH support — USDM (18 decimals) via Meridian facilitator
- Hybrid settlement — Local on-chain settlement via viem + optional external facilitators
- Solana support — SVM payments via @x402/svm facilitator pattern
- Replay protection — nonce tracking prevents double-spending
- Idempotency —
payment-identifierextension for safe retries without double-charging - Agent discovery —
/acceptedendpoint with full pricing, schemas, and network info - x402 well-known —
/.well-known/x402discovery document - Zero lock-in — Your backend never knows about x402; it just gets authenticated requests
- Deploy small — Bun microservice with LMDB
# Clone the template
git clone https://github.com/artlu99/x402-gateway-bun-lmdb.git
cd x402-gateway-bun-lmdb
# Install dependencies
bun install
# Configure
cp .env.example .env
# Edit .env with your values (see Configuration section)
# Run locally
bun dev
# Test
curl http://localhost:8080/health
curl http://localhost:8080/acceptedClient/Agent request
→ x402 Gateway (this service)
→ Verify payment signature (EIP-712 / SVM)
→ Settle on-chain (USDC transfer)
→ Proxy to your backend API (with internal auth)
→ Return response + payment receipt header
┌─────────────────────────────────────────────────┐
│ x402 Gateway │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Payment │→ │ On-chain │→ │ Backend Proxy │ │
│ │ Verify │ │ Settle │ │ (your API) │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
│ │ │ │
│ ┌──────────┐ ┌──────────┐ │
│ │ LMDB │ │ RPC │ │
│ │ (nonces) │ │ (chains) │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
Edit src/config/routes.ts to define your paid API endpoints:
export const ROUTE_CONFIG = {
// Each key becomes a route prefix: /v1/{key}/*
myapi: {
path: '/v1/myapi/*',
backendName: 'My API',
get backendUrl() { return process.env.MY_BACKEND_URL; },
backendApiKeyEnv: 'MY_BACKEND_API_KEY',
backendApiKeyHeader: 'x-api-key', // Header name your backend expects
get price() { return process.env.MY_PRICE || '$0.01'; },
get priceAtomic() { return process.env.MY_PRICE_ATOMIC || '10000'; }, // 0.01 USDC in 6-decimal units
get payTo() { return process.env.MY_PAY_TO_ADDRESS || process.env.PAY_TO_ADDRESS; },
get payToSol() { return process.env.MY_PAY_TO_ADDRESS_SOL; },
description: 'Description of your API for agent discovery',
mimeType: 'application/json',
},
};| Variable | Description |
|---|---|
SETTLEMENT_PRIVATE_KEY |
Private key (0x hex) for the gas-paying settlement wallet |
PAY_TO_ADDRESS |
Default EVM wallet that receives USDC payments |
BASE_RPC_URL |
At least one RPC URL (Base recommended for low fees) |
| Variable | Description |
|---|---|
MY_BACKEND_URL |
Your backend API base URL |
MY_BACKEND_API_KEY |
Internal API key for your backend |
| Variable | Chain | Required Gas Token |
|---|---|---|
BASE_RPC_URL |
Base | ETH (~$5) |
ETHEREUM_RPC_URL |
Ethereum | ETH (~$20-50) |
ARBITRUM_RPC_URL |
Arbitrum | ETH (~$5) |
OPTIMISM_RPC_URL |
Optimism | ETH (~$5) |
POLYGON_RPC_URL |
Polygon | POL (~$2) |
AVALANCHE_RPC_URL |
Avalanche | AVAX (~$2) |
UNICHAIN_RPC_URL |
Unichain | ETH (~$2) |
LINEA_RPC_URL |
Linea | ETH (~$2) |
MEGAETH_RPC_URL |
MegaETH | N/A (facilitator pays) |
SOLANA_RPC_URL |
Solana | SOL (~$2) |
| Variable | Description |
|---|---|
SOLANA_FACILITATOR_PRIVATE_KEY |
Base58 private key for Solana fee payer |
MY_PAY_TO_ADDRESS_SOL |
Solana wallet to receive USDC payments |
| Variable | Description |
|---|---|
PORT |
Server port (default: 8080) |
MY_PRICE |
Display price (e.g., "$0.05") |
MY_PRICE_ATOMIC |
Price in token atomic units (e.g., "50000" for $0.05 USDC) |
The settlement wallet only pays gas to submit transferWithAuthorization on-chain. It never holds or receives stablecoins — payments flow directly from the payer to your payTo address.
Payer → (USDC) → Your payTo wallet ← receives payment
Settlement wallet → (gas) → on-chain ← only pays gas fees
Fund it with small amounts of native gas tokens on each chain you enable.
- Add route config in
src/config/routes.jt - Register the route in
src/index.ts:
// Paid route
app.all('/v1/myapi/{*path}', x402PaymentMiddleware('myapi'), async (req, res) => {
const subpath = getSubpath(req.params);
const route = ROUTE_CONFIG.myapi;
await proxyToBackend({
req, res,
targetBase: route.backendUrl,
targetPath: '/api/' + subpath,
apiKey: process.env[route.backendApiKeyEnv],
apiKeyHeader: route.backendApiKeyHeader,
});
});
// Free route (no middleware)
app.get('/v1/myapi/health', async (req, res) => {
// Proxy directly without payment
});Map user-friendly paths to your backend's actual routes:
const PATH_ALIASES = {
'analyze': 'internal-analyze-endpoint',
'report': 'generate-full-report',
};
const resolvedSubpath = PATH_ALIASES[subpath] || subpath;- Verify the chain has native Circle USDC with EIP-3009 support
- Add to
src/config/routes.ts:
const MY_CHAIN = {
vm: 'evm',
caip2: 'eip155:CHAIN_ID',
chainId: CHAIN_ID,
rpcEnvVar: 'MY_CHAIN_RPC_URL',
token: usdc('0xUSDC_CONTRACT_ADDRESS'),
};- Register in
ALL_NETWORKS - Add RPC URL to
.env - Fund settlement wallet with gas on that chain
- Add the viem chain import in
src/middleware/x402.js
For chains where USDC doesn't support EIP-3009 yet:
const MY_CHAIN = {
vm: 'evm',
caip2: 'eip155:CHAIN_ID',
chainId: CHAIN_ID,
rpcEnvVar: 'MY_CHAIN_RPC_URL',
facilitator: {
url: 'https://facilitator-api.example.com/v1',
apiKeyEnv: 'FACILITATOR_API_KEY',
networkName: 'mychain', // If facilitator uses short names
facilitatorContract: '0x...', // Facilitator's contract address
x402Version: 1, // Facilitator's x402 version
},
token: {
address: '0x...',
name: 'Token Name',
version: '1',
decimals: 18,
},
};Solana support uses the @x402/svm facilitator pattern where:
- The client partially signs a transaction
- Your facilitator wallet co-signs as fee payer and submits
Requirements:
SOLANA_FACILITATOR_PRIVATE_KEY— Base58 private keySOLANA_RPC_URL— Solana RPC endpoint*_PAY_TO_ADDRESS_SOL— Solana wallet per route
Ask your LLM about running the Bun binary as a service or with pm2
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
Free | Gateway health + backend status |
| GET | /accepted |
Free | Agent discovery — pricing, networks, schemas |
| GET | /.well-known/x402 |
Free | x402 discovery document |
| ALL | /v1/{route}/* |
Paid | Your protected API routes |
Agents call GET /accepted to discover your API before making paid requests:
curl https://your-gateway.com/acceptedReturns supported networks, pricing, and input/output schemas for each route. Compatible with Bazaar agent discovery protocol.
Any wallet that supports the x402 protocol works:
| Wallet | Description |
|---|---|
| @x402/fetch | Official SDK — drop-in fetch replacement |
| OpenClaw / Lobster | Agent framework with built-in x402 |
| AgentWallet (frames.ag) | x402 endpoint for agents |
| Vincent | MPC wallet with policy controls |
| Sponge | x402_fetch one-liner integration |
import { x402Fetch } from '@x402/fetch';
const res = await x402Fetch('https://your-gateway.com/v1/myapi/endpoint', {
method: 'POST',
body: JSON.stringify({ key: 'value' }),
wallet // any x402-compatible wallet
});
const data = await res.json();├── src/
│ ├── index.ts # app, route registration
│ ├── proxy.ts # Backend proxy (injects internal auth)
│ ├── middleware/
│ │ └── x402.ts # Payment verification + settlement
│ ├── config/
│ │ └── routes.ts # Route definitions + network registry
│ └── utils/
│ ├── cors.ts # cors headers
│ └── store.ts # Nonce tracking + idempotency cache
├── public/
│ └── index.html # Landing page (optional)
├── .env.example
└── package.json
- Settlement key — Store in a secrets manager (GCP Secret Manager, AWS Secrets Manager, etc.), never in env vars or code
- Settlement wallet — Only holds gas tokens, never stablecoins. If compromised, attacker can only drain small gas balances
- Pay-to address — This is YOUR wallet. Payments go directly from payer to you on-chain. The gateway never custodies funds
- LMDB — Used for nonce tracking. If store is unavailable, gateway fails open on reads (settlement still checks on-chain) and fails closed on writes (rejects payment to be safe)
- Backend API keys — The gateway injects these server-side. Your backend never sees x402 traffic directly
MIT