Smart Router

July 8, 2026 · View on GitHub

Smart Router — the reliability and security layer for blockchain RPC

Smart Router

Build and Test Release Go License

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 crafts and sends the relays each spec defines to every configured upstream node URL, 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. For each node URL it runs the standard latest-block call plus every verification the spec declares for that node's addons/extensions (archive / debug / trace and, when the spec supports subscriptions, a websocket check on wss:// URLs).

# 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 (address 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}'

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": "wss://ethereum-rpc.publicnode.com",
      "transport": "ws",
      "addons": ["debug"],
      "extensions": ["archive"],
      "specValid": true,
      "latestBlock": 25374584,
      "ok": false,
      "verifications": [
        { "name": "chain-id", "addon": "",      "extension": "",        "ok": true },
        { "name": "archive",  "addon": "",      "extension": "archive", "ok": false, "error": "block not found" }
      ]
    }
  ]
}

An upstream with multiple node URLs (e.g. an https:// and a wss:// endpoint) yields one row per URL, distinguished by url/transport. An endpoint's ok is true only when the spec loaded (specValid) and every verification passed. Top-level ok is the AND of all rows.

FlagDefaultPurpose
--use-static-spec(required)Spec source(s) — file, directory, or remote URL (same paths as the main command)
--include-backupfalseAlso probe upstreams under backup-direct-rpc
--timeout30sPer-upstream timeout, and the basis for the global wall-clock cap (upstreams probe concurrently; the run never exceeds timeout + 5s). A slow/blocked node aborts instead of hanging
--skip-websocket-verificationfalseExclude ws:///wss:// endpoints and the spec's websocket verification (see note)
--log-levelinfoLog verbosity (written to stderr)

Websocket is verified by default. For any chain whose spec supports subscriptions, the command probes the configured ws:///wss:// endpoints and runs the spec's websocket verification — so the health check exercises the full surface a supported chain exposes. A blocked or slow ws node can't stall the run: each upstream is bounded by --timeout, upstreams probe concurrently, and a global wall-clock cap (timeout + 5s) guarantees the command returns even if a connector wedges (an upstream that doesn't finish in time is reported as a timed-out row). Pass --skip-websocket-verification to exclude ws endpoints — useful for a fast HTTP-only sanity check; each excluded URL is then reported as a row marked "websocket verification skipped".

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:

  1. Listen — a per-interface listener (JSON-RPC, REST, gRPC, or Tendermint RPC) accepts the request and parses it into a normalised internal shape.
  2. Cache lookup — for cacheable methods (historical block data, immutable receipts, etc.), the cache layer (ecosystem/cache) checks for a recent response. Hits return immediately.
  3. 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.
  4. 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.
  5. 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.

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.

Full list (click to expand)
ChainIndexInterfaces
Agoric MainnetAGRgRPC, REST, Tendermint RPC
Agoric TestnetAGRTgRPC, REST, Tendermint RPC
Aptos MainnetAPT1REST
Arbitrum MainnetARBITRUMJSON-RPC
Arbitrum Nova TestnetARBITRUMNJSON-RPC
Arbitrum Sepolia TestnetARBITRUMSJSON-RPC
Avalanche C Chain MainnetAVALANCHECJSON-RPC
Avalanche C Chain TestnetAVALANCHECTJSON-RPC
Avalanche MainnetAVAXJSON-RPC
Avalanche P Chain MainnetAVALANCHEPJSON-RPC
Avalanche P Chain TestnetAVALANCHEPTJSON-RPC
Avalanche TestnetAVAXTJSON-RPC
Axelar MainnetAXELARgRPC, REST, Tendermint RPC
Axelar TestnetAXELARTgRPC, REST, Tendermint RPC
Base MainnetBASEJSON-RPC
Base Sepolia TestnetBASESJSON-RPC
Berachain Artio MainnetBERAJSON-RPC
Berachain Artio TestnetBERATJSON-RPC
Berachain Bartio TestnetBERAT2JSON-RPC
BitcoinBTCJSON-RPC
Bitcoin Cash MainnetBCHJSON-RPC
Bitcoin Cash TestnetBCHTJSON-RPC
Bitcoin TestnetBTCTJSON-RPC
Blast MainnetBLASTJSON-RPC
Blast Sepolia TestnetBLASTSPJSON-RPC
BSC MainnetBSCJSON-RPC
BSC TestnetBSCTJSON-RPC
Canto MainnetCANTOgRPC, JSON-RPC, REST, Tendermint RPC
Cardano MainnetCARDANOREST
Cardano Preprod TestnetCARDANOTREST
Casper MainnetCASPERJSON-RPC
Casper TestnetCASPERTJSON-RPC
Celestia Arabica TestnetCELESTIATAgRPC, JSON-RPC, REST, Tendermint RPC
Celestia MainnetCELESTIAgRPC, JSON-RPC, REST, Tendermint RPC
Celestia Mocha TestnetCELESTIATMgRPC, JSON-RPC, REST, Tendermint RPC
Celo Alfajores TestnetALFAJORESJSON-RPC
Celo MainnetCELOJSON-RPC
Cosmos Hub MainnetCOSMOSHUBgRPC, REST, Tendermint RPC
Cosmos Hub TestnetCOSMOSHUBTgRPC, REST, Tendermint RPC
Dogecoin MainnetDOGEJSON-RPC
Dogecoin TestnetDOGETJSON-RPC
Elys TestnetELYSgRPC, REST, Tendermint RPC
Ethereum Beacon MainnetETHBEACONREST
Ethereum MainnetETH1JSON-RPC
Ethereum Testnet HoleskyHOL1JSON-RPC
Ethereum Testnet SepoliaSEP1JSON-RPC
Evmos MainnetEVMOSgRPC, JSON-RPC, REST, Tendermint RPC
Evmos TestnetEVMOSTgRPC, JSON-RPC, REST, Tendermint RPC
Fantom MainnetFTM250JSON-RPC
Fantom TestnetFTM4002JSON-RPC
Filecoin MainnetFVMJSON-RPC
Filecoin TestnetFVMTJSON-RPC
Fuel Network GraphqlFUELNETWORKREST
Fuse MainnetFUSEJSON-RPC
Fuse TestnetSPARKJSON-RPC
Hedera MainnetHEDERAJSON-RPC
Hedera TestnetHEDERATJSON-RPC
Hyperliquid MainnetHYPERLIQUIDJSON-RPC
Hyperliquid TestnetHYPERLIQUIDTJSON-RPC
Injective MainnetINJECTIVEgRPC, REST, Tendermint RPC
Injective TestnetINJECTIVETgRPC, REST, Tendermint RPC
IOTA MainnetIOTAJSON-RPC
IOTA TestnetIOTATJSON-RPC
Juno MainnetJUN1gRPC, REST, Tendermint RPC
Juno TestnetJUNT1gRPC, REST, Tendermint RPC
Kakarot Sepolia TestnetKAKAROTTJSON-RPC
Lava MainnetLAVAgRPC, REST, Tendermint RPC
Lava TestnetLAV1gRPC, REST, Tendermint RPC
Litecoin MainnetLTCJSON-RPC
Litecoin TestnetLTCTJSON-RPC
Manta Pacific MainnetMANTAPACIFICJSON-RPC
Manta Pacific TestnetMANTAPACIFICTJSON-RPC
Mantle TestnetMANTLEJSON-RPC
Monad MainnetMONADJSON-RPC
Monad TestnetMONADTJSON-RPC
Moralis Advanced APIMORALISREST
Movement MainnetMOVEMENTREST
Movement Testnet BardockMOVEMENTTREST
Namada SE TestnetNAMTSETendermint RPC
NEAR MainnetNEARJSON-RPC
NEAR TestnetNEARTJSON-RPC
Optimism MainnetOPTMJSON-RPC
Optimism Sepolia TestnetOPTMSJSON-RPC
Osmosis MainnetOSMOSISgRPC, REST, Tendermint RPC
Osmosis TestnetOSMOSISTgRPC, REST, Tendermint RPC
Polkadot Asset Hub MainnetPOLKADOTASSETHUBJSON-RPC
Polygon Amoy TestnetPOLYGONAJSON-RPC
Polygon MainnetPOLYGONJSON-RPC
Ripple MainnetXRPJSON-RPC
Ripple TestnetXRPTJSON-RPC
Scroll MainnetSCROLLJSON-RPC
Scroll Sepolia TestnetSCROLLSJSON-RPC
Secret MainnetSECRETgRPC, REST, Tendermint RPC
Secret TestnetSECRETPgRPC, REST, Tendermint RPC
Side TestnetSIDETgRPC, REST, Tendermint RPC
Solana MainnetSOLANAJSON-RPC
Sonic Blaze TestnetSONICTJSON-RPC
Sonic MainnetSONICJSON-RPC
Stargaze MainnetSTRGZgRPC, REST, Tendermint RPC
Stargaze TestnetSTRGZTgRPC, REST, Tendermint RPC
Starknet MainnetSTRKJSON-RPC
Starknet Sepolia TestnetSTRKSJSON-RPC
Stellar MainnetXLMJSON-RPC, REST
Stellar TestnetXLMTJSON-RPC, REST
Stride MainnetSTRIDEgRPC, REST, Tendermint RPC
Stride TestnetSTRIDETgRPC, REST, Tendermint RPC
Subsquid-Powered SubgraphSQDSUBGRAPHREST
Sui DevnetSUITJSON-RPC
Tezos MainnetTEZOSREST
Tezos Shadownet TestnetTEZOSTREST
TON MainnetTONREST
TON TestnetTONTREST
Tron MainnetTRXREST
Tron Shasta TestnetTRXTREST
Union TestnetUNIONTgRPC, REST, Tendermint RPC
Westend Asset Hub TestnetPOLKADOTASSETHUBTJSON-RPC
Worldchain MainnetWORLDCHAINJSON-RPC
Worldchain Sepolia TestnetWORLDCHAINSJSON-RPC
zkSync Era MainnetZKSYNCJSON-RPC
zkSync Era Sepolia TestnetZKSYNCSPJSON-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, retry bans, 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 ReleaseRun 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

TagSourceStability
:vX.Y.Zrelease tagimmutable, byte-identical to the standalone binary at that version
:latestrelease tagfloating — points at the most recent final release (not RC/beta)
:mainpush to main branchfloating — most recent dev build from main, not a release artifact
:<branch>-<version>push to other branchesper-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 sales@magmadevs.com.

Enterprise — production support, SLAs, and custom features beyond the open edition are available under that license. Talk to us.

Community