RPC proxy — Reference
The complete reference for the fusion endpoint: the request flow, the config model, route matching, caching, request forms, and the Blurt routing map. For a gentle introduction, start with the Quick guide.
The proxy is a generalized JSON-RPC reverse proxy (Saboin/temp-rpc-proxy) — Python / Sanic, config-driven, HTTP and WebSocket. It is not Blurt-specific; Blurt supplies a routing config that fuses blurtd, nexus-go, and the wallet indexer behind one endpoint.
Request flow
client ──► POST / (JSON-RPC 2.0)
├─ parse method ("api.method" or the legacy "call" form)
├─ match the first route whose regex matches the method
├─ cache hit? return cached response (Redis, per-route TTL)
└─ else forward to the route's target, cache, and returnA request is a JSON-RPC 2.0 object (jsonrpc, method, params, id) sent to POST /. WebSocket connections are served on the same endpoint with the same routing.
Configuration model
Routing is a single JSON file with four top-level keys:
{
"targets": { "<name>": "<backend URL>", ... }, // named backends
"timeouts": { "<name>": <seconds>, ... }, // per-target request timeout
"routes": { "<method regex>": { "target": "<name>", "cache": <sec>, "translate_to_app_base": <bool> }, ... },
"default-cache": <seconds> // fallback TTL (0 = no cache)
}targets— a name → backend-URL map. The URL scheme selects the transport (http(s)://,ws(s)://, orsock://).timeouts— per-target request timeout in seconds.routes— an ordered map of method regex → route config.default-cache— TTL applied when a route sets nocache.
Route config
| Key | Meaning |
|---|---|
target | which named target to forward to |
cache | response TTL in seconds for this route (omitted → default-cache) |
translate_to_app_base | rewrite the call to the node's legacy call form before forwarding (see Request forms) |
Route matching
The proxy takes the JSON-RPC method (e.g. bridge.get_ranked_posts) and matches it against the routes regexes in declaration order; the first match wins. Because matching is first-wins, specific routes must come before catch-alls. A typical namespace ends with a catch-all so nothing is unrouted:
"^condenser_api.get_content(.*)$": { "target": "nexus", "cache": 3 }, // specific → social
"^condenser_api.broadcast_(.*)$": { "target": "blurtd" }, // specific → node
"^condenser_api.(.*)$": { "target": "blurtd" } // catch-all → nodeRequest forms
The proxy accepts both JSON-RPC call styles and normalizes them:
// modern: dotted method
{ "jsonrpc": "2.0", "id": 1, "method": "condenser_api.get_content", "params": [...] }
// legacy: "call" with [api, method, params]
{ "jsonrpc": "2.0", "id": 1, "method": "call", "params": ["condenser_api", "get_content", [...]] }translate_to_app_base handles the reverse: for routes that set it, the proxy rewrites a dotted request into the legacy call form ("method": "call", "params": ["condenser_api", <method>, <params>]) that the node expects for certain namespaces.
Caching
Responses are cached in Redis (via aiocache) with a per-route TTL in seconds. default-cache (usually 0) applies when a route omits cache. Read methods that are hot and tolerate slight staleness (feeds, blogs, trending, witness lists) carry a small TTL; broadcast and consensus-critical methods carry no cache. Caching shields the backends from repeated identical reads without changing correctness beyond the TTL window.
Transports
The target URL scheme selects how the proxy reaches the backend:
| Scheme | Transport |
|---|---|
http:// / https:// | HTTP |
ws:// / wss:// | WebSocket |
sock:// | Unix socket |
Clients may connect over HTTP POST or WebSocket; routing is identical.
The Blurt routing map
Blurt's config fuses three backends — the node (blurtd), the social layer (nexus-go, as nexus), and the wallet indexer (wallet_indexer). The routing follows one principle: consensus and broadcast go to the node; social reads go to nexus-go; wallet history goes to the wallet indexer.
| Method pattern | Target | Why |
|---|---|---|
bridge.* | nexus-go | the social API (feeds, communities, profiles, …) |
wallet_history.* | wallet indexer | account history and reward screens |
tags_api.*, follow_api.*, hive.* | nexus-go | social/discovery reads |
condenser_api.get_content*, get_discussions_by*, get_blog*, get_followers/following/follow_count | nexus-go | the social subset of the classic API |
condenser_api.broadcast_*, network_broadcast_api.* | blurtd | writes to the chain |
condenser_api.get_account_history, account_history_api.* | blurtd | chain history (the wallet indexer accelerates the wallet's slice) |
condenser_api.* (catch-all), database_api.*, block_api.*, chain_api.*, witness/key APIs | blurtd | consensus and chain state |
This is exactly the split the wallet indexer and nexus-go pages describe from the other side: a front-end uses one RPC URL, and the proxy sends each method to the service built for it.
Adding a backend is a config change
Fronting a new service (as Blurt did for the wallet indexer) is just a new targets entry plus a route above the catch-alls — no client change, since the front-end keeps using the same single URL.
Configuration (runtime)
| Variable | Default | Purpose |
|---|---|---|
HOST | 127.0.0.1 | bind address (front with a web server for TLS) |
PORT | 5002 | listen port |
WORKERS | 4 | Sanic worker processes |
DEBUG | False | debug mode |
Redis must be reachable (default 127.0.0.1:6379) for the cache. The routing config is passed with --config. Deployment specifics (service unit, TLS termination, backend addresses) are operator-specific and live with the deployment; the repository ships a systemd unit and example configs.
