Skip to content
Magma Devs
Smart Router for developers

One endpoint for every RPC provider. On your infrastructure.

Smart Router sits between your services and the providers you already contract with. It runs on your infrastructure, your keys and contracts stay yours, and it records why it chose each upstream. A routing layer you operate and inspect, not a black box you send traffic through.

Runs in your infrastructureno SDKyour keys, your provider contractsopen source

Would rather not operate it yourself? Smart Router Cloud runs the same software for you →

quick start
git clone https://github.com/Magma-Devs/smart-router.git
cd smart-router

docker compose -f docker/docker-compose.yml up --build
first request
curl -X POST http://127.0.0.1:3360 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
That request now goes through Smart Router instead of directly to one RPC endpoint.
Failover

A provider fails. The request still lands.

Three upstreams are configured and taking traffic. One goes down: the failing provider leaves the request path and rejoins after a successful relay. Unservable requests give up inside their budget instead of hanging.

Defaults: 2 retries · 30s relay budget · 10 attempts · 1,000 sessions/node · health check 5m

Provider incidents are not hypothetical. Our RPC provider tracker logs them continuously.

request path
Application
Smart Router
  • ├──Infurahealthy34%
  • ├──Alchemyhealthy33%
  • └──Private Nodehealthy33%

// All upstreams eligible. Share follows the routing strategy.

metricsdemo values
request rate
1.2k rps
p95 latency
78 ms
retries / min
0
failovers / min
0
upstreams eligible
3 of 3

Illustrative product demo. Values are not benchmarks.

hedged request · example trace
0 ms      request received
            ↓
4 ms      upstream alchemy selected
            ↓
310 ms    response still pending
            ↓
          hedge sent to infura
            ↓
355 ms    infura responds
            ↓
          first valid response returned

And if all three are down at once.

Smart Router forwards requests, it does not sync the chain, so an empty pool has nothing behind it. Your backup-direct-rpc tier takes over if configured; otherwise the upstream's own error reaches your client unmodified.

It removes single-provider risk, not the need for more than one provider.

Nothing here is hidden, and every default is yours to change.

Retry

Set the limit to 0 to turn retries off, or set your own per-attempt budget per request.

Hedging

A slow attempt gets a parallel retry before the first times out, on a tick derived from block time and method. Read paths only.

Integrity checks

Checks the response against what the request asked for, so a wrong block or truncated result is caught at the routing layer.

Circuit breaking

2 consecutive empty-pool errors trip the breaker: the relay then fails immediately instead of looping until the timeout.

Transaction broadcasting

eth_sendRawTransaction and its equivalents go to every eligible upstream in parallel, so one provider dropping your transaction does not lose it.

Block-aware caching

Keyed by chain, method, params and resolved block: finalized reads 1h, latest-style 500ms. Writes and pending state are never cached, and a request header forces a refresh.

Cross-validation

A 200 response can still be wrong.

For calls where one response is not enough, Smart Router sends the same request to independent providers and compares results before returning one.

Set how many providers participate and how many must agree. Group labels can require separate infrastructure. Callers can raise participation, never shrink the operator's minimum.

When the quorum does not hold the request fails and says why. A minority answer never becomes the response.

no-agreementinsufficient-responsesdiversity-unmetinsufficient-capacityinsufficient-groups

Use it on selected methods, not every call across every provider.

eth_getLogs · participants 3 · agreement 2
  • Provider Apending
  • Provider Bpending
  • Provider Cpending
2 matching responses
1 conflict
result accepted
config.yml
cross-validation:
  policies:
    - chain-id: "ETH1"
      api-interface: "jsonrpc"
      method: "eth_getLogs"
      enabled: true
      agreement-threshold: { floor: 2 }
      max-participants: { floor: 3, cap: 5 }

    - chain-id: "ETH1"
      api-interface: "jsonrpc"
      method: "eth_blockNumber"
      forbid-caller-cv: true
Node selection

The next provider is not picked at random.

Smart Router scores upstreams on observed latency, sync freshness and availability. A fast node behind the chain does not win; a provider gets less attractive as its latency or error rate climbs.

scored upstreams · example output
provider        latency     block       errors      score
alchemy         71 ms       current     0.1%        0.95
infura          84 ms       current     0.2%        0.91
quicknode       96 ms       current     0.4%        0.83
node-03         42 ms       -4 blocks   0.0%        0.48

