Networks
AIOpen as plain markdown for AIA network is a logical grouping of upstreams that serve one chain. eRPC discovers networks lazily — on first request to a network, every configured upstream that supports that chain is enrolled automatically. You only need to enumerate networks[] when you want to customize behavior (failsafe, selection policy, rate-limit budget, finality semantics, integrity checks, static responses, alias).
You can configure:
- Chain identity —
architecture: evm+evm.chainId; optionally a friendlyalias(e.g.ethereuminstead ofevm/1) - Finality semantics —
fallbackFinalityDepth, dynamic block-time multipliers,fallbackStatePollerDebounce - Failsafe —
timeout,retry,hedge,consensus, withmatchMethod/matchFinalityscoping - Selection policy — JS eval function to decide which upstreams handle which method
- Rate limits — bind a
rateLimitBudgetenforced before any upstream is contacted - Request directives defaults — turn on
retryEmpty, set a defaultuseUpstream, etc. - eth_getLogs and trace_filter — proactive splitting + split-on-error + hard limits
- eth_sendRawTransaction — idempotent broadcasting for safe retry/hedge
- Static responses — canned answers for
(method, params)pairs that no real upstream can serve - Method classification overrides — extend or replace the default cacheable-method table per network
Minimum useful config
Networks are lazy-loaded. The smallest explicit network is just architecture + evm.chainId plus whatever feature you want to override.
projects: - id: main networks: - architecture: evm evm: chainId: 1 alias: ethereum # friendly URL: /main/ethereum instead of /main/evm/1The single-object failsafe: form (one object instead of an array) is accepted as shorthand for an array with one entry. The array form with matchMethod: "*" is what you want as soon as you need per-method scoping.
Production config — network-level failsafe + selection + rate limit
A realistic mainnet entry with a network-wide failsafe (covers retries across upstreams when any one is rate-limited), a hedge to short-circuit slow tails, and a network-level rate-limit budget.
projects: - id: main networks: - architecture: evm evm: chainId: 1 rateLimitBudget: mainnet-network # enforced before any upstream is contacted failsafe: - matchMethod: "*" timeout: # Network timeout covers the full request lifecycle (every retry/hedge across upstreams) duration: 30s retry: maxAttempts: 3 delay: 0ms # 0 = no wait; immediately fail over to the next upstream hedge: # If the primary takes longer than 'delay', race a second copy on another upstream. delay: 200ms maxCount: 3 directiveDefaults: retryEmpty: true # retry empty responses unless the method is in retry.emptyResultAccept useUpstream: "alchemy-*|localnode-*" # default upstream filter; can be overridden per-requestLazy-load defaults
Networks not listed under networks[] still work — they're discovered on first request and inherit from networkDefaults. Use this to apply baseline failsafe and directives across every chain in the project.
projects: - id: main networkDefaults: # Applies to every network in this project — both statically listed and lazy-loaded. rateLimitBudget: my-default-budget failsafe: - matchMethod: "*" timeout: { duration: 30s } retry: { maxAttempts: 3, delay: 0ms } hedge: { delay: 200ms, maxCount: 3 } directiveDefaults: retryEmpty: true retryPending: false skipCacheRead: false # false | true | wildcard pattern (e.g. "memory*") networks: # ...network-specific overrides go here; deep-merged on top of networkDefaultsIf a network has its own failsafe: defined, none of networkDefaults.failsafe is merged in — defaults are wholesale replaced, not deep-merged for that field. Same for selectionPolicy. Other top-level fields (rateLimitBudget, directiveDefaults) are deep-merged.
multiplexing is also settable under networkDefaults and behaves like any other scalar field: a per-network multiplexing value wins; if absent, the default applies to every network in the project.
Name aliasing
Use a friendly alias instead of the architecture/chainId URL segment. Aliases are only available for statically-defined networks (lazy-loaded networks don't have one).
projects:
- id: main
networks:
- { architecture: evm, evm: { chainId: 1 }, alias: ethereum }
- { architecture: evm, evm: { chainId: 42161 }, alias: arbitrum }
- { architecture: evm, evm: { chainId: 137 }, alias: polygon }POST http://localhost:4000/main/ethereum
POST http://localhost:4000/main/arbitrum
POST http://localhost:4000/main/polygonAliases must contain only alphanumeric characters, dashes, and underscores.
Static responses
For chains that deviate from common client assumptions in ways no real upstream can serve. Example: a chain whose genesis block is at height 1 instead of 0 has no valid answer for eth_getBlockByNumber("0x0", false). staticResponses[] returns a canned response for matching (method, params) pairs — no upstream is contacted.
projects:
- id: main
networks:
- architecture: evm
evm:
chainId: 999
staticResponses:
# Synthetic genesis block
- method: eth_getBlockByNumber
params: ["0x0", false]
response:
result:
number: "0x0"
hash: "0x0000000000000000000000000000000000000000000000000000000000000000"
parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000"
# ...remaining block fields
# Or return a JSON-RPC error instead of a result
- method: some_unsupported_method
params: []
response:
error:
code: -32601
message: "Method not found"
data: # optional, attached as response.error.data
hint: "use eth_call instead"methodmust match exactly.paramsis matched by deep equality. Keys may be in any order; numbers compare by value (1matches1.0); hex strings compare literally ("0x0"≠"0x00").- First-match-wins, in declaration order.
- Exactly one of
response.result/response.errormust be set. - Matched requests skip cache, multiplexer, and upstream selection entirely. Hits are exported as
erpc_network_static_response_served_total. - Internal state pollers (latest/finalized block lookups) still go to upstreams — they're not intercepted.
Copy for your AI assistant — full networks referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
Every field on networks[]
| Field | Type | Notes |
|---|---|---|
architecture | string (required) | evm (only supported value today). |
evm | object | EVM-specific config — see "evm.* fields". |
alias | string | Friendly URL segment for this network (ethereum, arbitrum). Only applies to static networks. Allowed characters: alphanumeric, -, _. |
failsafe | array | Per-network failsafe policies. Wraps the full request lifecycle (including any upstream-level retries). Accepts matchMethod and matchFinality per entry. |
selectionPolicy | object | evalFunc (JS, run on evalInterval) returning the ordered list of upstreams to use. Order IS the routing decision — position 0 = primary, missing = excluded. See "selectionPolicy fields". |
directiveDefaults | object | Default request directives applied if the request doesn't override them via header/query. |
rateLimitBudget | string | Bind to a budget from rateLimiters.budgets[]. Enforced before any upstream is contacted. |
methods | object | Per-network override of cacheable-method classification — see "Per-network method overrides". |
multiplexing | bool | Per-network override for the in-flight request deduplication. Default true (inherits global). When false, identical concurrent requests each hit upstreams independently. |
staticResponses | array | Canned (method, params) → response mappings — see "Static responses" above. |
evm.* fields
| Field | Default | Notes |
|---|---|---|
chainId | required when architecture: evm | The chain's EIP-155 chain ID. |
fallbackFinalityDepth | auto-detected via eth_getBlockByNumber("finalized") | Used when an upstream doesn't expose the finalized tag. Finalized block = latest - fallbackFinalityDepth. Higher values are safer (more reorg-resistant) at the cost of cache hit rate. |
fallbackStatePollerDebounce | 5s | Static debounce used for block polling until enough blocks have been observed to compute a dynamic block time. |
dynamicBlockTimeDebounceMultiplier | 0.7 | Multiplier on the observed block time to derive the dynamic polling debounce. 0.7 means polling at 70% of the block time (more frequent than chain tick) so the latest-block pointer stays fresh. Lower = more aggressive polling (fresher data, more upstream load); higher = gentler (less load, slightly more latency on tip-following). Fast chains like Arbitrum or BNB benefit from lower values (e.g. 0.5); slow chains like Ethereum L1 can use higher (e.g. 0.9). |
blockUnavailableDelayMultiplier | 0.8 | Multiplier on the observed block time used as the retry delay when ALL upstreams returned "block not available." Falls back to the static retry.blockUnavailableDelay until block time is known. Lower = shorter wait between retries (faster recovery, more polling pressure); higher = longer wait (less pressure, more latency). Fast chains benefit from lower values; slow chains can tolerate higher. |
maxRetryableBlockDistance | 128 | Cap on how far ahead of any upstream's known head a request can target before retries stop being attempted. When a request asks for a block that is beyond every upstream's latest tip, eRPC would otherwise retry indefinitely while upstreams catch up. Setting maxRetryableBlockDistance limits how far ahead is still considered "catching up" — once the requested block exceeds the nearest upstream's tip by more than this value, eRPC fails fast with a missing-data error instead of looping. Increase for very slow-syncing chains; decrease to surface indexing gaps faster. |
idempotentTransactionBroadcast | true | When true, duplicate-transaction errors on eth_sendRawTransaction are converted to successful responses by re-computing the tx hash. Lets retry/hedge be safe for transaction broadcast. |
markEmptyAsErrorMethods | none | List of methods where an empty response should be treated as an error (and thus retried/rotated). For example, ["eth_getTransactionReceipt"] on a chain where empty receipts indicate the tx is missing rather than pending. |
getLogsMaxAllowedRange | none | Hard limit on eth_getLogs block range. Requests beyond this are rejected with a 413-style error before being sent. |
getLogsMaxAllowedAddresses | none | Hard limit on the length of the address array in eth_getLogs. |
getLogsMaxAllowedTopics | none | Hard limit on topics[0] OR-list length. |
getLogsSplitOnError | true | When true and an upstream returns "too many results" / 413-style errors, retry by splitting the range, then the addresses, then topics[0]. Results merged server-side. |
getLogsSplitConcurrency | 16 | Parallelism cap for split sub-requests. |
traceFilterSplitOnError | false | Same idea as getLogsSplitOnError for trace_filter / arbtrace_filter. |
traceFilterSplitConcurrency | 10 | Parallelism cap for trace-filter splits. |
enforceBlockAvailability | nil (= true) | Network-level toggle for block-availability enforcement. nil or true means upstreams are skipped when the requested block falls outside their evm.blockAvailability bounds. Set false to globally disable the filter for this network — upstreams will be tried regardless of their declared availability bounds. For per-method control, override enforceBlockAvailability inside methods.definitions.<methodName>. |
Per-network method overrides — methods.*
By default, eRPC uses a built-in cacheable-method table (see evmJsonRpcCache → default methods). You can extend or replace it per network:
networks:
- architecture: evm
evm: { chainId: 999 }
methods:
# When true (default), per-network entries augment the global defaults.
# Set false to ENTIRELY REPLACE the defaults with the definitions below.
preserveDefaultMethods: true
definitions:
# Add a chain-specific RPC method that's not in the default table
custom_specialQuery:
finalized: true
# Override an existing default: don't translate `latest` tag to a number for this method
eth_blockNumber:
translateLatestTag: falseEach entry in definitions is a CacheMethodConfig with the following fields:
| Field | Type | Default | Notes |
|---|---|---|---|
finalized | bool | false | When true, responses are treated as finalized data — cached indefinitely under the finalized finality state. Use for immutable point-lookups (eth_getBlockByHash, eth_getTransactionByHash for a confirmed tx). |
realtime | bool | false | When true, this method observes live mempool state. Realtime responses are not cached. |
stateful | bool | false | When true, the response depends on caller-specific state. Disables multiplexing — each caller gets its own upstream call. Use for custom RPC methods that tie results to connection-level session context. |
reqRefs | [][]string | — | JSON-path segments pointing to block-reference fields in the request params. Used for finality classification, cache key construction, and block-availability filtering. Example: [[1]] means second param is the block number. |
respRefs | [][]string | — | JSON-path segments pointing to block-reference fields in the response result. Used to extract the canonical block number from the response object (e.g. [["blockNumber"]] for a transaction). |
translateLatestTag | bool | true | When true, the latest tag in this method's request is rewritten to a concrete hex block number before caching. Set false when latest should be preserved as-is in the cache key. |
translateFinalizedTag | bool | true | Same as translateLatestTag but for the finalized tag. |
enforceBlockAvailability | bool | true | Per-method override of the network-level evm.enforceBlockAvailability. Set false to skip block-availability filtering for this method only — useful for methods without a meaningful block parameter that would otherwise be filtered. |
When to use stateful: true. Mark a method stateful when its result depends on caller-supplied session state — for example, a custom RPC method tenant_query that takes a session token as a parameter and returns data scoped to that caller. Without stateful: true, eRPC can multiplex different callers' requests through the same upstream connection, which can yield cross-tenant cache hits or wrong results if the upstream ties its response to the connection's session context. Setting stateful: true disables request multiplexing for that method.
networks:
- architecture: evm
evm: { chainId: 999 }
methods:
definitions:
tenant_query:
stateful: true # each caller gets a dedicated upstream connectionselectionPolicy fields
selectionPolicy:
evalInterval: 15s # how often to re-evaluate eligibility (default 15s)
evalTimeout: 100ms # per-tick eval deadline (must be < evalInterval; prior cache retained on timeout)
evalScope: network # 'network' (default) | 'network-method' | 'network-finality' | 'network-method-finality'
evalFunc: |
(upstreams, ctx) =>
upstreams
.removeCordoned()
.excludeIf(all(samplesAbove(10), errorRateAbove(0.7)))
.whenEmpty(() => upstreams)
.preferTag('!tier:fallback', { minHealthy: 1, fallback: 'tier:fallback' })
.sortByScore(PREFER_FASTEST)
.stickyPrimary({ hysteresis: 0.30, minSwitchInterval: '30s' })
.probeExcluded({ sampleRate: 0.1, minSamples: 10, maxConcurrent: 4, timeout: '10s' })evalFunc returns an ordered Upstream[]: position 0 is the primary, the rest are retry order, anything missing is excluded. Use the chainable stdlib (removeCordoned, excludeIf, whenEmpty, preferTag, sortByScore, stickyPrimary, probeExcluded, …) — see Selection policy for the full DSL + every method.
Default policy: omitting selectionPolicy (or evalFunc) applies a production-hardened chain-agnostic default — removeCordoned + excludeIf filters (error / throttle rates each gated on ≥ 10 samples, p70 latency deviation gated on ≥ 20 samples, block-head lag) + tier-based fallback via preferTag('!tier:fallback') + sortByScore(PREFER_FASTEST) + sticky primary + probeExcluded shadow-mirroring (gives excluded upstreams a chance to heal via background traffic). See the full default-policy chain for thresholds and rationale. Investigations are exposed via OTLP tracing and the erpc_selection_* Prometheus metric family.
directiveDefaults — request directives at network level
These apply to every request on this network unless the client explicitly overrides them via HTTP header (X-ERPC-…) or query param.
| Directive | Default | Notes |
|---|---|---|
retryEmpty | true | Retry empty responses (unless method is in retry.emptyResultAccept). |
retryPending | false | Treat pending-block responses as retryable. |
skipCacheRead | false | false = read from every cache; true = skip ALL caches; string = wildcard pattern matching connector IDs to skip (e.g. "memory*" skips all in-memory cache connectors while still reading from Redis/Postgres). |
useUpstream | none | Matcher: only consider upstreams whose id matches (alchemy-*|localnode-*). |
skipInterpolation | false | Skip block-tag → concrete-number rewriting in request params and cache keys entirely. Use only when your client intentionally sends symbolic tags and you want them preserved verbatim through the cache layer. Rarely needed. |
enforceHighestBlock | true | Track highest block seen across upstreams; serve latest/finalized consistently. |
enforceGetLogsBlockRange | true | Validate eth_getLogs block range against upstream availability. |
enforceNonNullTaggedBlocks | true | Convert null responses to errors for eth_getBlockByNumber("latest"|"pending"|...). Numeric block requests are always errors when null regardless. Set false for chains like zkSync that legitimately return null for some tags. |
validateTransactionsRoot, validateTransactionFields, validateTransactionBlockInfo, validateHeaderFieldLengths | false | Cross-validate transaction-root, per-tx fields, block-info, and header field lengths. Expensive but catches malformed responses. |
validateLogFields, validateLogsBloomEmptiness, validateLogsBloomMatch | false | Log-level validations. validateLogsBloomMatch recomputes the bloom filter from logs — most expensive of the three. |
enforceLogIndexStrictIncrements, validateTxHashUniqueness, validateTransactionIndex | false | Receipt-level validations. |
validateReceiptTransactionMatch, validateContractCreation | false | Cross-validate receipt vs transaction. Requires ground-truth transactions in library mode. |
receiptsCountExact | none | Exact expected receipt count for a block response. When set, eRPC rejects responses whose receipt list length doesn't match exactly. Use in conjunction with a known block to catch missing-receipt bugs on specific upstreams. |
receiptsCountAtLeast | none | Minimum expected receipt count. Less strict than receiptsCountExact — rejects only if the upstream returns fewer receipts than this threshold. |
validationExpectedBlockHash | none | Expected block hash (hex string) for the response block. Rejects any response whose hash field doesn't match. Useful in library mode when the caller knows the canonical hash and wants to catch equivocating upstreams. |
validationExpectedBlockNumber | none | Expected block number (integer). Rejects responses whose number field doesn't match. Use alongside validationExpectedBlockHash for full block-identity validation. |
eth_getLogs — splitting and limits
| Mechanism | Setting | Purpose |
|---|---|---|
| Validation | directiveDefaults.enforceGetLogsBlockRange (default true) | Reject if range exceeds the chosen upstream's known availability. |
| Hard limits | evm.getLogsMaxAllowedRange / getLogsMaxAllowedAddresses / getLogsMaxAllowedTopics | Reject oversized requests upfront with a 413-style error. |
| Proactive splitting | upstream.evm.getLogsAutoSplittingRangeThreshold (per upstream) | Network takes the min positive across selected upstreams and splits into contiguous ranges of at most that size. |
| Split on error | evm.getLogsSplitOnError (default true) | Retry by bisecting on range, then addresses, then topics[0] if an upstream returns "too many results". |
| Concurrency | evm.getLogsSplitConcurrency (default 16) | Parallelism for split sub-requests. |
Splitting preserves order. Address count is the length of the address array (if present). Topic count considers only topics[0] when it's an OR-list.
trace_filter and arbtrace_filter — splitting
Same pattern as eth_getLogs, but opt-in (disabled by default):
- Proactive splitting: set
upstream.evm.traceFilterAutoSplittingRangeThresholdto a positive value. Pick something below the upstream's per-response result cap for typical trace density. - Split on error: set
network.evm.traceFilterSplitOnError: true. Bisects block range, thenfromAddress, thentoAddress. - Concurrency:
network.evm.traceFilterSplitConcurrency(default10).
Sub-ranges and address-list halves are disjoint by construction — no server-side deduplication. Order is preserved by sub-request index.
eth_sendRawTransaction — idempotent broadcasting
Enabled by default via evm.idempotentTransactionBroadcast: true. When set:
- "Already known" / duplicate-transaction errors are converted to success responses (the tx hash is recomputed from the signed payload).
- "Nonce too low" errors are verified against on-chain state — if the tx exists, return success.
This makes retry and hedge policies safe for transaction broadcast — duplicate broadcasts to multiple upstreams don't surface as errors to the client.
To disable (e.g. on chains where you want to see the duplicate-broadcast errors):
networks:
- architecture: evm
evm:
chainId: 1
idempotentTransactionBroadcast: falseeth_getTransactionCount — return highest nonce, not most-common
When fanning nonce queries across multiple upstreams, you usually want the highest value, not the most-agreed value (lagging upstreams will report stale lower nonces).
networks:
- architecture: evm
evm: { chainId: 1 }
failsafe:
- matchMethod: eth_getTransactionCount
consensus:
maxParticipants: 3 # query 3 upstreams in parallel
agreementThreshold: 1 # only need 1 valid response; pick the highest
preferHighestValueFor:
eth_getTransactionCount:
- result # the result IS the nonce (hex string)The preferHighestValueFor map keys are method names; values are arrays of JSON field paths to compare:
"result"— compare the response's direct result value (works foreth_getTransactionCountwhich returns"0x5"directly).- Field name(s) — for object results (e.g.
["nonce", "blockNumber"]foreth_getTransactionByHashwhere you want highest nonce, breaking ties by highest blockNumber). - Multiple fields are compared in declaration order; first decides, later ones break ties.
When preferHighestValueFor is set:
maxParticipants— set to the number of upstreams you want to compare (2-3 is typical).agreementThreshold— recommended1. The highest nonce typically reflects the most recently mined tx; requiring agreement would prefer the stale value. Use≥2only when you fear compromised upstreams returning artificially-high nonces.
preferHighestValueFor takes precedence over normal hash-based consensus for the matched method. Error responses are ignored; only valid numeric responses are compared.
markEmptyAsErrorMethods
Treat empty responses on specific methods as errors (so they're retried and the upstream is scored down):
networks:
- architecture: evm
evm:
chainId: 1
markEmptyAsErrorMethods:
- eth_getTransactionReceipt # empty here means tx missing, not pendingBy default, empty responses on most methods are valid data (caching them is safe). Use this only when an empty value indicates the upstream lacks the data.
Static-response error variant
The staticResponses example in the human section shows result-style. For error-style:
staticResponses:
- method: some_unsupported_method
params: []
response:
error:
code: -32601 # JSON-RPC standard error codes; -32601 = method not found
message: "Method not found"
data: # optional, embedded as response.error.data
hint: "use eth_call instead"The same (method, params) matching rules apply. Use this for methods you want clients to immediately know are unsupported on this chain without round-tripping to an upstream.
Multiplexing override
By default, identical concurrent requests on the same network are deduplicated — a single upstream call is made, and the result is shared with all callers. To disable for one network (e.g. to force every probe through to upstream during latency testing):
networks:
- architecture: evm
evm: { chainId: 1 }
multiplexing: falsetrue is the default and almost always what you want — disabling triples upstream RPS during traffic spikes for no benefit in production.
Common pitfalls
failsafereplaces, doesn't deep-mergenetworkDefaults.failsafe— ifnetworks[].failsafeis set, the network entirely overrides defaults for that field. Same forselectionPolicy. Other fields likerateLimitBudgetanddirectiveDefaultsARE deep-merged.matchFinality: ["latest"]— there is nolatestfinality state. Valid values arefinalized,unfinalized,realtime,unknown. Invalid values silently never match.- Network timeout vs upstream timeout — the network
timeout.durationcovers the full request lifecycle including every upstream retry. The upstream's owntimeoutonly bounds one attempt. Set the network timeout generously (≥ upstream timeout × maxAttempts). fallbackFinalityDepth: 1024blocks finality detection — if you set this and the upstream actually supportseth_getBlockByNumber("finalized"), eRPC still uses the dynamic value. The fallback only kicks in if the upstream doesn't.- Static responses bypass everything — including auth's rate-limit budgets. Don't put privileged data in a static response.
alias: eth/1is invalid — only alphanumeric, dash, and underscore allowed.
Append .llms.txt to this URL (or use the AI link above) to fetch the entire expanded reference as plain markdown for an AI assistant.