Pay-per-request LLM gateway with stablecoin micropayments on Stellar.
No API keys. No subscriptions. Minimal rate limits.
Just pay USDC on-chain and access any LLM endpoint.
Architecture · Quickstart · API · SDK · Contracts · Deploy
Payments use testnet USDC with no real value. A Stellar mainnet launch is gated by the items in
MAINNET_READINESS.md— most critically an independent contract audit (the Soroban contracts are self-tested; no external audit has been completed) and a fresh mainnet contract deployment.
x402 extends the HTTP 402 Payment Required status code into a real protocol for AI API access. Instead of managing API keys, rate limits, and billing systems, you pay for each request with stablecoins on the Stellar blockchain.
The gateway acts as a reverse proxy that:
- Receives an LLM API request (OpenAI-compatible format)
- Returns HTTP 402 with a Stellar payment address and price quote
- Verifies the on-chain payment via Horizon
- Forwards the request to the upstream LLM
- Returns the LLM response with a payment receipt
This enables permissionless AI access — anyone with a Stellar wallet can use LLMs without signing up, providing payment details, or managing API keys.
| Feature | Benefit |
|---|---|
| $0.00001 fees | Economical for micropayments as small as $0.001 |
| 5-second finality | Near-instant payment confirmation |
| USDC native | Stablecoin support without bridges or wrapped tokens |
| Soroban smart contracts | On-chain verification, escrow, and multisig payouts |
| Horizon API | Simple REST API for querying transactions |
HTTP 402 + Quote
┌──────────────┐ ◄──────────────────── ┌──────────────────────┐
│ │ │ │
│ Caller │ ──── Pay USDC ───────► │ x402 Gateway │
│ (Agent/App) │ │ (NestJS) │
│ │ ◄─── LLM Response ──── │ │
└──────────────┘ └──────────┬───────────┘
│ │
┌────────┘ └─────────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ Stellar │ │ Upstream LLM │
│ Horizon / │ │ OpenAI, etc. │
│ Soroban │ │ │
└──────┬───────┘ └──────────────────┘
│
▼
┌──────────────┐
│ Provider │
│ Dashboard │
│ (Next.js) │
└──────────────┘
Caller Gateway Stellar Upstream LLM
│ │ │ │
│── POST /chat ───────►│ │ │
│ │ │ │
│◄─ 402 {quote} ───────│ │ │
│ │ │ │
│───── Payment ────────│──────────────────────►│ │
│ │ │── confirm ──────────►│
│ │◄── tx_recorded ───────│ │
│ │ │ │
│── POST + txHash ────►│ │ │
│ │── verify tx ─────────►│ │
│ │◄── tx_valid ──────────│ │
│ │ │ │
│ │────────────────────────────── forward ──────►│
│ │◄───────────────────────────── response ──────│
│◄─ LLM response ──────│ │ │
- HTTP 402 Payment Required — Standards-compliant payment flow
- OpenAI-compatible API — Drop-in replacement for
/v1/chat/completions - Streaming (SSE) support — Real-time token streaming to clients
- Single-use payments — Each transaction hash is consumed atomically (DB claim + Redis + on-chain guards); double-use is rejected
- Underpayment enforcement — Per-token debt ledger gates future access until a top-up payment clears it
- Rate limiting — Configurable per-route rate limits for unpaid requests
- Multi-provider — Host multiple LLM providers behind one gateway
- Per-route configuration — Different pricing, models, and upstream URLs per route
| Model | How It Works | Use Case |
|---|---|---|
| Flat-rate | Fixed price per request | Standard API access, known costs |
| Per-token | Pay per token consumed (usage.total_tokens) |
Variable-length responses, fair billing |
For per-token pricing, the client sends a deposit (estimated from max_tokens, or a default token budget when omitted) and the gateway caps forwarded completions to that budget. After the response it calculates the actual cost from usage.total_tokens and reports the surplus/underpayment via headers. Underpayments are recorded as open debt per payer: future requests from that payer are refused with a 402 top-up quote covering deposit + debt until one payment clears the ledger (see MAINNET_READINESS.md).
- Real-time revenue and request analytics
- Route and provider CRUD management
- Payment history with filtering and pagination
- Audit log of all gateway operations
- Wallet-based authentication (Freighter, xBull, Albedo)
- Webhook configuration and testing
- TypeScript/JavaScript SDK with automatic 402 → pay → retry flow
- Streaming support via async generators
- Stellar wallet integration (secret key or external signer)
- Payment confirmation polling with configurable timeout
- Lightweight — depends only on
stellar-sdkandfetch
- Webhook delivery with retry logic and optional HMAC-SHA256 signed payloads
- In-app notifications surfaced in the dashboard
- Event types:
payment_received,verification_failed,request_forwarded - Extensible notification channel system
x402-llm-gateway/
├── apps/
│ ├── gateway/ # NestJS reverse proxy server
│ │ └── src/
│ │ ├── modules/
│ │ │ ├── proxy/ # HTTP proxy with 402 flow
│ │ │ ├── x402/ # Quote generation, payment verification
│ │ │ ├── payments/ # Payment records and status
│ │ │ ├── routes/ # Route configuration CRUD
│ │ │ ├── providers/ # Provider management
│ │ │ ├── analytics/ # Usage and revenue analytics
│ │ │ ├── admin/ # Audit logs and admin operations
│ │ │ ├── webhooks/ # Webhook delivery
│ │ │ └── auth/ # Wallet-based authentication
│ │ ├── common/ # Guards, filters, shared modules
│ │ └── e2e/ # End-to-end tests
│ └── dashboard/ # Next.js provider dashboard
│ └── src/
│ ├── app/ # Pages (routes, payments, settings, etc.)
│ ├── components/ # UI components (sidebar, navbar, providers)
│ └── lib/ # API client, hooks, auth utilities
│
├── contracts/ # Soroban smart contracts (Rust)
│ ├── payment-verifier/ # On-chain payment recording
│ ├── credit-escrow/ # Prepaid credit balances
│ └── multisig/ # Provider payout wallet security
│
├── packages/ # Shared libraries (published as @x402/*)
│ ├── types/ # TypeScript type definitions
│ ├── x402-core/ # Quote generation, payment verification, replay protection
│ ├── sdk/ # Client SDK (402 → pay → retry)
│ ├── config/ # Centralized configuration with env validation
│ ├── logger/ # Structured logging (text + JSON modes)
│ ├── validation/ # Zod schemas for request validation
│ ├── database/ # Prisma client, schema, and migrations
│ ├── wallet/ # Stellar wallet utilities (tx building, Horizon)
│ ├── authentication/ # Wallet challenge-response auth
│ ├── analytics/ # Usage & revenue analytics service
│ ├── notifications/ # Email/webhook/in-app notification delivery
│ ├── shared/ # General utilities (ID generation, timestamps)
│ └── ui/ # Shared UI utilities
│
├── infrastructure/
│ └── docker/ # Dockerfiles (gateway, dashboard) + compose
│
├── .github/workflows/
│ ├── ci.yml # Lint → Test → Build (with PostgreSQL + Redis services)
│ └── deploy.yml # Docker push + Soroban contract deployment (tag-triggered)
│
└── docs/ # Documentation
├── README.md
├── DEPLOYMENT.md
├── CONTRIBUTING.md
└── SECURITY.md
| Model | Purpose |
|---|---|
Provider |
LLM provider/merchant with Stellar wallet |
Route |
Protected endpoint → upstream mapping with pricing |
Payment |
Payment records with on-chain verification data |
Wallet |
Stellar wallet addresses |
PrepaidCredit |
Escrow balances for credit-based billing (v2) |
| Notification | Delivered notification records |
| AnalyticsEvent | Request and payment events for analytics |
| AuditLog | Immutable audit trail of all operations |
- Node.js ≥ 20
- pnpm ≥ 9
- PostgreSQL ≥ 16
- Redis ≥ 7
- Rust (optional — only needed for Soroban contracts)
git clone https://github.com/Pay-Per-Token-LLM-Gateway/pay-per-token-llm-gateway.git
cd pay-per-token-llm-gateway
pnpm install
pnpm nx run database:generate
# 1. Copy the example environment file
cp .env.example .env
# 2. Generate a real JWT_SECRET and paste it into .env
openssl rand -base64 32
# ⚠️ The gateway refuses to start with a missing or placeholder JWT_SECRET.The gateway auto-loads .env from the repository root on startup — no manual export is required. See Environment Files.
docker compose -f infrastructure/docker/docker-compose.yml up -dpnpm nx run database:pushpnpm dev:gateway
# → http://localhost:3000
# → Swagger docs: http://localhost:3000/api/docs
# → Liveness: http://localhost:3000/health · /health/live
# → Readiness: http://localhost:3000/health/ready (Postgres + Redis)
# → Metrics: http://localhost:3000/metrics (Prometheus)pnpm dev:dashboard
# → http://localhost:3001# Without payment — expect HTTP 402
curl -X POST http://localhost:3000/api/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello, world!"}]
}'The gateway supports both testnet and mainnet via the STELLAR_NETWORK environment variable. When deploying to mainnet, ensure you update the following variables to their production counterparts:
STELLAR_NETWORK=mainnetNETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"- The gateway will automatically configure the correct network-aware USDC issuer.
- Use production-grade RPC nodes for
HORIZON_URLandSOROBAN_RPC_URL.
- The gateway loads a
.envfile from the repository root on startup (via@x402/config). This is what makescp .env.example .envwork — no manualexportis needed forpnpm dev:gateway,pnpm exec nx start gateway, or the Docker image (as long as the file is present). - Precedence: variables already present in the environment (Docker, Railway, CI, or your shell) always win and are never overridden by
.env. - Missing file: when
.envdoes not exist, loading is a silent no-op — the gateway simply uses whatever is already in the environment (e.g. containers that inject variables directly). .envis gitignored; only.env.exampletemplates should be committed.
POST /api/v1/chat/completions
| Header | Required | Description |
|---|---|---|
Content-Type |
Yes | application/json |
X-Payment-Hash |
No | Stellar transaction hash (required after paying) |
Request body: OpenAI-compatible chat completion request.
Responses:
| Status | Condition |
|---|---|
200 |
Payment verified, LLM response returned |
402 |
Payment required — quote and instructions in body |
404 |
No route configured for the requested model |
502 |
Upstream LLM request failed |
{
"status": 402,
"message": "Payment Required",
"quote": {
"id": "uuid",
"route": "/v1/chat/completions",
"pricingModel": "flat",
"amount": "1000000",
"asset": "USDC",
"paymentAddress": "GA5ZSE...",
"network": "testnet",
"expiresAt": 1712345678,
"statusUrl": "http://localhost:3000/api/v1/payments/uuid/status"
},
"instructions": "Send 1000000 USDC to GA5ZSE... then retry with X-Payment-Hash header",
"docs": "http://localhost:3000/api/docs"
}# Providers
GET /api/v1/providers
POST /api/v1/providers
GET /api/v1/providers/:id
PUT /api/v1/providers/:id
DELETE /api/v1/providers/:id
# Routes
GET /api/v1/routes
POST /api/v1/routes
GET /api/v1/routes/:id
PUT /api/v1/routes/:id
DELETE /api/v1/routes/:id
# Payments
GET /api/v1/payments
GET /api/v1/payments/:quoteId/status
# Analytics
GET /api/v1/analytics/summary
GET /api/v1/analytics/timeseries
# Admin (all require a wallet session Bearer token)
GET /api/v1/admin/stats
GET /api/v1/admin/audit # scoped to the authenticated wallet's providers
# Webhooks
POST /api/v1/webhooks/test
# Auth
POST /api/v1/auth/challenge
POST /api/v1/auth/verify
GET /api/v1/auth/session
DELETE /api/v1/auth/session
import { X402Client } from '@x402/sdk';
const client = new X402Client({
gatewayUrl: 'https://my-gateway.example.com',
secretKey: 'S...', // Your Stellar secret key for auto-pay
network: 'testnet',
defaultAsset: 'USDC',
});
// Standard call — automatic 402 → pay → retry
const result = await client.call({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Explain x402 in one sentence.' }],
});
if (result.success) {
console.log(result.response.choices[0].message.content);
console.log(`Cost: ${result.cost.amount} ${result.cost.asset}`);
}
// Streaming call
const stream = await client.callStream({
model: 'gpt-4',
messages: [{ role: 'user', content: 'Tell me a story.' }],
});
if (stream.success && stream.stream) {
for await (const chunk of stream.stream) {
process.stdout.write(chunk.choices[0]?.delta?.content || '');
}
}- Sends the LLM request to the gateway
- If HTTP 402: parses the quote, builds a Stellar payment transaction, signs and submits it
- Polls Horizon for confirmation
- Retries the original request with
X-Payment-Hashheader - Returns the LLM response
All of this is transparent to the caller — you write normal LLM API code and the SDK handles payments automatically.
Three Soroban (Rust) smart contracts provide on-chain guarantees:
Records verified payments on-chain with immutable audit trail. Provides:
record_payment— Admin-only payment recording with replay protectionis_payment_used— Deduplication check by transaction hashget_payment/get_payments— Paginated payment queries
Holds prepaid credit balances for account-based billing (v2):
deposit/withdraw— Token deposit and withdrawalcharge— Admin-only balance deduction for usagebalance/get_usage— Balance checks and usage history
Requires M-of-N signer approval for provider payouts:
propose— Create a payout proposalapprove— Signer approval; executes transfer when threshold is metget_proposal/get_config— Proposal and configuration queries
bash scripts/build-contracts.sh
STELLAR_NETWORK=testnet STELLAR_SECRET_KEY=S... bash scripts/deploy-contracts.shdeploy-contracts.sh builds all three contracts, deploys them to the target network, and records the contract IDs in contracts/deployed-addresses.json (gitignored — it is a per-environment deploy artifact). The gateway reads this file at startup via @x402/config and falls back to hardcoded testnet IDs when it is missing.
The contracts store unbounded state (payment audit trail, escrow balances/usage, multisig proposals) as individual persistent ledger entries with per-entry TTLs, so per-transaction gas stays constant as history grows. Storage layout changed in the persistent-storage migration — always deploy the current WASM fresh rather than upgrading in place. See MAINNET_READINESS.md for the mainnet go/no-go gate.
# railway.json is pre-configured
# Deploy via Railway dashboard or CLI
railway upThe gateway Docker image includes Node.js, pnpm, Prisma client generation, and the NestJS build. Railway auto-provisions PostgreSQL and Redis.
# vercel.json is pre-configured
vercel --proddocker compose -f infrastructure/docker/docker-compose.yml build
docker compose -f infrastructure/docker/docker-compose.yml up -dCI automatically deploys contracts to Stellar testnet on v* tags (requires STELLAR_SECRET_KEY secret).
See DEPLOYMENT.md for the complete step-by-step guide.
| Variable | Default | Description |
|---|---|---|
NODE_ENV |
development |
Environment (production, test, development) |
PORT |
3000 |
Gateway server port |
HOST |
0.0.0.0 |
Gateway server host |
DATABASE_URL |
— | PostgreSQL connection string |
REDIS_URL |
— | Redis connection string |
STELLAR_NETWORK |
testnet |
Stellar network (testnet, mainnet, futurenet) — on mainnet the gateway refuses to boot if Horizon/RPC point at a test/future network, the passphrase is foreign, or USDC_ISSUER is not Circle's |
HORIZON_URL |
https://horizon-testnet.stellar.org |
Horizon API endpoint |
SOROBAN_RPC_URL |
https://soroban-testnet.stellar.org |
Soroban RPC endpoint |
HORIZON_TIMEOUT_MS |
10000 |
Per-request Horizon timeout — a hung endpoint can never hold a request open |
SOROBAN_RPC_TIMEOUT_MS |
10000 |
Per-request Soroban RPC timeout |
NETWORK_PASSPHRASE |
Test SDF Network ; September 2015 |
Stellar network passphrase |
USDC_ISSUER |
GBBD47... |
USDC token issuer on Stellar — mainnet requires Circle's issuer |
PUBLIC_GATEWAY_URL |
— | Public base URL used in payment quotes/instructions |
MIN_PAYMENT_AMOUNT |
10000 |
Minimum payment amount in stroops |
PAYMENT_CACHE_TTL |
3600 |
Payment verification cache TTL in seconds |
RATE_LIMIT_WINDOW / RATE_LIMIT_MAX |
60 / 10 |
Per-IP rate limit window (seconds) and max unpaid requests |
SESSION_DURATION |
86400 |
Dashboard session duration in seconds |
CONTRACT_ADMIN_SECRET |
— | Secret key for on-chain payment recording / escrow settlement (store in a secret manager) |
ESCROW_SETTLEMENT_ENABLED |
false |
Opt-in, experimental per-token on-chain settlement via the credit-escrow contract |
JWT_SECRET |
— (required) | Secret key for JWT session tokens — the gateway fails fast if missing or set to a known placeholder (openssl rand -base64 32) |
AUTH_DEV_MODE |
false |
Accept dev-sig- signatures as any wallet — local development only; the gateway refuses to boot with it in production |
TRUST_PROXY |
1 |
Express trust proxy hops so IP-based rate limiting sees real client IPs behind Cloudflare/NGINX/Railway |
QUOTE_EXPIRY_SECONDS |
300 |
Time before quotes expire (5 min) |
LLM_REQUEST_TIMEOUT |
120000 |
Upstream LLM timeout in ms |
LLM_STREAM_TIMEOUT |
600000 |
Upstream streaming timeout in ms |
LLM_MAX_RETRIES |
2 |
Max upstream retries (4xx never retried) |
CORS_ORIGINS |
http://localhost:3001 |
Allowed CORS origins (comma-separated) |
UPSTREAM_API_KEY_<PROVIDER> |
— | Upstream LLM API key per provider |
- Gateway reverse proxy with HTTP 402 flow
- Flat-rate and per-token pricing models
- Stellar payment verification via Horizon
- Redis-backed replay protection
- TypeScript Client SDK (402 → pay → retry)
- Next.js provider dashboard with analytics
- Payment history, audit logs, webhook notifications
- Wallet-based authentication (Freighter, xBull, Albedo)
- Soroban smart contracts (payment-verifier, credit-escrow, multisig)
- CI/CD pipeline (lint → test → build → deploy)
- Docker images and Railway/Vercel deployment configs
- Streaming (SSE) support with per-token pricing in SDK
- Per-token underpayment enforcement (debt gating, top-up quotes, completion cap)
- Mainnet hardening (boot guards, path-payment restriction, persistent-storage contracts)
- Multi-provider routing with load balancing
- Python SDK with LangChain integration
- Kubernetes deployment manifests
- Provider payout automation via multisig contracts
- Prepaid credit escrow contract integration (opt-in experimental today — see MAINNET_READINESS.md)
- Stellar mainnet launch (gated by MAINNET_READINESS.md)
- Multi-chain support (EVM chains, Solana)
- Decentralized provider registry on Soroban
- Fiat on-ramp integration (credit card → USDC → LLM)
- LLM benchmark and quality-of-service scoring on-chain
Self-tested — external audit pending. No third-party firm has audited the
Soroban contracts or the gateway as of September 2026. The in-repo
AUDIT.md is the audit findings ledger; its actionable findings
have been fixed (latest pass 2026-09-08: quote-window integrity, network
fetch timeouts, request-size bounds, readiness + metrics endpoints,
dependency overrides to 0 critical, CI secret/container/lockfile scans,
non-root containers). See MAINNET_READINESS.md
for the go/no-go gate and what a mainnet launch requires first.
| Doc | Contents |
|---|---|
ARCHITECTURE.md |
Components, request flow, storage, contracts, topology |
THREAT-MODEL.md |
Assets, trust boundaries, per-threat mitigations |
API.md |
Full HTTP API reference |
GAS-OPTIMIZATION.md |
Soroban storage/gas design + benchmarking methodology |
OPERATIONS.md |
RTO/RPO, backup/restore, DR, runbooks |
OBSERVABILITY.md |
Logs, metrics, alerts, Grafana dashboard |
DEPLOYMENT.md |
Railway/Vercel/Docker + testnet verification journey |
SECURITY.md |
Disclosure policy, residual risks, production checklist |
- Blockchain as source of truth — All payments verified on-chain via Horizon
- Zero trust for clients — Client-submitted payment proofs are never trusted
- Server-side API keys — Upstream LLM keys are never exposed to callers
- Single-use payments — Every payment hash is consumed atomically (DB claim + Redis replay guard + on-chain guard); double-use is rejected
- Rate limiting — Unpaid requests are throttled per IP (the original "per IP or wallet" wording overstated this)
| Threat | What could go wrong | Status |
|---|---|---|
| Horizon unavailable during payment verification | The gateway reads payment state from Horizon. If Horizon errors or times out at verify time, the request fails with a 5xx — valid payments are never falsely accepted, but legitimate traffic is blocked for the duration of the outage. | Mitigated (fail-closed) — no false acceptance. Open availability exposure: run dedicated Horizon/Soroban RPC providers with API keys and alert on verification-failure spikes. |
| Replay across testnet/mainnet passphrases | A testnet payment replayed on mainnet to obtain paid LLM access. Impossible at the protocol level: Stellar signatures and transaction hashes are scoped to the network passphrase, and the replay guards (DB, Redis, on-chain) are per deployment. | Mitigated at the protocol level and at boot: packages/config now fails fast when STELLAR_NETWORK=mainnet is paired with test/future Horizon/RPC endpoints, a foreign passphrase, or a non-Circle USDC issuer. Residual risk is operator use of a provider-specific mainnet endpoint that is misconfigured — see "Network & replay risk" in MAINNET_READINESS.md. |
| Quote front-running | An observer grabs a victim's 402 quote and pays the payment address first, consuming the quote and forcing the victim to re-quote. Quotes and payment hashes are single-use (atomic DB claim + Redis), and the quote memo is attribution-only — it is not enforced, so a third party can pay someone else's quote. | Partially mitigated. The payment lands in the provider's account — the attacker pays real funds and receives nothing — so this is griefing/DoS rather than theft; the victim simply re-quotes. Memo enforcement is deliberately off to keep the SDK's retry flow working. |
- Use dedicated Horizon/Soroban RPC providers with API keys
- Enable Redis persistence (AOF) for replay protection durability
- Run behind Cloudflare/NGINX with TLS termination
- Rotate JWT secrets regularly
- Use separate Stellar accounts for receiving vs. payouts
- Set up monitoring alerts for payment verification failures
- Implement circuit breakers for upstream LLM failures
(per-hostname, Redis-shared: 5 failures → open 30 s, half-open probe —
see THREAT-MODEL G7 and the
x402_circuit_breaker_opens_totalmetric)
See SECURITY.md for full security policy.
We welcome contributions! See CONTRIBUTING.md for:
- Development setup
- Conventional Commits format
- Code style guide
- Testing instructions
- PR review process
MIT License — see LICENSE for details.
Built with ❤️ on Stellar — the blockchain for real-world payments.