Scoring is also how a bad node leaves and returns: an endpoint that keeps failing is disabled until a successful relay or the epoch tick. Health checks 5m · optional 5s liveness probe · 1,000 sessions per node.

strategy
smartrouter config.yml \
  --use-static-spec specs/ \
  --strategy latency
See selection policies
Integration

Keep your client. Keep your providers.

It sits in front of the RPCs you already use and decides where each request goes. Point your existing client at it. No SDK, no Magma-specific request format.

one endpoint, many upstreams
                         Infura
                           ▲
                           │
viem                       │
ethers  ──▶ Smart Router ──┼──▶ Alchemy
web3.py                    │
backend                    ├──▶ QuickNode
                           ├──▶ Helius
                           └──▶ your node
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const client = createPublicClient({
  chain: mainnet,
  transport: http('http://127.0.0.1:3360'),
});

Per-upstream auth covers headers, query keys and mTLS. Inbound auth, CORS and rate limiting stay at the edge.

Interfaces are set per listener with api-interface and must be supported by the chain spec.

Observability

See which RPC is slow before your users tell you.

smart-router · ethereum-mainnet 4 upstreamsdemo data
providerlatencyblockerrorssharescore
Infura82 mscurrent0.2%38%0.91
Alchemy72 mscurrent0.1%41%0.95
QuickNode95 mscurrent0.4%15%0.83
Node-0344 ms-4 blocks0.0%6%0.48
p50 / p95 / p99 latency69 / 92 / 141 ms
requests / min
72k
error rate
0.2%
retries
34
hedges
19
cache hits
41%
xval mismatches
1
recent events
  • hedge eth_call → alchemy after 300ms on infura
  • retry eth_getLogs → quicknode (upstream 503)
  • skip node-03 lag 4 blocks > threshold 2
  • cache hit eth_getBlockByNumber (block 21_904_118)
  • xval eth_getLogs 3 participants, 3 agree
  • probe node-03 back within lag threshold

Prometheus metrics on metrics-listen-address, OpenTelemetry traces with W3C context propagation, and an optional dashboard for per-upstream share, percentiles, retries, hedges, cache hits and mismatches.

The highest-signal alert is any cross-validation mismatch on finalized data, where an upstream disagreed about history that cannot change. Starting alert set in the docs: no endpoint serving, error ratio > 5%, p99 > 2s, cache hits < 20%.

Configuration

Keep the routing config in your repo.

One YAML file defines the listeners it opens (endpoints) and the upstreams it routes to. Provider keys stay behind ${var} placeholders, rendered at launch from a gitignored .env.

A config the router cannot serve fails at startup. Changes need a restart, and a 25s drain means rolling one instance at a time. Pin the image: a major bump means a breaking change.

Working configs ship under config/smartrouter_examples/. The wizard generates and health-checks a starting setup from your chains and upstreams.

config.yml
 1endpoints: 2  - network-address: "0.0.0.0:3360" 3    chain-id: "ETH1" 4    api-interface: "jsonrpc" 5 6direct-rpc: 7  - name: "eth-provider-a" 8    chain-id: "ETH1" 9    api-interface: "jsonrpc"10    group-label: "external"11    node-urls:12      - url: "https://your-eth-provider.example.com"13        timeout: 10s14        auth-config:15          auth-headers:16            x-api-key: "${RPC_KEY_ETH}"17      - url: "wss://your-eth-provider.example.com/websocket"1819  - name: "eth-own-node"20    chain-id: "ETH1"21    api-interface: "jsonrpc"22    group-label: "internal"23    node-urls:24      - url: "http://10.0.4.11:8545"25      - url: "ws://10.0.4.11:8546"
run
smartrouter path/to/config.yml \
  --use-static-spec specs/

Start with an example that already runs.

Other chains resolve from the spec catalog at startup. Supported chains

Don't take our word for it. Run it.

Clone Smart Router. Add your RPC endpoints. Put traffic through it. Then kill one, kill all of them, and kill the router.

It is stateless: run 2+ instances behind your load balancer. /metrics/overall-health returns 503 when no upstream is serving. Wire it to your LB and your pager.

clone
git clone https://github.com/Magma-Devs/smart-router.git