Skip to content

Repository files navigation

Solana RPC Proxy

A production-grade FastAPI proxy in front of the Solana JSON-RPC API, adding API-key auth, Redis-backed sliding-window rate limiting, structured error handling, and signed webhooks for real-time account-change notifications.

Built to demonstrate the difference between "a script that calls an API" and "a service you'd actually put in front of production traffic": bounded retries, atomic rate limiting, observability, graceful shutdown, and tests that actually exercise the failure paths.

Why this exists

Public and even paid Solana RPC endpoints throttle hard, and every serious Solana app ends up needing the same infrastructure: a layer that absorbs rate limits, retries transient failures, and turns raw JSON-RPC errors into something a client can actually branch on. This proxy is that layer, plus webhook-based account watching so consumers don't have to manage their own WebSocket subscriptions.

Architecture

                    ┌─────────────────────────────────────────┐
                    │              FastAPI App                 │
                    │                                            │
  Client ──POST──▶  │  RequestContext │ RateLimit │ CORS          │
  /rpc               │  middleware      middleware                │
                    │        │                                   │
                    │        ▼                                   │
                    │  /rpc route ──▶ SolanaRpcClient ──▶ Upstream │
                    │                 (retries, timeouts)  Solana  │
                    │                                       RPC    │
                    │  /webhooks route ──▶ AccountSubscriptionMgr  │
                    │        │                    │                │
                    │        ▼                    ▼                │
                    │     Redis            Upstream Solana          │
                    │  (subscriptions,     WebSocket                │
                    │   rate limit state,  (accountSubscribe)        │
                    │   delivery log)             │                │
                    │                              ▼                │
                    │                     WebhookDeliveryService     │
                    │                     (HMAC-signed POST,        │
                    │                      exponential backoff)     │
                    │                              │                │
                    └──────────────────────────────┼────────────────┘
                                                   ▼
                                        Your callback_url

Key components:

Component File Responsibility
RPC proxy app/routes/rpc.py, app/services/solana_client.py Forwards JSON-RPC 2.0 calls upstream with retries/timeouts
Rate limiter app/services/rate_limiter.py Atomic sliding-window limiting via a Redis Lua script
Webhook manager app/services/account_subscription_manager.py Owns upstream accountSubscribe WebSocket connections, fans out to subscribers
Webhook delivery app/services/webhook_delivery.py HMAC-signs and delivers webhook payloads with exponential backoff
Error handling app/core/exceptions.py, app/core/error_handlers.py Normalizes every failure mode into one consistent JSON error shape

Features

  • Transparent JSON-RPC proxy — point any @solana/web3.js Connection at this URL; the request/response shape is unchanged
  • Sliding-window rate limiting — a Redis Lua script makes the check-and-increment atomic, avoiding the 2x-burst-at-window-boundary bug of naive fixed-window counters
  • Signed webhooks for account changes — register a callback_url, get an HMAC-SHA256-signed POST every time a watched account's balance or data changes
  • Structured errors — every error response has the same shape (error_code, message, details, request_id), whether it originated from validation, rate limiting, or the upstream Solana node
  • Bounded retries — transient upstream failures (timeouts, connection resets) are retried with backoff; a valid JSON-RPC error is never retried
  • Observability — JSON structured logs, request-ID correlation, Prometheus metrics at /metrics
  • Graceful shutdown — WebSocket listeners and delivery tasks are cancelled cleanly on SIGTERM

Quickstart

git clone <this-repo>
cd solana-rpc-proxy
cp .env.example .env
# edit .env: set API_KEYS and WEBHOOK_SIGNING_SECRET to real values

docker compose up --build

The API is now live at http://localhost:8000. Interactive docs at http://localhost:8000/docs (disabled automatically when ENVIRONMENT=production).

# Proxy a JSON-RPC call
curl -X POST http://localhost:8000/rpc \
  -H "X-API-Key: dev-local-key-change-me" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"getSlot","params":[]}'

# Watch an account for balance/data changes
curl -X POST http://localhost:8000/webhooks \
  -H "X-API-Key: dev-local-key-change-me" \
  -H "Content-Type: application/json" \
  -d '{
    "account_address": "11111111111111111111111111111111",
    "callback_url": "https://your-server.example.com/hook",
    "commitment": "confirmed"
  }'

API reference

POST /rpc

