/config/projects/networks.llms.txt
Networks
A network is one chain inside a project. Declare it once and eRPC owns the whole pipeline: smarter caching, automatic failover, and in-flight deduplication — on every chain you touch. Three things make it ergonomic: zero-config chains bootstrap automatically from your providers on first request, networkDefaults lets one block set shared policy for every chain, and a human-readable alias replaces evm/42161 in every URL.
What you get:
- Any EVM chain or SVM (Solana) cluster, any provider — routes automatically on first request.
- One
networkDefaultsblock replaces repetitive per-chain config. alias: arbitrumturns every URL into/main/arbitruminstead of/main/evm/42161.- Static canned replies for chains with non-standard genesis blocks.
- In-flight deduplication so 100 identical concurrent requests cost you one upstream call.
Quick taste
Illustrative, not a tuned production config — declare Ethereum with a human-readable alias:
projects: - id: main networks: - architecture: evm evm: chainId: 1 # human-readable alias replaces evm/1 in every URL and metric label alias: ethereumAgent reference
Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly:
Prompt Example #1: add multiple chains with shared policy
I'm running eRPC for several EVM chains and want to declare Ethereum mainnet, Arbitrum, and Base in a single project. Set human-readable aliases (e.g. "ethereum", "arbitrum", "base"), share a common rate-limit budget and failsafe via networkDefaults, and keep per-chain blocks minimal. Work with my existing eRPC config. Read the full reference first: https://docs.erpc.cloud/config/projects/networks.llms.txt
Prompt Example #2: tune getLogs limits and splitting
My eRPC instance is hitting "block range too large" errors from upstreams on eth_getLogs. Adjust getLogsMaxAllowedRange for my Ethereum mainnet network, enable automatic range splitting on errors, and set a safe concurrency cap so sub-requests don't flood the provider. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/projects/networks.llms.txt
Prompt Example #3: fix backward tip jumps in multi-pod deployment
My clients occasionally see older block numbers between requests, which I think means different eRPC pods are returning different "latest" tips. Enable cluster-min served tip for the "latest" axis on my production chains so all pods agree on the served tip. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/projects/networks.llms.txt
Prompt Example #4: disable validation for a non-standard chain
I'm adding zkSync Era to eRPC and eth_getBlockByNumber responses from zkSync use a non-standard transactionsRoot — the data-integrity module is rejecting them. For the zkSync network only, make sure data-integrity is off (or disable the transactionsRoot checks via the integrity block) without touching the global policy. Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/projects/networks.llms.txt
Prompt Example #5: debug 503 on a newly added chain
I added a new chain to eRPC's networks list but requests are returning HTTP 503 with ErrNetworkInitializing. Walk me through what the bootstrap task does, what conditions produce 503 vs 404, and which config keys to check first (providers, onlyNetworks/ignoreNetworks, rateLimitBudget). Work with my existing eRPC config. Reference: https://docs.erpc.cloud/config/projects/networks.llms.txt
Networks — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
How it works
Architecture concept. NetworkArchitecture is a string enum with two valid values: "evm" and "svm" (common/network.go:L14-17). The canonical id is evm:<chainId> from util.EvmNetworkId, or svm:<cluster> / svm:<chain>:<cluster> from util.SvmNetworkId (util/ids.go:L1-30). The network Prometheus label equals the alias when set, otherwise the raw id.
architecture is inferred when omitted: a network carrying an svm block derives architecture: svm, one carrying evm (or neither) derives evm (common/defaults.go:L2010-2028). Declaring architecture: svm auto-creates an empty svm block, and networkDefaults.evm is never injected into an SVM network. See SvmNetworkConfig below and the SVM reference pages.
Lazy creation. NetworksRegistry.GetNetwork fast-paths a sync.Map lookup; on miss it validates the id format and schedules a bootstrap task. The task: (1) resolves or synthesizes a NetworkConfig and runs SetDefaults so it inherits networkDefaults exactly like static networks; (2) calls PrepareUpstreamsForNetwork — fans out to every configured provider, polls 200 ms for up to 30 s until ≥1 upstream is ready; (3) wires the cache DAL; (4) registers with the selection-policy engine. Statically-declared networks bootstrap in background at startup through the same machinery; a failed eager bootstrap is retried when the first request arrives.
Forward pipeline order (exact sequence, erpc/networks.go:L931-1603):
- Apply
directiveDefaults+ start OTel span. - Static response match — returns immediately if hit (before multiplexer, cache, upstreams).
- Multiplexer — leader/follower on
CacheHash; followers copy the leader's response. - Cache read — non-null hit closes the mux and returns.
- Policy-engine upstream ordering — falls back to registration order if empty;
ErrNoUpstreamsFound(404) if still empty. - EVM pre-forward hook (future-block short-circuit; never cached).
- Stateful-method guard, network rate-limit, request normalization.
- Failsafe executor selection — first config-order entry matching method + finality.
- Upstream sweep — per-upstream block-availability gate, hedge/retry orchestration.
- Post-sweep: timeout translation, last-valid-response fallback, async cache set, misbehavior accounting.
networkDefaults inheritance (common/defaults.go:L1781-1973). "Network wins; defaults fill gaps." Scalars inherit when zero-valued; pointers when nil. selectionPolicy and directiveDefaults are whole-struct copies (no per-field merge). evm is a whole-struct copy when absent from the network, otherwise per-field fill for 14 named fields. Failsafe: if the network has entries, each is matched against the first compatible default (wildcard method + finality) and missing sub-fields are inherited; if the network has no entries the defaults list is deep-copied wholesale.
Finality classification (erpc/networks.go:L1645-1743). Explicit finalized/realtime flags in methods.definitions win. Otherwise the block ref/number is extracted from the request, then from the response body (for hash-keyed cache hits). Non-numeric tags → realtime; numeric blocks are checked via EvmIsBlockFinalized on the serving upstream, then the last upstream tried, then the network-wide lowest-finalized heuristic.
Served tip. Default ("max mode"): EvmHighestLatestBlockNumber / EvmHighestFinalizedBlockNumber return the MAX across policy-eligible, non-syncing upstreams. Setting evm.servedTip.enabledFor switches that axis to a cluster-min, cross-pod monotonic counter. Rollback tolerance is 1024 blocks (DefaultToleratedBlockHeadRollback, architecture/evm/evm_state_poller.go:L27 (opens in a new tab)). A use-upstream selector produces a per-group scoped tip keyed by the SHA-256 (first 8 bytes, hex-encoded) of the sorted matched upstream-id set; unmatched selectors compute a stateless min and emit no gauges. Group partitions are capped at 16 (maxServedTipPartitions); a "simple" group selector must be ≤128 chars, no whitespace, at most one leading !, and the matched set must be ≥2 upstreams and less than all upstreams. Syncing upstreams are excluded from every head computation (max mode, cluster mode, lowest-finalized, and guaranteed-method floors). EvmLeaderUpstream returns the upstream with the highest raw LatestBlock(); EvmLowestFinalizedBlockNumber returns the min positive effective finalized across non-syncing upstreams.
Config schema
projects[].networks[] — NetworkConfig
| Field | Type | Default | Behavior / footguns |
|---|---|---|---|
architecture | string (evm | svm) | Inferred from the sub-block: svm present → "svm", otherwise "evm" (common/defaults.go:L2010-2028) | Both evm and svm are valid (common/network.go:L82-88). Required by validation. |
evm | EvmNetworkConfig | Auto-created empty struct for EVM networks (common/defaults.go:L1914-1916); never injected into an SVM network | See EvmNetworkConfig table below. |
svm | SvmNetworkConfig | Auto-created empty struct when architecture: svm (common/defaults.go:L2026-2028) | Required for SVM networks — network.*.svm is required for svm networks otherwise. See SvmNetworkConfig table below. |
alias | string | "" | Charset [a-zA-Z0-9_-]+ (common/validation.go:L1264-1269); unique per project (common/validation.go:L622-627). Used as the network metric label via Network.Label() (erpc/networks.go:L278-286). Aliases work in URL path only — not in body networkId. |
rateLimitBudget | string | "" (none); inherits networkDefaults.rateLimitBudget when empty (common/defaults.go:L1783-1785) | Must reference an existing budget. Enforced per-request before upstream contact → ErrNetworkRateLimitRuleExceeded. |
failsafe[] | []FailsafeConfig | nil; inherited/merged from networkDefaults.failsafe | One networkExecutor per entry + a no-op catch-all (erpc/networks_registry.go:L115-138). Selection: first config-order match on matchMethod wildcard + matchFinality (erpc/networks.go:L907-929). Old single-object YAML auto-converted to a one-element list with matchMethod: "*". |
selectionPolicy | SelectionPolicyConfig | nil; inherited wholesale from networkDefaults.selectionPolicy when nil (common/defaults.go:L1833-1836) | Auto-attached when any upstream has tag tier:fallback (common/defaults.go:L1932-1940). See Selection & scoring. |
directiveDefaults | DirectiveDefaultsConfig | Always materialized (common/defaults.go:L1947-1950); inherited wholesale from networkDefaults.directiveDefaults when nil (common/defaults.go:L1837-1840) | Footgun: a network that sets ANY directiveDefaults field ignores networkDefaults.directiveDefaults entirely — no per-field merge. Applied at request start (erpc/networks.go:L937). |
multiplexing | *bool | nil = enabled (common/config.go:L2034-2039); inherits networkDefaults.multiplexing when nil (common/defaults.go:L1841-1844) | Gates in-flight identical-request dedup. Footgun: legacy single-object networkDefaults.failsafe YAML drops this field silently (old struct has no Multiplexing field, common/config.go:L626-655). |
staticResponses[] | []StaticResponseConfig | nil | Checked before multiplexer, cache, upstreams (erpc/networks.go:L976-981). See Static responses. |
staticResponses[].method | string | required | Exact JSON-RPC method name; case-sensitive string equality (common/validation.go:L1282-1284). |
staticResponses[].params | []any | nil | nil and [] are interchangeable in matching (both have len==0). YAML params: [] deserializes to non-nil empty slice; omitted params: deserializes to nil — both match requests with zero params. Hex strings ("0x0" vs "0x00") are NOT normalized — exact string match only. Declaration order matters: first match wins. |
staticResponses[].response.result | any | — | Exactly one of result or error must be set; both or neither → startup error. |
staticResponses[].response.error.code | int | 0 | No minimum/maximum enforced. Footgun: code 0 passes validation but is omitted from the wire JSON (json:"code,omitempty" on int, common/errors.go:L2226). Always set a non-zero code in production. |
staticResponses[].response.error.message | string | required when error set | Non-empty string required; Validate() returns "response.error.message is required" otherwise. |
staticResponses[].response.error.data | any | nil | Optional arbitrary extra data; omitted from JSON when nil. |
methods.preserveDefaultMethods | bool | false | Critical footgun: false + any definitions entries → ALL built-in methods (hundreds, including eth_call, eth_getLogs) are silently replaced by only your custom entries plus stateful markers. false + no definitions → all built-ins kept (same as omitting the block). true + any definitions → built-ins copied first, then user entries merged on top (common/defaults.go:L493-576). |
methods.definitions | map[string]CacheMethodConfig | Full built-in table (common/defaults.go:L493-528) | Per-method fields: finalized, realtime, stateful, reqRefs, respRefs, translateLatestTag, translateFinalizedTag, enforceBlockAvailability. |
methods.definitions.<m>.finalized | bool | false (true for static-cache methods) | Footgun: setting true on a non-finalized method (e.g. eth_getBalance) permanently caches ALL responses from that method with long/permanent TTL — a severe data-correctness bug (erpc/networks.go:L1656-1660). |
methods.definitions.<m>.realtime | bool | false (true for eth_blockNumber and similar) | Forces GetFinality → Realtime; responses use the realtime cache policy (short TTL). |
methods.definitions.<m>.stateful | bool | true for DefaultStatefulMethodNames, else false | Footgun: stateful: false on any of the 6 built-in stateful methods (eth_newFilter, eth_newBlockFilter, eth_newPendingTransactionFilter, eth_getFilterChanges, eth_getFilterLogs, eth_uninstallFilter) is silently overridden to true with no warning (common/defaults.go:L512-519). |
methods.definitions.<m>.translateLatestTag | *bool | nil = enabled | nil/true → "latest" tag replaced with current block hex before forwarding and cache keying. false → tag passed through unchanged. eth_getBlockByNumber ships with false so it can discover new blocks. Footgun: false on a changing method means all callers share one "latest" cache key; stale data returned until TTL expires (common/config.go:L307-309). |
methods.definitions.<m>.translateFinalizedTag | *bool | nil = enabled | Same as translateLatestTag for the "finalized" tag (common/config.go:L310-312). |
methods.definitions.<m>.enforceBlockAvailability | *bool | nil (inherit) | Per-method override; highest priority in the 5-step enforcement chain (erpc/networks.go:L1806-1813). |
projects[].networks[].evm — EvmNetworkConfig
| Field | Type | Default | Notes |
|---|---|---|---|
chainId | int64 | — (no default) | Drives networkId = "evm:<chainId>". Not validated > 0 for networks; chainId: 0 yields evm:0. |
fallbackFinalityDepth | int64 | 1024 (common/defaults.go:L1975) | Used when an upstream doesn't expose the finalized tag. Must be > 0 after defaults. |
fallbackStatePollerDebounce | Duration | 5s (common/defaults.go:L1976) | Static debounce for block polling until dynamic block time is learned. Must be > 0. |
dynamicBlockTimeDebounceMultiplier | *float64 | 0.7 (common/defaults.go:L1977) | Polling debounce = EMA block time × multiplier. Lower → more aggressive polling (fresher data, more upstream load). |
blockUnavailableDelayMultiplier | *float64 | 1.0 (common/defaults.go:L1978) | Retry delay for block-unavailable = EMA block time × multiplier. |
enforceBlockAvailability | *bool | nil (defer; fallback = enabled) | Network-level priority 2 in 5-step chain. nil defers to upstream bounds and system defaults. false disables enforcement even when an upstream has explicit bounds configured. |
maxRetryableBlockDistance | *int64 | nil → 128 at use site (erpc/networks.go:L1973-1979) | Block-unavailable errors within this distance of upstream head are retryable; beyond → non-retryable skip. |
getLogsMaxAllowedRange | int64 | 30_000 (common/defaults.go:L2092-2094) | Hard limit on eth_getLogs block range. Must be > 0; 0 inherits defaults value. |
getLogsMaxAllowedAddresses | int64 | 0 = unlimited | Enforced only when > 0 (architecture/evm/eth_getLogs.go:L212). |
getLogsMaxAllowedTopics | int64 | 0 = unlimited | Enforced only when > 0. |
getLogsSplitOnError | *bool | true (common/defaults.go:L2095-2097) | Retry by bisecting range on "too many results" errors. |
getLogsSplitConcurrency | int | 10 (common/defaults.go:L2098-2100) | Parallelism cap for split sub-requests. |
traceFilterSplitOnError | *bool | nil = off (deliberate opt-in, common/defaults.go:L2102-2104) | Opt-in bisecting for trace_filter/arbtrace_filter. |
traceFilterSplitConcurrency | int | 10 (common/defaults.go:L2105-2107) | Parallelism cap for trace-filter splits. |
idempotentTransactionBroadcast | *bool | nil = enabled (architecture/evm/eth_sendRawTransaction.go:L27-36) | "Already known"/"nonce too low"-verified errors become success-with-tx-hash, making retry/hedge safe for eth_sendRawTransaction. |
markEmptyAsErrorMethods | []string | eth_blockNumber, eth_getBlockByNumber, eth_getTransactionByHash, and 8 others (common/defaults.go:L2044-2057) | Methods where empty response = error (retried, upstream scored down). eth_getTransactionReceipt deliberately excluded. |
emptyResultConfidence | blockHead | finalizedBlock | blockHead (common/defaults.go:L2075-2079) | How confirmed a block must be for empty point-lookups to be retried as missing data. |
safeBlockSource | string (upstream selector) | "" = inherit or provider-defined routing | Routes standard block-tagged JSON-RPC requests carrying safe to matching upstreams and skips cache reads. The tag stays verbatim so those upstreams define both height and hash. Operator routing overrides client use-upstream; retry/consensus still applies inside the matching pool. An empty network value inherits networkDefaults.evm.safeBlockSource when present. Does not affect eth_query* or direct gRPC Query APIs. |
evm.integrity.enforceHighestBlock | *bool | true (common/defaults.go:L2117-2120) | Deprecated — migrated into directiveDefaults.enforceHighestBlock at SetDefaults time when the directive is unset (common/defaults.go:L1952-1966). Prefer directiveDefaults.enforceHighestBlock. |
evm.integrity.enforceGetLogsBlockRange | *bool | true (common/defaults.go:L2121-2123) | Deprecated — same migration path as enforceHighestBlock. |
evm.integrity.enforceNonNullTaggedBlocks | *bool | true (common/defaults.go:L2124-2126) | Deprecated — same migration path. |
maxFutureBlockRetryDistance | *int64 | nil | Deprecated — warned about and set to nil in SetDefaults (common/defaults.go:L2080-2083). Has no effect; remove from configs. |
servedTip | EvmServedTipConfig | nil = max mode | Copied wholesale from networkDefaults.evm.servedTip when nil. |
servedTip.enabledFor | []string | [] (max mode) | Valid: latest, finalized, safe. Listing a tag switches that axis from max-across-upstreams to cluster-min + monotonic clamp via shared state. |
servedTip.clusterDelta | int64 | 0 = auto-derive from EMA block time, clamped [2, 10] | Must be ≥ 0. |
servedTip.guaranteedMethods | []string (glob) | [] | Global tip clamped to per-method supporting-set floor (erpc/networks.go:L643-649). |
projects[].networks[].svm — SvmNetworkConfig
| Field | Type | Default | Behavior / footguns |
|---|---|---|---|
cluster | string | "" (required) | The cluster these upstreams serve — mainnet-beta, devnet, testnet, or a fork's own cluster name. Together with chain it forms the network id. This is network identity (the SVM analogue of evm.chainId) and is therefore not inherited from networkDefaults.svm. |
chain | string | "" → treated as "solana" | Which SVM chain this network runs on. Set it explicitly for forks and variants (fogo, eclipse) so several SVM chains can be hosted side by side without network-id or cache-key collisions. Inherited from networkDefaults.svm.chain. |
commitment | string | "" — no default | One of finalized, confirmed, processed. When unset nothing is injected and each upstream's own server-side default governs (Solana's is finalized), so upstreams can disagree on identical requests. Setting it pins one level across the pool so the cache and consensus compare like-for-like; note that it also makes finality classification track the configured level. Details: Commitment & finality. |
statePollerDebounce | Duration | 400ms (one slot) (common/defaults.go:L2237-2243) | Minimum interval between poll fan-outs of an upstream's slot/health view. A gate, not the ticker period — the ticker stays pinned at 400 ms, so values below it have no effect. Raising it widens the getBlock availability guard's staleness margin proportionally. |
maxFinalizedSlotLag | *int64 | 100 slots ≈ 40 s (common/defaults.go:L2244-2251) | Bounds how far an upstream's finalized slot may trail the pool reference before it is excluded from consensus voting on finalized data. Applied only when a consensus policy is active AND the request's resolved finality is Finalized. Tri-state: omitted → 100; explicit 0 → filter disabled; >0 → that value. The field is a pointer precisely so an explicit 0 is distinguishable from "unset" — a plain integer would collapse the two and make the documented disable switch unreachable. |
enforceBlockAvailability | *bool | nil → true (materialized at read time by SvmEnforceBlockAvailability(), erpc/networks.go:L704-708) | Gates the getBlock/getConfirmedBlock guard that short-circuits requests for slots above the pool's highest indexed slot, saving quota and firing the indexing-lag retry immediately. Set false when shred-insert tracking is unavailable or unreliable on a deployment. An explicit false in networkDefaults.svm is honored. |
Not present on SvmNetworkConfig: there is no maxSlotsPerSignaturesQuery. An early design proposed one as an analogue of EVM's getLogs range cap, but getSignaturesForAddress is bounded by signature cursors (before/until), not by a slot range, so the key described semantics Solana does not have and was removed. minContextSlot is a node-freshness floor — the minimum bank slot at which a request may be evaluated — not a lower bound on returned history, so it cannot serve as a range cap either.
Cluster and genesis-hash validation. Known (chain, cluster) pairs — currently Solana mainnet-beta, devnet, and testnet — have their genesis hash validated at bootstrap by a single getGenesisHash call compared against a hardcoded table. Both a mismatch and a fetch failure fail the upstream, so a node mis-pointed at the wrong cluster never joins the pool. For unknown clusters (forks, private clusters) the same check runs only when the upstream sets svm.checkGenesisHash: true. getGenesisHash itself is then answered from the table with no upstream round-trip, mirroring EVM's eth_chainId short-circuit.
projects[].networks[].directiveDefaults — DirectiveDefaultsConfig
| Field | Type | Default |
|---|---|---|
retryEmpty | *bool | nil |
retryPending | *bool | nil |
skipCacheRead | bool|string | nil |
useUpstream | *string | nil |
skipInterpolation | *bool | nil |
skipConsensus | *bool | nil |
enforceHighestBlock | *bool | true (common/defaults.go:L1458-1460) |
enforceGetLogsBlockRange | *bool | true (common/defaults.go:L1461-1463) |
enforceNonNullTaggedBlocks | *bool | true (common/defaults.go:L1464-1466) |
Data-integrity validation is configured under
networks[].integrity(notdirectiveDefaults) — an opt-inlevel/checks/profilesblock. The deprecated per-check flags (validateLogsBloomMatch,enforceLogIndexStrictIncrements,validateTransactionsRoot, …) are translated into it at startup. See Integrity checks.
projects[].networkDefaults — NetworkDefaults
| Field | Type | Default | Inheritance rule |
|---|---|---|---|
rateLimitBudget | string | "" | Copied when network's value is empty string (common/defaults.go:L1783-1785). Validated after inheritance per network — an invalid name surfaces as a per-network error (e.g. "network.*.rateLimitBudget 'x' does not exist"), not a top-level networkDefaults error. Networks that explicitly set their own non-empty rateLimitBudget are not affected. |
failsafe[] | []FailsafeConfig | nil | Network has none → deep-copied wholesale. Network has some → per-entry merge from the FIRST compatible default (wildcard method + finality match); break on first match (common/defaults.go:L1793-1832). |
selectionPolicy | SelectionPolicyConfig | nil | Shallow-copied when network's is nil (common/defaults.go:L1833-1836). |
directiveDefaults | DirectiveDefaultsConfig | nil | Shallow-copied when network's is nil — no per-field merge (common/defaults.go:L1837-1840). |
evm | EvmNetworkConfig | nil | Struct-copied wholesale when network has no evm block — except onto SVM networks, which never receive it. Otherwise per-field fill for: integrity, fallbackStatePollerDebounce, dynamicBlockTimeDebounceMultiplier, blockUnavailableDelayMultiplier, fallbackFinalityDepth, getLogsMaxAllowed*, getLogs*, traceFilter*, servedTip, emptyResultConfidence. NOT inherited individually: chainId, enforceBlockAvailability, maxRetryableBlockDistance, markEmptyAsErrorMethods, idempotentTransactionBroadcast (common/defaults.go:L1845-1889). |
svm | SvmNetworkConfig | nil | Per-field fill: chain, commitment, statePollerDebounce, maxFinalizedSlotLag, enforceBlockAvailability (common/defaults.go:L2197-2221). cluster is deliberately NOT inherited — it is network identity, and inheriting it would let two networks alias one another. The two pointer fields are tested for nil, not for their zero value, so an explicit maxFinalizedSlotLag: 0 or enforceBlockAvailability: false set here survives the merge. |
multiplexing | *bool | nil | Value-copied when network's multiplexing is nil (common/defaults.go:L1841-1844). No deep merge — a network must set its own multiplexing to deviate from the project-wide default. |
Note: networkDefaults has no alias, methods, staticResponses, or architecture fields — those are per-network only.
server.aliasing.rules[] — domain-based aliasing
| Field | Type | Default | Behavior |
|---|---|---|---|
rules[] | list | nil; auto-created {matchDomain: "*", serveProject: "main"} when config has zero projects (common/defaults.go:L100-110) | First rule whose matchDomain wildcard matches the request Host wins (erpc/http_server.go:L232-257). |
rules[].matchDomain | string (wildcard) | — | Matched against Host header (port stripped). |
rules[].serveProject | string | — | Pre-selects projectId. |
rules[].serveArchitecture | string | — | Pre-selects architecture. |
rules[].serveChain | string | — | Pre-selects chainId. Pre-selecting project+chain WITHOUT architecture is rejected at parse time. |
Worked examples
All patterns below are distilled from real production fleets; comments explain the non-obvious choices.
1. The production baseline: networkDefaults with finality-split failsafe policies. Block-availability races on realtime/unfinalized requests need a non-zero retry delay; finalized/unknown don't — this is why production splits them:
projects: - id: main networkDefaults: # All chains inherit these unless they set their own failsafe list directiveDefaults: retryEmpty: true retryPending: false # multiplexing: false in production to avoid follower-timeout amplification # on chains where every block is a write (e.g. high-throughput L2s) multiplexing: false evm: getLogsMaxAllowedRange: 30000 getLogsSplitOnError: true # cluster-min tip on "latest" prevents backward tip jumps across pods servedTip: enabledFor: [latest] failsafe: # NOTE: delay MUST NOT be 0 for realtime/unfinalized — a short non-zero # delay gives shared-state propagation time to catch up after a block # arrives on one upstream but not yet on another - matchMethod: "eth_call|eth_getLogs" matchFinality: [realtime, unfinalized] hedge: quantile: 0.95 maxCount: 1 minDelay: 500ms maxDelay: 10s retry: maxAttempts: 4 delay: 50ms - matchMethod: "eth_call|eth_getLogs" matchFinality: [finalized, unknown] hedge: quantile: 0.95 maxCount: 1 minDelay: 500ms maxDelay: 10s retry: maxAttempts: 4 delay: 0 - matchMethod: "*" retry: maxAttempts: 6 delay: 50ms networks: - architecture: evm evm: { chainId: 1 } alias: ethereum - architecture: evm evm: { chainId: 42161 } alias: arbitrum2. Per-chain bespoke policy appended before the shared catch-all. Ethereum mainnet adds consensus on eth_getLogs and state-read integrity checks; Arbitrum bumps the getLogs hedge floor for its faster blocks. Both spread sharedNetworkFailsafe at the end so the catch-all policy is never per-chain:
networks: - evm: { chainId: 1 } alias: ethereum failsafe: # Ethereum-specific: 2-of-3 consensus on getLogs for indexing integrity - matchMethod: "eth_getLogs|eth_getBlockReceipts" matchFinality: [unfinalized, unknown] timeout: { duration: 15s } hedge: quantile: 0.95 maxCount: 1 minDelay: 500ms maxDelay: 10s retry: maxAttempts: 6 delay: 0 emptyResultDelay: 1000ms # consensus config would follow here # ... sharedNetworkFailsafe spread after (catch-all always last)
- evm: { chainId: 42161 } alias: arbitrum-one failsafe: # Arbitrum-specific: getLogs minDelay raised to 1000ms — Arbitrum delivers # blocks at ~250ms and block-availability races persist slightly longer - matchMethod: "eth_getLogs|eth_getBlockReceipts" matchFinality: [unfinalized] hedge: quantile: 0.95 maxCount: 1 minDelay: 1000ms maxDelay: 10s retry: maxAttempts: 6 delay: 0 # ... sharedNetworkFailsafe spread after3. Static genesis-block response for a chain that starts at block 1. Serve a canned reply before the multiplexer and cache so clients don't hit a dead upstream on block 0. The staticResponses array is checked before everything else in the forward pipeline:
networks: - evm: { chainId: 1328 } alias: sei-testnet staticResponses: - method: eth_getBlockByNumber params: ["0x0", false] response: result: # Fill the fields your clients read; unrecognised fields are fine number: "0x0" hash: "0x0000000000000000000000000000000000000000000000000000000000000000" parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000" transactions: []4. Skip data-integrity validation for non-standard chains. ZkSync Era and other ZK-rollups use a non-standard transactionsRoot (and other encodings) the data-integrity module's recompute checks can't model. The module is opt-in, but if you enabled it project-wide, turn it off for that chain — a network integrity block overrides the project one:
networks: - evm: { chainId: 324 } alias: zksync-mainnet # Override the project-wide integrity level to off for this chain # (or disable just the transactionsRoot checks via integrity.checks). integrity: level: off5. Domain-based aliasing for fully path-free routing. Route an entire subdomain straight to a chain — no project or network segment in the URL path. The alias and domain rule work together so clients call a clean HTTPS endpoint:
server:
aliasing:
rules:
- matchDomain: "eth.rpc.example.com"
serveProject: main
serveArchitecture: evm
serveChain: "1"
# Wildcard: any unknown subdomain falls through to the main project
- matchDomain: "*.rpc.example.com"
serveProject: mainPOST https://eth.rpc.example.com/ → main/evm:16. Multiple SVM chains side by side. chain defaults to solana, so a plain Solana cluster keeps the short two-segment id. Naming a fork's chain switches the id to three segments, which is what keeps cache partitions and alias registration from colliding between svm:mainnet-beta and svm:fogo:mainnet. Note that cluster stays per-network — it is identity, so networkDefaults.svm carries only the shared policy:
projects: - id: main networkDefaults: svm: # shared policy only — cluster is network identity and must not be here commitment: confirmed statePollerDebounce: 500ms networks: # chain omitted → "solana" → networkId "svm:mainnet-beta" - architecture: svm svm: cluster: mainnet-beta # explicit chain → networkId "svm:fogo:mainnet" - architecture: svm svm: chain: fogo cluster: mainnetBoth id shapes are addressable the same three ways — URL path, request-body networkId, or an alias. The fork's colon is carried inside the third path segment:
POST /main/svm/mainnet-beta → svm:mainnet-beta
POST /main/svm/fogo:mainnet → svm:fogo:mainnet
POST /main → body {"networkId": "svm:fogo:mainnet", ...}Request/response behavior
- The
networklabel on every Prometheus metric equals the alias when set, otherwise the raw network id (evm:1,svm:mainnet-beta,svm:fogo:mainnet). This affects allerpc_network_*metrics.erpc/networks.go:L278-286(opens in a new tab) - Static-response hits short-circuit before the multiplexer;
static_response.hit=trueis set on theNetwork.ForwardOTel span. They never read or write the cache. - Multiplexer followers receive a copy of the leader's response; if the follower's context deadline fires before the leader completes, the error is
ErrNetworkRequestTimeout(HTTP 504 status code, JSON-RPC error code −32603). This is distinct fromErrFailsafeTimeoutExceeded(failsafe policy timeout) andErrEndpointRequestTimeout(individual upstream HTTP timeout). ErrNetworkInitializing(HTTP 503) is returned while the bootstrap task is still running. The client should retry. Failed (non-fatal) bootstrap tasks are auto-retried by the background loop with factor-1.5 backoff (3 s to 130 s, task timeout 120 s).ErrNetworkNotSupported(HTTP 404) is returned when all providers report the chain is not supported and zero upstreams were registered. It is never retried by the bootstrap loop.ErrNetworkNotFound(HTTP 404) is returned after a successful bootstrap task that stored nothing (defensive path). The caller must retry.ErrInvalidRequest(HTTP 400) is returned when the networkId in the URL is not a validevm:<int>format — the error message suggests using an alias orevm/42161form.erpc/networks_registry.go:L220-222(opens in a new tab)ErrNoUpstreamsFound(HTTP 404) fires when the policy engine returns an empty ordered list and the raw registration list is also empty — all upstreams have been filtered out.ErrUpstreamsExhaustedfires when every upstream in the ordered list was tried and all failed; it carries per-upstream errors.erpc/networks.go:L1392-1406(opens in a new tab)ErrNotImplemented(HTTP 501) is returned foreth_accountsandeth_sign(always); stateful methods with more than one candidate upstream also produce this error unless scoped by ause-upstreamdirective.ErrInvalidEvmChainId(HTTP 400) is returned when the chain id cannot be parsed.ErrNetworkRequestTimeoutwire behavior: JSON-RPC error code −32603; HTTP transport status is 200 (JSON-RPC over HTTP);ErrorStatusCode()returns 504 but this is only seen in non-JSON-RPC error paths. The error is retryable toward both network and upstream (no explicit non-retryable flag). Message format:"network-level request towards one or more upstreams timed out after <N>ms"where N is the elapsed time since the follower registered with the multiplexer.- An alias in the request body
networkIdfield is silently ignored — the body fallback splits the literal value on:with no alias lookup. Only URL path segments resolve aliases.
Best practices
- Always set
networkDefaults.failsafebefore adding per-chainfailsafeentries — once a network has any failsafe list, the defaults list is superseded entirely for that network. - Use
aliason every statically-declared chain you control. It makes URLs readable, shows up in every Prometheus label, and costs nothing. - Set
preserveDefaultMethods: truebefore adding any custommethods.definitionsentry. The defaultfalsesilently drops all built-in methods (includingeth_call,eth_getLogs) the moment you add one definition. - Never set
finalized: trueon a method that returns block-specific data (eth_getBalance,eth_getCode, etc.) — every response will be permanently cached at the first value ever returned. - Enable
servedTip.enabledFor: [latest, finalized]in production multi-pod deployments to prevent backward tip jumps across pods that could surface stale reads to clients. - Keep
getLogsMaxAllowedRangeat its default 30 000 unless your upstreams document a higher limit — range splits happen automatically on oversized requests butgetLogsSplitConcurrencycaps parallelism at 10 to avoid flooding providers. - For
eth_sendRawTransactionin high-retry configs, leaveidempotentTransactionBroadcastenabled (default). Disabling it turns idempotent "already known" errors back into real failures, making retries unsafe.
Edge cases & gotchas
- Aliases work in URLs only, not body
networkId— body fallback splits on:with no alias lookup; onlyparseUrlPathresolves aliases.erpc/http_server.go:L613-634(opens in a new tab) - Unknown alias segment falls through silently — an unresolvable single path segment is treated as architecture, yielding "architecture is not valid (must be 'evm')" instead of an alias-not-found error.
directiveDefaultsis all-or-nothing — a network-leveldirectiveDefaultsblock completely replacesnetworkDefaults.directiveDefaults; individual fields are not merged.common/defaults.go:L1837-1840(opens in a new tab)failsafereplaces, not merges — anynetworks[].failsafelist supersedesnetworkDefaults.failsafeentirely for that network.preserveDefaultMethods: false+ one customdefinitionsentry drops ALL built-ins — includingeth_call,eth_getLogs, etc. Only the custom entries plus the 6 stateful markers survive.common/defaults.go:L561-573(opens in a new tab)finalized: trueon a non-finalized method permanently caches all responses — every response is placed in thefinalizedcache bucket (long/permanent TTL). Setting this oneth_getBalancewould permanently cache every balance at the first value returned.erpc/networks.go:L1656-1660(opens in a new tab)networkDefaults.multiplexinghas no deep merge — a network must set its ownmultiplexingfield; there is no per-network override via defaults alone.- Legacy single-object
networkDefaults.failsafesilently dropsmultiplexing— the old-format fallback struct has noMultiplexingfield.common/config.go:L626-655(opens in a new tab) - Lazily-exposed networks never get a metrics alias resolver entry —
erpc/init.gobuilds the map only from staticcfg.Projects[].Networks[]; gRPC-cache metrics for lazy networks show the raw networkId.erpc/init.go:L62-77(opens in a new tab) evm.enforceBlockAvailability: falseoverrides upstream bounds — step 3 of the enforcement chain (upstream-has-bounds) is only reached when steps 1–2 yield no decision; an explicitfalseat the network level (step 2) short-circuits it.erpc/networks.go:L1814-1817(opens in a new tab)- Static response error code
0is omitted from the wire response —Codeis taggedjson:"code,omitempty"on int, so0disappears from the JSON error object. Always use a non-zero code in production.common/errors.go:L2226(opens in a new tab) - Failsafe merge picks the FIRST matching default — ordering of
networkDefaults.failsafeentries matters when multiple wildcard entries could match. stateful: falseon a built-in stateful method is silently overridden —MethodsConfig.SetDefaultsforce-setsStateful = truefor all 6 stateful methods in all three code paths, overwriting any user-suppliedfalsewithout any warning.common/defaults.go:L512-573(opens in a new tab)EnforceGetLogsBlockRangeandEnforceHighestBlockdirectives cannot be overridden per-request via HTTP headers foreth_getLogs,trace_filter,arbtrace_filter, andeth_blockNumber— these hooks read from the config baked in at startup, not live per-request directives. SetenforceGetLogsBlockRange: falseindirectiveDefaults, not as an HTTP header.architecture/evm/eth_getLogs.go:L282-285(opens in a new tab)ErrNetworkRequestTimeoutis produced only in the multiplexer follower path — a request that is itself the leader experiencesErrFailsafeTimeoutExceededor upstream sweep errors, neverErrNetworkRequestTimeout.erpc/networks.go:L2079-2084(opens in a new tab)evm.chainId: 0is not rejected —chainId: 0producesnetworkId = evm:0, which passesutil.IsValidNetworkIdbut is excluded by provideronlyNetworks/ignoreNetworkslists that use the stricterchainId > 0check.translateLatestTag: falseon a non-historical method collapses all callers to one cache key — all requests for that method share the literal"latest"key; stale data is returned until the cache TTL expires.preserveDefaultMethods: falsewith NOdefinitionsblock keeps ALL built-ins — the drop-all behavior only triggers whendefinitionsis non-empty ANDpreserveDefaultMethodsis false. A config withmethods: { preserveDefaultMethods: false }and nodefinitionskey is identical to omitting themethodsblock entirely.common/defaults.go:L494(opens in a new tab)- Lazy network alias registration happens after first bootstrap — statically-declared networks register aliases eagerly in the
NetworksRegistryconstructor; lazily-created networks only register their alias insideprepareNetwork, which runs during bootstrap. Until bootstrap completes, alias-based URLs for lazy networks return an alias-not-found error.erpc/networks_registry.go:L322-328(opens in a new tab) - Failed lazy bootstrap is user-retryable — non-fatal bootstrap tasks are retried by the background auto-retry loop and by the next request's
ExecuteTasks. OnlyresolveNetworkConfigformat errors areTaskFataland never retried.erpc/networks_registry.go:L263-266(opens in a new tab) - Provider
onlyNetworksbypasses vendor support checks entirely — a networkId listed inonlyNetworksreturnsSupportsNetwork = truewithout consulting the vendor's dynamic check;ignoreNetworksis evaluated first and short-circuits to false.thirdparty/provider.go:L34-49(opens in a new tab) - Synthesized lazy network configs become permanently visible —
ExposeNetworkConfigappends toproject.Config.Networks(admin API surface) and never overwrites an existing entry, so lazy networks persist in the admin response after first use.erpc/projects.go:L58-77(opens in a new tab) - Syncing upstreams are excluded from every head computation — max mode, cluster mode, lowest-finalized, and guaranteed-method floor calculations all skip upstreams in
EvmSyncingStateSyncing.erpc/networks.go:L327-330(opens in a new tab) - Future-block short-circuit compares against the MAX eligible head, not the cluster-min served tip — it never nulls a block that the most-ahead upstream reports having. The synthetic null response is never cached, and the short-circuit only fires for concrete block numbers (not tags or hashes).
erpc/networks.go:L570-610(opens in a new tab) - Wrong-empty misbehavior is suppressed for out-of-bounds blocks — if the block number in a missing-data response falls outside an upstream's configured
blockAvailabilitybounds, the empty response is expected and no misbehavior is recorded against that upstream.erpc/networks.go:L1566-1587(opens in a new tab) getLogsMaxAllowedRange: 0is rejected by validation — the field must be > 0 after defaults (common/validation.go:L1309-1311). UnlikegetLogsMaxAllowedAddressesandgetLogsMaxAllowedTopics(which default to 0 = unlimited),getLogsMaxAllowedRangedefaults to 30 000 and cannot be explicitly zeroed out; setting0in a network inheriting from defaults is treated as "unset" and inherits the parent value.common/defaults.go:L2092-2094(opens in a new tab)- Zero projects → implicit
mainproject + catch-all aliasing rule — when the config has no projects, eRPC auto-creates{matchDomain: "*", serveProject: "main"}, so/evm/123works without a project path segment.common/defaults.go:L100-110(opens in a new tab) - Selector-scoped tips never pollute network gauges — stateless scoped picks (unmatched or non-simple selectors) use a sentinel lane and emit no Prometheus gauge; equivalent selectors dedup into one partition keyed by matched-set hash; the cap of 16 partitions is enforced globally per network.
erpc/networks.go:L98-105(opens in a new tab) - Static response params matching has no wildcard — there is no glob or
*support; every params slot must match exactly. To catch a method regardless of params, add an entry with an emptyparamsarray. To match block 0 ("0x0"), be exact —"0x00"will not match.common/static_response.go:L14-99(opens in a new tab)
Observability
| Metric | Type | Labels | When it fires |
|---|---|---|---|
erpc_network_request_received_total | counter | project, network, category, finality, user, agent_name | On project forward entry |
erpc_network_successful_request_total | counter | project, network, vendor, upstream, category, attempt, finality, emptyish, user, agent_name | Successful response (vendor/upstream = <cache> for cache hits) |
erpc_network_failed_request_total | counter | project, network, category, attempt, error, severity, finality, user, agent_name | Failed response |
erpc_network_request_duration_seconds | histogram | project, network, vendor, upstream, category, finality, user | Request completed (vendor/upstream = <error> on failure) |
erpc_network_multiplexed_request_total | counter | project, network, category, finality, user, agent_name | Follower registered with the in-flight deduplicator |
erpc_network_static_response_served_total | counter | project, network, category | Static response matched and served |
erpc_network_timeout_fired_total | counter | project, network, category, finality, scope | Failsafe timeout fired; scope=network at network level |
erpc_network_served_tip_block_number | gauge | project, network, lane, axis | Served-tip value updated; absent in max mode |
erpc_network_served_tip_lag_blocks | gauge | project, network, lane, axis | MaxEligible - served, clamped ≥ 0; absent in max mode |
erpc_network_served_tip_upstream_excluded_total | counter | project, network, upstream, axis, reason | Upstream excluded from tip computation (velocity|outlier) |
erpc_network_hedged_request_total | counter | project, network, upstream, category, attempt, finality, user, agent_name | Hedge attempt dispatched for this upstream |
erpc_network_hedge_discards_total | counter | project, network, upstream, category, attempt, hedge, finality, user, agent_name | Hedge response discarded (a faster response already won) |
erpc_upstream_wrong_empty_response_total | counter | project, vendor, network, upstream, category, finality, user, agent_name | Upstream returned missing-data while another upstream served real data; suppressed when the block is outside the upstream's blockAvailability bounds |
erpc_unexpected_panic_total | counter | (standard labels) | Panic recovered in the async cache-set goroutine |
Notable logs (zerolog, at erpc/networks_registry.go and erpc/networks.go unless noted):
"registered network alias"/"skipping duplicate alias registration with different target"— alias map events."network initialization ended with zero upstreams"— bootstrap completed but no upstreams were registered."networks bootstrap completed"/"failed to bootstrap networks in background"— startup background bootstrap outcome."response served from cache"— cache hit in the forward pipeline."found identical request initiating multiplexer"— multiplexer follower registered."block availability check failed; failing open to allow request"— block-availability extraction or poller error; request proceeds."served static response (no upstream contacted)"(debug) — static response matched and served (erpc/networks_static_responses.go)."skipping static response: cannot inspect request"(debug) —JsonRpcRequest()failed; static check skipped."failed to build static response"(error) —NewJsonRpcResponsefailed on a matched static entry.- Provider task:
"registering N upstream(s) from provider"(upstream/registry.go).
Source code entry points
erpc/networks_registry.go:L83-376(opens in a new tab) —NetworksRegistry: lazyGetNetwork, bootstrap tasks,resolveNetworkConfigsynthesis, alias map register/resolve,NewNetworkconstructor.erpc/networks.go:L931-1603(opens in a new tab) —Network.Forward: full request pipeline, failsafe-executor matching, static responses, multiplexing, cache, upstream ordering, block-availability gating, finality classification.common/defaults.go:L1781-1973(opens in a new tab) —NetworkConfig.SetDefaults: networkDefaults inheritance, per-field fill rules, EvmNetworkConfig/MethodsConfig/DirectiveDefaults sub-passes.erpc/http_server.go:L810-1006(opens in a new tab) —parseUrlPath: URL/alias parsing decision table, domain-aliasing rules, body-networkId fallback.common/config.go:L1995-2256(opens in a new tab) —NetworkConfig,EvmNetworkConfigstruct definitions, legacy single-failsafe decode,NetworkId().upstream/registry.go:L155-293(opens in a new tab) —PrepareUpstreamsForNetwork: provider fan-out, ready-wait, 503/404 error states.erpc/networks_static_responses.go(opens in a new tab) —tryServeStaticResponse: canned-response serving, metric emit.common/static_response.go:L14-99(opens in a new tab) —FindStaticResponseMatch,paramsEqual,valueEqual: pure matching logic (YAML int vs JSON float64 tolerance, map key order independence).
Related pages
- SVM commitment & finality — how
svm.commitmentis injected and how SVM finality is classified. - SVM slot tracking & health — the state poller behind
statePollerDebounce, plusmaxFinalizedSlotLagandenforceBlockAvailabilityin depth. - SVM JSON-RPC cache — caching for SVM networks.
- Static responses — full matching semantics and edge cases for canned replies.
- Selection & scoring — upstream ordering within a network.
- Retry — per-network failsafe retry policy.
- Hedge — parallel backup requests on slow upstreams.
- Timeout — bounds the upstream sweep from outside.
- Rate limiters — budgets referenced by
rateLimitBudget. - Survive provider outages — the outcome networks + failsafe deliver together.