Smart Router
The reliability and security layer for blockchain RPC. Smart Router monitors and orchestrates multiple RPC upstreams in real-time, providing failover, cross-validation, caching, observability & more — across EVM and non-EVM networks.
Docs · Quick Start · How it works · Supported Chains · Releases · License · Contributing · Security
What is Smart Router
RPC proxies have been built in-house thousands of times — along with the failover, caching, and monitoring that surround them. Smart Router is that layer as a standard, actively maintained component: a single endpoint in front of all your RPC upstreams. Instead of building and maintaining your own, you get:
- Automatic failover — retries a bad upstream on another, hedges slow ones in parallel, skips out-of-sync upstreams, and trips a circuit breaker when the pool is exhausted.
- Data cross-validation — fans a read out to several upstreams and returns only once a quorum agrees, stopping conflicting or malicious responses before they reach you.
- Block-aware caching — serves repeat reads without hitting an upstream and without serving stale data; shareable across replicas.
- Transaction broadcasting — sends writes (
eth_sendRawTransaction and equivalents) to all eligible upstreams in parallel to raise success rate and speed.
- Multi-chain, multi-protocol — JSON-RPC, REST, gRPC, Tendermint RPC, and WebSocket across EVM chains, Solana, UTXO chains, Cosmos chains, and more; chains are defined by JSON specs.
- Built-in observability — Prometheus metrics, OpenTelemetry traces, structured logs, and a typed error taxonomy, with a companion Smart Router Dashboard.
Quick Start
The fastest way to start: install the binary, point it at a YAML config, run.
Prerequisites
Build & run
make install
smartrouter config/smartrouter_examples/smartrouter_eth.yml --use-static-spec specs/
After running, you get:
- An RPC endpoint per chain interface (ports from the YAML config; conventional default
:3360).
- Prometheus metrics on
:7779 — see docs/METRICS.md for the full reference.
- A health endpoint at
/lava/health.
- Provider rotation, RPC-aware retry, response caching, and metrics — all driven by the YAML config.
Config wizard
Don't want to hand-write the YAML? A Charm-based TUI builds a smartrouter config
and runs the local docker compose stack — from "which chains?" to a running,
health-verified router.
make wizard # from the repo root (builds the router, then launches)
# or
cd tools/wizard && go run . --repo /path/to/smart-router
See tools/wizard/README.md for the full walkthrough.
Health check (smartrouter health)
A spec-driven, one-shot diagnostic that sends every configured node URL the relays its spec defines,
over that URL's own connection, then prints a single JSON report to stdout. It's chain-agnostic —
it relies entirely on the loaded specs, so any chain or interface with a spec works out of the box
with no per-chain code. Each node URL gets the standard latest-block call plus every verification the
spec declares for what that URL serves (the base collection and its addons/extensions). A
ws(s):// URL answers for the checks of a collection that subscribes.
# Probe every node-url under direct-rpc in a config file
smartrouter health config/smartrouter_examples/smartrouter_eth.yml --use-static-spec specs/
# Or probe an ad-hoc endpoint inline (node-url chain-id api-interface)
smartrouter health https://ethereum-rpc.publicnode.com ETH1 jsonrpc --use-static-spec specs/
The report is the only thing on stdout (all logs go to stderr), so it pipes cleanly into jq
or a downstream verifier:
smartrouter health smartrouter_eth.yml --use-static-spec specs/ 2>/dev/null | jq '.results[] | {name, url, ok, router}'
The document is a uniform envelope — consumers always JSON.parse stdout and read .ok / .error /
.results; they never inspect the exit code, which is 0 for any completed run (endpoint
failures are reported as data, not as a non-zero exit). Only a fatal setup error (bad config,
missing --use-static-spec) exits non-zero, and even then a JSON envelope with a populated error
is printed first.
{
"ok": false,
"error": null,
"results": [
{
"name": "eth-publicnode",
"chainId": "ETH1",
"apiInterface": "jsonrpc",
"url": "https://ethereum-rpc.publicnode.com",
"transport": "http",
"addons": ["archive"],
"extensions": ["archive"],
"specValid": true,
"latestBlock": 25374584,
"ok": false,
"verifications": [
{ "name": "chain-id", "addon": "", "extension": "", "severity": "Fail", "ok": true },
{ "name": "pruning", "addon": "", "extension": "archive", "severity": "Fail", "ok": false, "error": "block not found" }
],
"router": { "verdict": "admitted", "refusedServices": ["archive"] }
}
]
}
There is one row per node URL, and every field in it is that URL's own: its checks ran over its
own connection, and latestBlock is its own height — 0 when the spec gives it no height request,
-1 when the request failed (latestBlockError says why). A row's ok is true only when the spec
loaded (specValid), the URL was reached, and every verification passed, whatever its severity.
Top-level ok is the AND of all rows.
router is what a router started with the same flags does with that URL:
verdict | Meaning |
|---|
admitted | Served, minus any refusedServices — the add-ons and extensions whose own checks failed |
refused | Dropped: it failed a check no single service owns, or could not be dialed. The provider serves from its other URLs |
excluded | The whole provider is dropped: a path it serves has no URL left, or its node URLs are a list the router cannot serve from (reason says which) |
unknown | The probe could not decide — its spec did not load, it timed out, or the URL's checks were only rate-limited |
A failed Warning check fails the row but never the verdict: the router logs it and serves.
When a URL's latest-block call fails, its row reports latestBlock: -1 and the cause in
latestBlockError. Checks that measure a distance from the head are then reported failed with
latest block unavailable instead of being sent; every other check still runs, so the row still
shows what the node does answer.
| Flag | Default | Purpose |
|---|
--use-static-spec | (required) | Spec source(s) — file, directory, or remote URL (same paths as the main command) |
--github-token / --gitlab-token | none | Token for a private spec repository |
--include-backup | false | Also probe upstreams under backup-direct-rpc |
--timeout | 30s | Per-upstream budget for dialing and every relay; a slow or blocked node aborts instead of hanging, and an upstream that has not finished 5s after it is reported as a timed-out row |
--concurrency | 8 | How many upstreams are probed at once |
--skip-websocket-verification | false | Only dial ws(s):// URLs, running no check over them (see note) |
--use-tls / --allow-insecure-connection | false / false | Inline gRPC endpoints only; a config's node URLs carry their own auth-config. Unset, the dial tries plaintext first and upgrades to TLS when that fails |
--log-level | info | Log verbosity (written to stderr) |
Websockets mirror the router's flag. By default a ws(s):// URL runs, over the socket, every
check of a collection that subscribes, as a router does without --skip-websocket-verification.
With the flag the socket is only dialed, as it is by a router started with it. Run health with the
same flag your routers run with, and router reads as theirs.
Run with Docker Compose
No host Go toolchain needed — build and run the binary (plus an optional cache) in Docker:
docker compose -f docker/docker-compose.yml up --build
A single parameterized stack serves every example config (SR_CONFIG=…), with the cache added by layering an overlay compose file. See docs/LOCAL-COMPOSE.md for the full guide — config switching, the cache overlay, multi-chain examples, and logging/metrics.
With the monitoring dashboard
The Smart Router Dashboard is a
Prometheus-driven observability UI for the router (chain/upstream health,
RPS, latency, errors, a live request console, and more). It lives in its own
repo and ships a self-contained stack — router + Prometheus + dashboard — from
one command, so there's nothing to run from here.
git clone https://github.com/Magma-Devs/smart-router-dashboard
cd smart-router-dashboard
make up # router + Prometheus + api (:8000) + web (:3000) + logs (Grafana :3001)
See the dashboard README
for configuration (auth, the values file, logs profile, pointing it at an
existing router on :7779, etc.).
AI / Codex setup
Using Codex or another AI coding agent to run Smart Router locally?
See AGENTS.md.
Configuration
Upstream endpoints are configured in a YAML file. See config/smartrouter_examples/smartrouter_cosmos.yml for an example targeting Cosmos Hub with two distinct public sources per interface (REST + gRPC + Tendermint RPC), and config/smartrouter_examples/smartrouter_multichain_cross_validation.yml for a multi-chain fleet with an active cross-validation policy block. Every bundled example points at public RPC vendors (PublicNode and each chain's official/community endpoints) — no API key required.
Setup scripts are available in scripts/pre_setups/:
# Lava blockchain (REST + gRPC + Tendermint RPC)
./scripts/pre_setups/init_smartrouter_lava.sh
# Ethereum (JSON-RPC)
./scripts/pre_setups/init_smartrouter_eth.sh
How it works
Clients (JSON-RPC, REST, gRPC, Tendermint RPC)
│
▼
┌──────────────────────────────────────────────────────────┐
│ Smart Router │
│ │
│ per-interface listener ─▶ cache check ─▶ hit? ──┐ │
│ │ miss │ │
│ ▼ │ │
│ QoS-weighted upstream │ │
│ selection │ │
│ │ │ │
│ ▼ │ │
│ relay + retry / failover │ │
│ │ │ │
│ ▼ │ │
│ ◀─── response (+ metadata headers, metrics) ────────┘ │
└────────────────────────────┬─────────────────────────────┘
│
▼
Statically-configured upstream RPC providers
(Lava chain providers, PublicNode, Infura,
self-hosted nodes, ...)
The hot path for a single request:
- Listen — a per-interface listener (JSON-RPC, REST, gRPC, or Tendermint RPC) accepts the request and parses it into a normalised internal shape.
- Cache lookup — for cacheable methods (historical block data, immutable receipts, etc.), the cache layer (
ecosystem/cache) checks for a recent response. Hits return immediately. Methods the chain spec marks stateful (writes) and the node-bound family (filters, subscriptions, one node's keys, mempool and status; protocol/rpcsmartrouter/node_bound_methods.go) are never cached: their answers belong to the node that produced them.
- Upstream selection — on cache miss, the upstream optimiser (
protocol/provideroptimizer) picks an upstream from the configured pool using QoS-weighted scoring. Healthy/fast upstreams are preferred; flaky ones get backed off automatically.
- Relay + failover — the request is sent to the chosen upstream. On failure (timeout, malformed response, certain status codes), the retry state machine picks an alternate upstream and retries within a configurable budget.
- Response — returned to the client with metadata headers (
Smart-Router-Version, Lava-Provider-Address, retry counts, etc.) annotating which upstream served the response. Prometheus metrics are emitted in parallel. The upstream's own response headers are not passed through: apart from the transport headers (Content-Type, Content-Encoding, and Retry-After with the upstream's Date, read for a 429's wait) and the headers the chain spec declares in the reply direction for the API interface (today the Cosmos block-height header and Aptos's eight ledger-state headers, pagination cursor included), every header the client sees is the router's.
Cross-validation (optional). For read methods that warrant extra assurance, the relay step can instead fan out to several upstreams in parallel and only return an answer once a quorum agree on an identical response — optionally requiring the quorum to span multiple distinct upstream groups (or each group to reach its own quorum). It defends against a single upstream returning a wrong-but-well-formed answer, and surfaces dissent via response headers and a bounded mismatch metric. See docs/CROSS-VALIDATION.md for an operator setup guide with runnable example configs, or protocol/rpcsmartrouter/README.md for the full knob/header reference.
What it's not
- Not a load balancer. Generic L4/L7 balancers don't speak RPC. They can't distinguish a transient timeout (retry against another upstream), "block not yet produced" (retrying anywhere won't help), and a malformed JSON-RPC envelope (return the error, don't retry). They can't cache by method+params, and they can't back off an upstream that's silently serving stale block data while still returning
200 OK. Smart Router does all of these.
- Not a node. Smart Router doesn't sync chain state or hold a block tree. It forwards requests to upstreams (managed services or self-hosted nodes) configured statically via YAML and scores them on response quality. If every configured upstream goes dark, the router has nothing to fall back on — pair it with an upstream set wide enough to survive operator-level outages.
Supported Chains
Smart Router ships with specs for 120+ chain networks — EVM L1s and L2s, Cosmos SDK chains, non-EVM L1s (Solana, Sui, TON, Starknet, NEAR, Aptos, Move, …), Bitcoin-family chains, Ethereum Beacon, and more. The Index column is the value to reference in your YAML config or --chain-id.
| Chain | Index | Interfaces |
|---|
| Agoric Mainnet | AGR | gRPC, REST, Tendermint RPC |
| Agoric Testnet | AGRT | gRPC, REST, Tendermint RPC |
| Aptos Mainnet | APT1 | REST |
| Arbitrum Mainnet | ARBITRUM | JSON-RPC |
| Arbitrum Nova Testnet | ARBITRUMN | JSON-RPC |
| Arbitrum Sepolia Testnet | ARBITRUMS | JSON-RPC |
| Avalanche C Chain Mainnet | AVALANCHEC | JSON-RPC |
| Avalanche C Chain Testnet | AVALANCHECT | JSON-RPC |
| Avalanche Mainnet | AVAX | JSON-RPC |
| Avalanche P Chain Mainnet | AVALANCHEP | JSON-RPC |
| Avalanche P Chain Testnet | AVALANCHEPT | JSON-RPC |
| Avalanche Testnet | AVAXT | JSON-RPC |
| Axelar Mainnet | AXELAR | gRPC, REST, Tendermint RPC |
| Axelar Testnet | AXELART | gRPC, REST, Tendermint RPC |
| Base Mainnet | BASE | JSON-RPC |
| Base Sepolia Testnet | BASES | JSON-RPC |
| Berachain Artio Mainnet | BERA | JSON-RPC |
| Berachain Artio Testnet | BERAT | JSON-RPC |
| Berachain Bartio Testnet | BERAT2 | JSON-RPC |
| Bitcoin | BTC | JSON-RPC |
| Bitcoin Cash Mainnet | BCH | JSON-RPC |
| Bitcoin Cash Testnet | BCHT | JSON-RPC |
| Bitcoin Testnet | BTCT | JSON-RPC |
| Blast Mainnet | BLAST | JSON-RPC |
| Blast Sepolia Testnet | BLASTSP | JSON-RPC |
| BSC Mainnet | BSC | JSON-RPC |
| BSC Testnet | BSCT | JSON-RPC |
|
Development
Build targets
make build # Build smartrouter binary to build/
make install # Install smartrouter to $GOPATH/bin
make snapshot # Reproduce a release locally in dist/ (binaries + multi-arch Docker image)
make setup # One-time: ensure docker buildx is configured (auto-run by `make snapshot`)
make test # Run all tests
make test-short # Run smart router tests only
make lint # Run go vet
make tidy # Run go mod tidy
make clean # Remove build artifacts
make build and make install inject the same version metadata via ldflags that CI uses (git describe for Version, git rev-parse HEAD for Commit), so a local build on a given commit is byte-identical to CI's for that commit (under the same Go toolchain).
Project structure
cmd/smartrouter/ — Standalone Smart Router binary
protocol/ — Core protocol implementation
chainlib/ — Chain-specific parsers and proxies
rpcsmartrouter/ — Smart router server and relay logic
lavasession/ — Session and connection management
provideroptimizer/ — QoS-based upstream selection
relaycore/ — Relay processing pipeline
metrics/ — Prometheus metrics
types/ — Shared type definitions
specs/ — Chain specification JSON files
Debug endpoints
When started with --debug-address <addr> (and devMode.enabled=true), the router serves a small reset HTTP API for integration tests. It is off by default and absent from production builds.
The reset tests rely on is POST /debug/reset-all, which drains the router's internal state stores (optimizer scores, Ristretto, seen-block caches, session-manager state) and — so a single call returns the router to a serving state after an all-providers-down stress burst — also re-enables endpoint health and cold-rebuilds pairing.
Why that matters: an endpoint that hits MaxConsecutiveConnectionAttempts consecutive connection failures is disabled (Endpoint.Enabled=false), and the only paths back are a successful relay or the ~15-minute epoch tick. After a stress test drives every provider down, those endpoints stay disabled and contaminate later tests until a pod restart. reset-all now re-enables them (mirroring the reset onto the Prometheus health gauge) and re-admits demoted providers via a cold rebuildPairingFromConfig (no re-probing). Every existing reset-all caller inherits this — no test migration.
For tests that only need the endpoint-health reset, POST /debug/reset-endpoint-health does exactly that and nothing else; its name mirrors Endpoint.ResetHealth() in the source. Response (any method other than POST returns 405):
{"reset": true, "endpoints_reenabled": 3}
/debug/reset-pairing and /debug/reset-scores remain available for targeted cleanup.
Releases
Releases are cut by pushing a semver tag matching vX.Y.Z (pre-release suffixes like v1.2.0-rc1 are allowed). The tag push triggers .github/workflows/release.yml, which builds the release artifacts.
git checkout main
git pull
git tag v1.2.0
git push origin v1.2.0
The release is created as a draft. After CI completes, open the Releases page, find the draft, and click Publish release to make it visible. Whether the release is marked as a pre-release is derived from the tag suffix: vX.Y.Z is a final release, vX.Y.Z-rc1 / -beta.2 / etc. are pre-releases. The draft gate is deliberate; flip the draft flag in .goreleaser.yaml to automate.
The :latest Docker tag only moves on final releases — RC and beta tags publish their per-version images but do not overwrite ghcr.io/magma-devs/smart-router:latest.
To re-run the release for an existing tag, go to GitHub → Actions → Publish Smart Router Release → Run workflow, passing the tag name as release_tag.
Artifacts
A release publishes:
- Four statically-linked binaries attached to the GitHub Release:
smartrouter-vX.Y.Z-{linux,darwin}-{amd64,arm64}, plus a sha256sum.txt checksum file.
- A multi-arch Docker image at
ghcr.io/magma-devs/smart-router:vX.Y.Z for linux/amd64 and linux/arm64.
The standalone Linux binaries and the binaries inside the Docker image are produced by the same go build invocation — same toolchain, same flags, byte-identical. GoReleaser owns the entire release-time build via the dockers_v2: block in .goreleaser.yaml.
Docker image tags
| Tag | Source | Stability |
|---|
:vX.Y.Z | release tag | immutable, byte-identical to the standalone binary at that version |
:latest | release tag | floating — points at the most recent final release (not RC/beta) |
:main | push to main branch | floating — most recent dev build from main, not a release artifact |
:<branch>-<version> | push to other branches | per-branch build for testing |
Customers should pin to :vX.Y.Z. :latest is for non-production "just give me the newest stable" use; :main is for previewing unreleased work from main.
The version string is injected at build time from the git tag — smartrouter version prints the tag verbatim, including the v prefix. Builds from non-tagged commits carry git describe output (e.g. v1.2.0-3-gabc1234), so a dev binary cannot masquerade as a release.
Pulling the image
Smart Router Docker images are published to GHCR:
docker pull ghcr.io/magma-devs/smart-router:vX.Y.Z
Tag conventions
- Trigger pattern:
v[0-9]+.[0-9]+.[0-9]+*. A tag like 1.2.0 (without the leading v) does not fire the workflow.
- Follow semver
MAJOR.MINOR.PATCH. For smart-router specifically:
- MAJOR — breaking change to the wire surface customers integrate with: HTTP metadata headers (e.g.
Smart-Router-Version, Lava-Provider-Address), JSON-RPC envelope shape, removed/renamed CLI flags, removed/renamed config fields.
- MINOR — new capabilities: additional supported chains in
specs/, new CLI flags, new metrics, new config fields with safe defaults, new HTTP metadata headers.
- PATCH — internal-only changes: bug fixes, performance improvements, refactors, dependency bumps, docs.
Security
For vulnerability reporting, see SECURITY.md. Do not open public issues for security concerns.
License
Smart Router is dual-licensed. Noncommercial use is free under the PolyForm Noncommercial License 1.0.0, including personal, educational, research, evaluation, development, and testing use. Commercial use requires a separate written Enterprise License from Magma Devs. See LICENSING.md for the dual-license summary and the full commercial terms.
Any commercial use, production use by or for a commercial entity, hosted/SaaS or managed-service use offered to customers or other third parties as part of a commercial product or service, resale, redistribution as part of a commercial offering, use as part of a paid product, or use of premium/enterprise features requires a separate written Enterprise License from Magma Devs. For Enterprise licensing, contact Magma Devs at [email protected].
Enterprise — production support, SLAs, and custom features beyond the open edition are available under that license. Talk to us.
Community