A high performance, multi chain payment gateway built with Rust and Axum, implementing x402 (V2) to monetize HTTP APIs using USDC across EVM and Solana networks, with integrated Oyster CVM TEE signature verification for secure, enclave backed request validation.
- Multi-Chain Support: Accept payments on multiple networks simultaneously (e.g., Base, Polygon, Solana).
- x402 V2 Protocol: Payment requirements returned in the
payment-requiredheader. - Per-Endpoint Pricing: Configure different payment amounts for different routes.
- TEE Signatures: Responses are signed using a secp256k1 key (via Oyster KMS or env var) for enclave-backed verification.
The service is configured via a config.json file. You can set the path using the CONFIG_PATH environment variable (defaults to config.json).
{
"gateway_port": 3000,
"facilitator_url": "https://www.x402.org/facilitator",
"target_api_url": "http://127.0.0.1:11434",
"networks": [
{
"type": "evm",
"network": "base-sepolia",
"payment_address": "0xYOUR_EVM_ADDRESS"
},
{
"type": "solana",
"network": "solana-devnet",
"payment_address": "YOUR_SOLANA_PUBKEY"
}
],
"routes": {
"free": [
"/api/version"
],
"protected": [
{
"path": "/api/chat",
"usdc_amount": 1
}
]
}
}gateway_port: The port the gateway listens on (default: 3000).facilitator_url: The x402 facilitator service URL.target_api_url: The backend API URL to proxy requests to.networks: Array of supported blockchain networks.type:"evm"or"solana".network: Network identifier (e.g.,"base-sepolia","solana-devnet").payment_address: Your wallet address for receiving payments.
routes:free: List of public routes that bypass payment checks.protected: List of routes requiring payment.path: The URL path.usdc_amount: Cost in USDC microunits (e.g., 1000 = 0.001 USDC).
| Variable | Description | Default |
|---|---|---|
CONFIG_PATH |
Path to config.json |
config.json |
SIGNING_PRIVATE_KEY_HEX |
Hex-encoded 32-byte secp256k1 private key for signing responses | — |
SIGNING_KEY_DERIVE_URL |
URL to derive signing key from KMS | http://127.0.0.1:1100/derive/secp256k1?path=signing-server |
If
SIGNING_PRIVATE_KEY_HEXis set, it takes priority. Otherwise the gateway fetches the key from the KMS derive URL (used in Oyster CVM deployments).
- Install Rust: Ensure you have Rust and Cargo installed.
- Configure: Copy
config.example.jsontoconfig.jsonand update with your details. - Set signing key (for local dev):
export SIGNING_PRIVATE_KEY_HEX="your_64_char_hex_private_key"
- Run:
Or with custom config path:
cargo run
CONFIG_PATH=production.json cargo run --release
This setup demonstrates monetizing an Ollama LLM behind the x402 gateway.
- Ollama installed locally, or use the Docker Compose setup below.
- Start Ollama:
ollama serve
- Pull a model:
ollama pull qwen3:0.6b
- Update
config.jsonto point to Ollama:{ "target_api_url": "http://127.0.0.1:11434", "routes": { "free": ["/api/version"], "protected": [{ "path": "/api/chat", "usdc_amount": 1 }] } } - Run the gateway:
SIGNING_PRIVATE_KEY_HEX="your_key" cargo run - Test a free route:
curl http://localhost:3000/api/version
- Test a protected route (returns 402):
curl -v http://localhost:3000/api/chat
The docker-compose.yml bundles the gateway with Ollama and auto-pulls the qwen3:0.6b model:
services:
x402-gateway:
image: sagarparker/x402-gateway:latest
network_mode: host
environment:
- CONFIG_PATH=/init-params/config.json
volumes:
- /init-params:/init-params:ro
ollama_server:
image: alpine/ollama:0.10.1
network_mode: host
ollama_model:
image: alpine/ollama:0.10.1
command: pull qwen3:0.6b
network_mode: host
depends_on:
ollama_server:
condition: service_healthyYou can deploy the gateway to an Oyster CVM enclave. The config file is provided externally via init-params.
-
Simulate locally (for testing):
oyster-cvm simulate --docker-compose docker-compose.yml --init-params "config.json:1:0:file:./config.json" -
Deploy to Oyster CVM:
oyster-cvm deploy \ --wallet-private-key <key> \ --duration-in-minutes 30 \ --arch amd64 \ --docker-compose docker-compose.yml \ --init-params "config.json:1:0:file:./config.json"
The --init-params flag follows the format: <enclave_path>:<attest>:<encrypt>:<type>:<value>
config.json— placed at/init-params/config.jsoninside the enclave1— included in attestation0— not encrypted (use1if your config contains secrets)file— read from a local file./config.json— path to the local config file
Access protected routes directly. The gateway returns 402 Payment Required with payment details in the payment-required header if no valid payment is present.
curl -v http://localhost:3000/api/chatEVM Mainnets: Base, Polygon, Avalanche, Sei, XDC, XRPL EVM, Peaq, IoTeX, Celo
EVM Testnets: Base Sepolia, Polygon Amoy, Avalanche Fuji, Sei Testnet, Celo Sepolia
Solana: Mainnet, Devnet
Note: Set
SIGNING_PRIVATE_KEY_HEX(for local) or deploy on Oyster CVM (for KMS-derived keys).
Recover the public key from a signed response:
cargo run --bin verifier -- http://<ENCLAVE_IP>:8888/your-endpointGet the expected public key directly from the KMS:
oyster-cvm kms-derive \
--image-id <IMAGE_ID> \
--path signing-server \
--key-type secp256k1/publicThe public key from the verifier should match the public key from kms-derive. This confirms that:
- The response was signed by a valid Oyster enclave
- The enclave is running the expected image (identified by
image-id) - The signature was created using the KMS-derived key for
signing-serverpath
The X-Signature header contains a 65-byte hex-encoded signature:
- Bytes 0–63: ECDSA signature (r, s)
- Byte 64: Recovery ID + 27 (Ethereum-style)
The signed message is the Keccak256 hash of:
"oyster-signature-v2\0" ||
u32be(len(request_method)) || request_method ||
u32be(len(request_path_and_query)) || request_path_and_query ||
u64be(len(request_body)) || request_body ||
u64be(len(response_body)) || response_body