A reverse proxy that transforms any backend service into a monetized API using the x402 protocol.
Servex is a production-ready reverse proxy written in Rust that sits between your clients and backend service. It handles:
- Payments - Automatic USDC payments via the x402 protocol
- Authentication - HMAC-SHA256 signed requests to your backend
- Rate Limiting - Per-endpoint rate limits with Redis
- Async Jobs - Queue long-running tasks and poll for results
┌─────────┐ ┌─────────────────────────────────────────┐ ┌─────────────┐
│ Client │ ──────> │ Servex │ ──────> │ Backend │
└─────────┘ │ │ │ Service │
│ ┌───────────┐ ┌───────────┐ │ └─────────────┘
│ │ x402 │ │ Rate │ │ │
│ │ Payment │ │ Limiter │ │ │
│ └───────────┘ └───────────┘ │ │
│ ┌───────────┐ ┌───────────┐ │ ┌──────▼──────┐
│ │ Job │ │ HMAC │ │ │ Callback │
│ │ Queue │ │ Auth │ │ <────── │ (on done) │
│ └───────────┘ └───────────┘ │ └─────────────┘
└─────────────────────────────────────────┘
│ │
┌─────────▼───────┐ ┌───────▼────────┐
│ Redis │ │ PostgreSQL │
│ (jobs/cache) │ │ (audit logs) │
└─────────────────┘ └────────────────┘
- x402 Payment Protocol - Accept USDC payments on Base, Ethereum, Polygon
- Bazaar Discovery - Let AI agents discover your API automatically
- HMAC Authentication - Cryptographically sign requests to backend
- Rate Limiting - Redis-backed per-endpoint rate limits
- Async Job Processing - Queue jobs, poll for results, automatic callbacks
- Multi-Tenant Support - Route by subdomain or custom domain
- YAML Configuration - Simple, declarative configuration
- Production Ready - Built with Tokio, Axum, and battle-tested crates
- Rust 1.85+ (Edition 2024)
- Redis
- PostgreSQL
git clone https://github.com/builders-garden/servex-rs.git
cd servex-rs
cargo build --release# Copy example configuration
cp config/servex.example.yaml servex.yaml
cp .env.example .env
# Edit servex.yaml with your settings
# - Set your wallet address
# - Set your HMAC secret
# - Configure endpoints# Start Redis and PostgreSQL (example with Docker)
docker run -d -p 6379:6379 redis:alpine
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=password postgres:alpine
# Run Servex
cargo run --release# Health check (free endpoint)
curl http://localhost:3000/health
# Paid endpoint returns 402 Payment Required
curl http://localhost:3000/generate -X POSTServex uses a YAML configuration file. See config/servex.example.yaml for a complete example.
server:
host: 0.0.0.0
port: 3000storage:
redis:
url: redis://localhost:6379
postgres:
url: postgres://user:password@localhost:5432/servexx402:
# Coinbase CDP facilitator (requires CDP_API_KEY_ID and CDP_API_KEY_SECRET env vars)
facilitator_url: https://api.cdp.coinbase.com/platform/v2/x402services:
- name: my-api
subdomain: my-api
secret: "your-hmac-secret"
wallet: "0xYourWalletAddress"
backend_url: http://localhost:8080
endpoints:
- path: /generate
method: POST
backend_path: /generate
chain_id: 8453 # Base mainnet
price: 0.01 # USDC
rate_limit: 10/minute
async: true
timeout: 30s
result_ttl: 3600Environment variables override config file settings:
| Variable | Description |
|---|---|
REDIS_URL |
Redis connection URL |
DATABASE_URL |
PostgreSQL connection URL |
X402_FACILITATOR_URL |
x402 facilitator endpoint |
CDP_API_KEY_ID |
Coinbase CDP API key ID |
CDP_API_KEY_SECRET |
Coinbase CDP API secret |
SERVEX_ADMIN_KEY |
Admin API authentication key |
SERVEX_BASE_DOMAIN |
Base domain for subdomain extraction (e.g., servex.io) |
RUST_LOG |
Log level (e.g., servex=info) |
Servex can route requests to different services based on the Host header.
With one service, subdomains are optional. All requests route to your service:
curl http://localhost:3000/generate # ✅ Works
curl http://test.localhost:3000/generate # ✅ Also worksWith multiple services, subdomains are required to distinguish them:
services:
- name: image-api
subdomain: images
backend_url: http://localhost:8080
- name: audio-api
subdomain: audio
backend_url: http://localhost:8081curl http://images.servex.io/generate # → image-api backend
curl http://audio.servex.io/transcribe # → audio-api backend- Check
custom_domains(exact match) - Extract subdomain using
SERVEX_BASE_DOMAIN - Fallback: if single service, route all requests to it
SERVEX_BASE_DOMAIN=servex.io cargo run# Single service - just use localhost
curl http://localhost:3000/generate
# Multiple services - use *.localhost
curl http://images.localhost:3000/generate
curl http://audio.localhost:3000/transcribeServex implements the x402 payment protocol:
1. Client ──> GET /generate
2. Servex <── 402 Payment Required
{ "accepts": [{ "network": "base", "maxAmountRequired": "10000", ... }] }
3. Client pays via facilitator, receives payment proof
4. Client ──> GET /generate
X-Payment: <payment-proof>
5. Servex validates payment with facilitator
6. Servex ──> Backend (with HMAC signature)
7. Client <── 200 OK { result }
| Network | Chain ID | Currency |
|---|---|---|
| Base | 8453 | USDC |
| Base Sepolia (testnet) | 84532 | USDC |
| Ethereum | 1 | USDC |
| Polygon | 137 | USDC |
x402 Bazaar is a discovery layer that helps AI agents and developers find x402-enabled APIs programmatically.
Add bazaar metadata to your paid endpoints:
endpoints:
- path: /generate
method: POST
price: 0.01
chain_id: 8453
bazaar:
description: "Generate AI images from text prompts"
input:
example: { "prompt": "a sunset over mountains" }
schema:
type: object
required: [prompt]
properties:
prompt: { type: string }
output:
example: { "job_id": "abc123", "status": "pending" }On startup, Servex automatically registers Bazaar-enabled endpoints with the discovery service. The metadata is also included in 402 Payment Required responses.
- Paid endpoints only - Free endpoints are not registered
- Requires
bazaarconfig - Endpoints withoutbazaarmetadata are skipped - Automatic on startup - No manual registration needed
Your backend receives requests that Servex has already authenticated and rate-limited. You need to:
- Verify the HMAC signature — ensures requests came from Servex
- Do your work
- For async jobs — POST results to the callback URL
Tip: Implement signature verification as middleware to keep your route handlers clean.
| Header | Description |
|---|---|
X-Servex-Signature |
HMAC-SHA256(secret, {timestamp}.{METHOD}.{path}.{body}) |
X-Servex-Timestamp |
Unix timestamp (reject if > 5 min old) |
X-Servex-Job-Id |
Job ID (async endpoints only) |
X-Servex-Callback |
Callback URL (async endpoints only) |
Express (Node.js):
// middleware/servex.js
const crypto = require('crypto');
const SECRET = process.env.SERVEX_SECRET;
function servexAuth(req, res, next) {
const signature = req.headers['x-servex-signature'];
const timestamp = req.headers['x-servex-timestamp'];
if (!signature || !timestamp) {
return res.status(401).json({ error: 'Missing Servex headers' });
}
// Reject requests older than 5 minutes (prevents replay attacks)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(401).json({ error: 'Request expired' });
}
// Reconstruct signed message: "{timestamp}.{METHOD}.{path}.{body}"
const body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
const message = `${timestamp}.${req.method}.${req.path}.${body}`;
const expected = crypto.createHmac('sha256', SECRET).update(message).digest('hex');
// Constant-time comparison (prevents timing attacks)
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Attach context for async jobs
req.servex = {
jobId: req.headers['x-servex-job-id'],
callbackUrl: req.headers['x-servex-callback'],
};
next();
}
// Usage: app.post('/generate', servexAuth, handler);FastAPI (Python):
# middleware/servex.py
import hmac, hashlib, time, os
from fastapi import Request, HTTPException
SECRET = os.environ['SERVEX_SECRET'].encode()
async def verify_servex(request: Request):
signature = request.headers.get('x-servex-signature')
timestamp = request.headers.get('x-servex-timestamp')
if not signature or not timestamp:
raise HTTPException(401, 'Missing Servex headers')
# Reject requests older than 5 minutes (prevents replay attacks)
if abs(time.time() - int(timestamp)) > 300:
raise HTTPException(401, 'Request expired')
# Reconstruct signed message: "{timestamp}.{METHOD}.{path}.{body}"
body = (await request.body()).decode()
message = f"{timestamp}.{request.method}.{request.url.path}.{body}"
expected = hmac.new(SECRET, message.encode(), hashlib.sha256).hexdigest()
# Constant-time comparison (prevents timing attacks)
if not hmac.compare_digest(signature, expected):
raise HTTPException(401, 'Invalid signature')
return {
'job_id': request.headers.get('x-servex-job-id'),
'callback_url': request.headers.get('x-servex-callback'),
}
# Usage: @app.post('/generate')
# async def generate(ctx: dict = Depends(verify_servex)): ...For endpoints with async: true, POST results to X-Servex-Callback when done:
POST {callback_url}
X-Servex-Signature: {signed}
X-Servex-Timestamp: {timestamp}
Content-Type: application/json
{"status": "completed", "result": {"your": "data"}}
For failures: {"status": "failed", "result": {"error": "message"}}
Sign callbacks the same way: HMAC(secret, "{timestamp}.POST.{path}.{body}")
Servex exposes admin endpoints for management:
| Method | Path | Description |
|---|---|---|
GET |
/admin/health |
Health check |
GET |
/admin/jobs |
List jobs (with filters) |
GET |
/admin/jobs/{job_id} |
Get job details |
DELETE |
/admin/jobs/{job_id} |
Cancel job |
GET |
/admin/rate-limits |
View rate limit status |
DELETE |
/admin/rate-limits/{key} |
Reset rate limit |
Protected by X-Admin-Key header when SERVEX_ADMIN_KEY is set.
# Run all tests
cargo test
# Run with logging
RUST_LOG=debug cargo test -- --nocapture# Terminal 1: Start the test backend
cargo run --example test_backend
# Terminal 2: Start Servex
cargo run
# Terminal 3: Run the x402 client
PRIVATE_KEY=0x... cargo run --example x402_clientservex-rs/
├── src/
│ ├── main.rs # Entry point
│ ├── config/ # YAML configuration parsing
│ ├── server/ # HTTP router and state
│ ├── proxy/ # Request forwarding, HMAC signing
│ ├── x402/ # Payment protocol implementation
│ ├── rate_limit/ # Redis-based rate limiting
│ ├── jobs/ # Async job queue and callbacks
│ ├── routing/ # Subdomain/domain routing
│ ├── storage/ # Redis and PostgreSQL clients
│ └── admin/ # Admin API endpoints
├── examples/
│ ├── test_backend.rs # Mock backend for testing
│ └── x402_client.rs # Payment client example
├── migrations/ # PostgreSQL migrations
└── config/
└── servex.example.yaml
Ensure Redis and PostgreSQL are running and accessible at the configured URLs.
This is expected for paid endpoints. Use the x402 client example or implement payment in your client.
Check that your backend is using the same HMAC secret and signature format.
Wait for the Retry-After seconds or reset via admin API.
MIT License - see LICENSE for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request