Transparent JSON-RPC 2.0 proxy. Requires X-API-Key. Blocks sendTransaction and requestAirdrop by default (edit _BLOCKED_METHODS in app/routes/rpc.py if you want to allow them through this proxy — think carefully before you do).

POST /webhooks

Register a webhook. Body:

{
  "account_address": "base58 pubkey",
  "callback_url": "https://...",
  "commitment": "processed | confirmed | finalized",
  "label": "optional string"
}

Returns a subscription_id. Every account change triggers a POST to callback_url with body:

{
  "event": "account_change",
  "subscription_id": "...",
  "account_address": "...",
  "lamports": 123456,
  "owner": "...",
  "executable": false,
  "rent_epoch": 361,
  "data_base64": "...",
  "slot": 123456789,
  "timestamp": "2026-08-02T12:00:00+00:00"
}

and header X-Webhook-Signature: an HMAC-SHA256 hex digest of the raw request body, keyed with WEBHOOK_SIGNING_SECRET. Verify this before trusting the payload — anyone can POST to your endpoint pretending to be this proxy:

import hmac, hashlib

def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header)

GET /webhooks, GET /webhooks/{id}, DELETE /webhooks/{id}

List, inspect, and remove subscriptions.

GET /webhooks/{id}/deliveries

Last 50 delivery attempts for a subscription — status codes, errors, timestamps. Useful for debugging a flaky callback_url.

GET /health, GET /health/ready

/health is a bare liveness check (no auth, no dependency checks). /health/ready additionally pings Redis and returns 503 if it's unreachable — use this one for your load balancer's readiness probe.

GET /metrics

Prometheus-format metrics: request latency/count/status by route (via prometheus-fastapi-instrumentator), plus solana_proxy_rpc_requests_total, solana_proxy_webhook_deliveries_total, solana_proxy_rate_limit_rejections_total.

Configuration

All configuration is environment variables (see .env.example for the full list with defaults). The important ones:

Variable Default Notes
SOLANA_RPC_URL api.mainnet-beta.solana.com Point at Helius/QuickNode for anything beyond light testing
REDIS_URL redis://localhost:6379/0 Required — the app refuses to start if unreachable
API_KEYS (empty) Comma-separated; set REQUIRE_API_KEY=false to disable auth entirely (not recommended)
RATE_LIMIT_REQUESTS / RATE_LIMIT_WINDOW_SECONDS 60 / 60 Per-API-key (or per-IP if unauthenticated)
WEBHOOK_SIGNING_SECRET (placeholder — change this) HMAC key for signing outgoing webhook payloads

Testing

pip install -r requirements-dev.txt

# Needs a local Redis reachable at REDIS_URL (or run via Docker below)
pytest -v --cov=app --cov-report=term-missing

Or fully containerized, no local Redis needed:

docker compose -f docker-compose.test.yml up --build \
  --abort-on-container-exit --exit-code-from tests

Tests mock the upstream Solana RPC with respx and use fakeredis for Redis, so the suite runs with no real network calls except within the Docker/CI Redis service container itself.

Known limitations / scaling notes

  • Single-instance webhook ownership. AccountSubscriptionManager holds upstream WebSocket connections and subscriber state in process memory. Running multiple replicas of this service would cause each replica to independently open its own accountSubscribe connection for the same account, delivering every webhook multiple times. This is why the Docker image runs with --workers 1. Scaling this out requires a coordination layer (e.g., a Redis lock per account_address so only one replica owns the upstream subscription, with events published to the others over Redis pub/sub) — a natural next step, deliberately left out of scope here to keep the core proxy logic legible.
  • Webhook retries are in-memory, not durable. If the process restarts mid-retry-backoff, an in-flight delivery attempt is lost (though the subscription itself survives via Redis and will resume delivering on the next account change). A durable queue (e.g. Redis Streams or a proper task queue) would close this gap for high-reliability use cases.
  • No persistent request-level API key store. Keys are a static list from config. app/core/security.py isolates this behind one function so swapping in a database-backed key store with per-key usage tracking is a contained change.

Tech stack

FastAPI · Redis (redis.asyncio) · httpx · websockets · Pydantic v2 · Prometheus · Docker

License

MIT — see LICENSE.

About

Production-grade FastAPI proxy for the Solana JSON-RPC API — rate limiting, structured error handling, and signed webhooks for real-time account-change notifications.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages