Skip to content

builders-garden/servex-rs

Repository files navigation

Servex

A reverse proxy that transforms any backend service into a monetized API using the x402 protocol.

What is Servex?

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)  │
                    └─────────────────┘ └────────────────┘

Features

  • 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

Quick Start

Prerequisites

  • Rust 1.85+ (Edition 2024)
  • Redis
  • PostgreSQL

1. Clone and Build

git clone https://github.com/builders-garden/servex-rs.git
cd servex-rs
cargo build --release

2. Configure

# 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

3. Run

# 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

4. Test

# Health check (free endpoint)
curl http://localhost:3000/health

# Paid endpoint returns 402 Payment Required
curl http://localhost:3000/generate -X POST

Configuration

Servex uses a YAML configuration file. See config/servex.example.yaml for a complete example.

Key Configuration Sections

Server

server:
  host: 0.0.0.0
  port: 3000

Storage

storage:
  redis:
    url: redis://localhost:6379
  postgres:
    url: postgres://user:password@localhost:5432/servex

x402 Facilitator

x402:
  # Coinbase CDP facilitator (requires CDP_API_KEY_ID and CDP_API_KEY_SECRET env vars)
  facilitator_url: https://api.cdp.coinbase.com/platform/v2/x402

Service & Endpoints

services:
  - 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: 3600

Environment Variables

Environment 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)

Subdomain Routing

Servex can route requests to different services based on the Host header.

Single Service (Simple)

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 works

Multiple Services

With 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:8081
curl http://images.servex.io/generate   # → image-api backend
curl http://audio.servex.io/transcribe  # → audio-api backend

Resolution Order

  1. Check custom_domains (exact match)
  2. Extract subdomain using SERVEX_BASE_DOMAIN
  3. Fallback: if single service, route all requests to it

Production Setup

SERVEX_BASE_DOMAIN=servex.io cargo run

Local Development

# 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/transcribe

Payment Flow

Servex 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 }

Supported Networks

Network Chain ID Currency
Base 8453 USDC
Base Sepolia (testnet) 84532 USDC
Ethereum 1 USDC
Polygon 137 USDC

Bazaar Discovery

x402 Bazaar is a discovery layer that helps AI agents and developers find x402-enabled APIs programmatically.

Enable Bazaar

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.

What Gets Registered

  • Paid endpoints only - Free endpoints are not registered
  • Requires bazaar config - Endpoints without bazaar metadata are skipped
  • Automatic on startup - No manual registration needed

Backend Integration Guide

Your backend receives requests that Servex has already authenticated and rate-limited. You need to:

  1. Verify the HMAC signature — ensures requests came from Servex
  2. Do your work
  3. For async jobs — POST results to the callback URL

Tip: Implement signature verification as middleware to keep your route handlers clean.

Headers From Servex

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)

Signature Verification Middleware

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)): ...

Async Job Callbacks

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}")

Admin API

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.

Development

Running Tests

# Run all tests
cargo test

# Run with logging
RUST_LOG=debug cargo test -- --nocapture

Running Examples

# 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_client

Project Structure

servex-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

Troubleshooting

Connection Refused

Ensure Redis and PostgreSQL are running and accessible at the configured URLs.

402 Payment Required

This is expected for paid endpoints. Use the x402 client example or implement payment in your client.

Invalid Signature

Check that your backend is using the same HMAC secret and signature format.

Rate Limit Exceeded

Wait for the Retry-After seconds or reset via admin API.

License

MIT License - see LICENSE for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

About

A Rust reverse proxy that turns any backend into a paid API powered by x402, rate limiting, and async job queues.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors