Reference
EVM behaviors
Method handlers
AI agents: fetch https://docs.erpc.cloud/reference/evm/method-handlers.llms.txt for the complete machine-readable version of this page (full configuration schema, defaults, worked examples, and source links). Append `.llms.txt` to any docs URL for the same treatment.AIFor agents: /reference/evm/method-handlers.llms.txt

Method Handlers

Every EVM node lies a little differently: one returns a block behind on eth_blockNumber, another rejects eth_sendRawTransaction as "already known" after a successful broadcast, a third chokes on a wide trace_filter. eRPC intercepts those inconsistencies before they reach your application — normalizing block tags for cache correctness, making transaction broadcast idempotent across retries, and auto-splitting trace ranges that upstreams can't handle.

Quick taste

Illustrative, not a tuned production config — idempotent broadcast and reactive trace splitting enabled:

projects[].networks[].evm
erpc.yaml
projects:  - id: main    networks:      - architecture: evm        evm:          chainId: 1          # "already known" / "nonce too low" replies become synthetic success          idempotentTransactionBroadcast: true          # reactive bisection when an upstream rejects a trace_filter range          traceFilterSplitOnError: true

Agent 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: make eth_sendRawTransaction retries safe and idempotent
My wallet app retries eth_sendRawTransaction on timeout but sometimes gets "already known"
or "nonce too low" errors that confuse users. Configure eRPC in my eRPC config to make
broadcast idempotent — "already known" should become a synthetic success, and "nonce too low"
should probe whether the same tx is already in-chain before deciding. Read the full reference:
https://docs.erpc.cloud/reference/evm/method-handlers.llms.txt
Prompt Example #2: stop stale block numbers from reaching my app
Some of my upstreams occasionally return a block number that is one block behind the
network head. Configure eRPC in my eRPC config so that eth_blockNumber and
eth_getBlockByNumber always return the highest confirmed block across all upstreams, and
explain which integrity flags control this behavior. Reference:
https://docs.erpc.cloud/reference/evm/method-handlers.llms.txt
Prompt Example #3: enable trace_filter auto-splitting for wide block ranges
My trace pipeline sends trace_filter requests spanning thousands of blocks and some
upstreams reject them with range-too-large errors. Set up proactive splitting per upstream
and reactive bisection as a fallback in my eRPC config. Also tell me which metric to watch
for split failures so I can tune concurrency. Reference:
https://docs.erpc.cloud/reference/evm/method-handlers.llms.txt
Prompt Example #4: debug why my markEmptyAsErrorMethods customization is wrong
I added a custom markEmptyAsErrorMethods list in my eRPC config but now some methods
that returned empty arrays correctly are getting retried in a loop. Explain the footgun
(custom list fully replaces defaults, not merges), show me the default list, and help me
rebuild a correct combined list. Reference:
https://docs.erpc.cloud/reference/evm/method-handlers.llms.txt
Method handlers — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.

How it works

Hook topology. Five hook layers fire at specific points in the request lifecycle, ordered around the cache read and the failsafe retry loop:

  1. Project.PreForward (HandleProjectPreForward, architecture/evm/hooks.go:L10) — fires before cache read. Handles eth_blockNumber (returns highest known, replaces real upstream response), eth_call and eth_estimateGas (inject missing block param), eth_chainId (responds from config), and records trace_filter/arbtrace_filter range histogram.
  2. Network.PreForward (HandleNetworkPreForward, architecture/evm/hooks.go:L40) — fires after upstream selection. Handles eth_chainId (responds from config), and proactive trace_filter/arbtrace_filter auto-splitting when the request range exceeds the per-upstream threshold.
  3. Upstream.PreForward (HandleUpstreamPreForward, architecture/evm/hooks.go:L86) — fires once per upstream attempt. Handles eth_getLogs/trace_filter block-range availability and the eth_chainId fallback (upstream config → network ID string → network config).
  4. Upstream.PostForward (HandleUpstreamPostForward, architecture/evm/hooks.go:L109) — fires after each upstream response. Handles eth_getBlockByNumber/eth_getBlockByHash block validation, eth_getBlockReceipts integrity checks, and eth_sendRawTransaction nonce-exception interception. Also applies the configurable markEmptyAsErrorMethods gate to convert null/empty results to retryable ErrEndpointMissingData. Conversion only fires when the RetryEmpty directive is true on the request; when RetryEmpty=false, null/empty passes through unchanged even for methods in the list.
  5. Network.PostForward (HandleNetworkPostForward, architecture/evm/hooks.go:L62) — fires once after the failsafe loop. Handles eth_getBlockByNumber highest-block enforcement, eth_sendRawTransaction exhausted-broadcast recovery, and reactive trace_filter splitting.

Block-tag interpolation. NormalizeHttpJsonRpc (architecture/evm/json_rpc.go:L92) runs on every request before upstream selection. It resolves "latest" to the highest known block number and "finalized" to the highest known finalized block number (when available), normalizes numeric hex to canonical form, caches the result in the request for downstream cache-key generation, and sets EvmBlockRef metadata ("latest", "finalized", or "*" when both appear) for routing and observability. "safe" and "pending" pass through unchanged. When a ReqRefs path points to a map/object param (rather than a scalar block tag), the function skips replacing that param — this prevents accidentally replacing a complex call-data object with a hex string.

Resolution is lock-efficient: param values are extracted under a read lock, resolved outside the lock, then deep-copy + write-commit only occur when a change is actually needed. The SkipInterpolation directive and per-method translateLatestTag/translateFinalizedTag flags can disable interpolation on a per-request or per-method basis.

Future/missing block guard (emptyResultBeyondConfidence). When an upstream returns null/empty for a numeric block request, emptyResultBeyondConfidence (architecture/evm/common.go:L62) compares the request's cached block number against the network's confidence head (EvmHighestLatestBlockNumber by default; EvmHighestFinalizedBlockNumber when emptyResultConfidence: finalizedBlock). If the requested block is strictly above the head, the result is truthful — no retry is triggered. If the head is unknown (0) the guard fails open: any block is treated as retryable. This prevents retry storms for not-yet-produced future blocks while preserving normal retry behavior for stale nodes.

eth_call / eth_estimateGas missing-block-param injection. If the request arrives with exactly one parameter (the call object, no block tag), the project pre-forward hook appends "latest" as the second param before forwarding; interpolation then pins it to the network's latest block number, so no upstream applies its own default block. eth_call is intentionally excluded from markEmptyAsErrorMethods and from structured error wrapping — callers expect raw ABI revert data (a hex string), not a structured error object.

eth_chainId synthetic response. All three hook levels (project, network, upstream) attempt to return a synthetic response from config without contacting any upstream. The check gates on ShouldSkipCacheRead(""): when x-erpc-skip-cache-read: true is set, all three layers are bypassed so force-refresh requests reach a real upstream for validation. At the upstream level, the handler prefers upstreams[].evm.chainId, then falls back to parsing from the upstream's network ID string (e.g. "evm:1"), then falls back to the network config value.

eth_blockNumber highest-block enforcement. The project pre-forward hook forwards the request normally, then compares the upstream's response against EvmHighestLatestBlockNumber from the network's block poller. If the poller head is higher, the response is replaced with a synthetic one containing the highest known block number and erpc_upstream_stale_latest_block_total is incremented. Cached responses are returned as-is without comparison: the TTL is the correctness control for stale cached data. Controlled by networks[].evm.integrity.enforceHighestBlock (default true).

eth_getBlockByNumber and eth_getBlockByHash. Network.PostForward runs two checks:

  • enforceHighestBlock: For "latest" or "finalized" tagged requests, if the response block number is behind the network's known highest, eRPC re-issues the request excluding the stale upstream, fetches the newer block, and returns whichever of the two is higher via pickHighestBlock. Increments erpc_upstream_stale_latest_block_total or erpc_upstream_stale_finalized_block_total.
  • enforceNonNullBlock: Converts null/empty results to ErrEndpointMissingData. For numeric block params this always applies. For tag params it only applies when enforceNonNullTaggedBlocks is true. Both branches are guarded by emptyResultBeyondConfidence.

Upstream.PostForward runs opt-in block validation when directives are set: ValidateTransactionsRoot, ValidateHeaderFieldLengths, ValidateTransactionFields, and ValidateTransactionBlockInfo. Phantom/system transactions are handled specially — their blocks legitimately have transactionsRoot = emptyTrieRoot despite a non-empty transactions array. Recognized conventions: Polygon PoS / BSC (from=0x0, gas=0x0) and HyperEVM native/L1 system txs (synthetic signature r=0x1 and gasPrice=0).

eth_getBlockReceipts. The upstream post-forward hook runs the opt-in data-integrity module — when enabled via networks[].integrity, its receipt checks (schema, same-block-hash, tx-hash uniqueness, transaction-index consistency, log field shapes/metadata, bloom emptiness/match, log-index contiguity, and — at the authoritative level — receipts-root recompute and receipt-vs-canonical-block corroboration). With no integrity config nothing runs. eth_getBlockReceipts is intentionally NOT in the default markEmptyAsErrorMethods list — empty arrays are the correct response for zero-transaction blocks.

eth_sendRawTransaction idempotency. eRPC makes eth_sendRawTransaction idempotent across retries and failsafe exhaustion.

Upstream-level (nonce exception handling): when an upstream returns ErrCodeEndpointNonceException:

  • already_known (NonceExceptionReasonAlreadyKnown, common/errors.go:L2754) → synthesize success with the locally-derived tx hash immediately (no probe). Matched message patterns: "already known", "known transaction", "already imported", "transaction already in mempool", "tx already in mempool", "already in the mempool", "transaction already exists", "already have transaction", "already exists in mempool". Source: architecture/evm/error_normalizer.go:L304-L323 (opens in a new tab).
  • nonce_too_low (NonceExceptionReasonNonceTooLow, common/errors.go:L2756) → probe eth_getTransactionByHash on the same upstream. If the same tx is found (hash matches), return synthetic success. If a different tx with that nonce is found, re-raise as -32003. If probe itself errors, the original error is returned unchanged. Matched message patterns: "nonce too low", "nonce is too low", "nonce has already been used". Source: architecture/evm/error_normalizer.go:L326-L340 (opens in a new tab).

ErrEndpointNonceException always carries retryableTowardNetwork=false (common/errors.go:L2773). This means N:no: if the upstream-level idempotency hook does not intercept (e.g., idempotentTransactionBroadcast: false), the error will NOT be retried on another upstream. This is intentional — once "already known" is established, retrying on another upstream would either echo the same result or erroneously succeed.

Network-level (exhausted broadcast recovery): after the failsafe loop exhausts all attempts (ErrCodeUpstreamsExhausted, ErrCodeFailsafeRetryExceeded, or ErrCodeFailsafeTimeoutExceeded), eRPC probes eth_getTransactionByHash across the network using context.WithoutCancel and a 3-second independent timeout — so the probe runs even when the parent context is already expired. The returned tx hash is cross-checked against the locally-derived hash before granting synthetic success; a mismatch or missing hash field returns the original error.

Tx hash extraction supports legacy (type-0 RLP) and EIP-2718 typed transactions (EIP-1559, EIP-2930) via ethtypes.Transaction.UnmarshalBinary.

Error normalizer N:yes override for eth_sendRawTransaction: revert/VM-execution errors and out-of-gas/-32003 errors are flipped to retryableTowardNetwork=true (different providers may accept a tx that another rejects). Insufficient-funds errors remain N:no — they are deterministic. The nonce detection rules in the error normalizer MUST appear before the generic -32003 handler. Some vendors use -32003 for "already known" or "nonce too low" messages; if the generic rule fired first, these would bypass the idempotency machinery entirely. Source: architecture/evm/error_normalizer.go:L297-L301 (opens in a new tab).

trace_filter and arbtrace_filter auto-splitting. Two splitting modes coexist:

Proactive splitting (Network.PreForward): if the request's block range exceeds min(TraceFilterAutoSplittingRangeThreshold) across selected upstreams, the request is immediately split into contiguous chunks and dispatched concurrently. Re-entrancy is blocked via ParentRequestId.

Reactive splitting (Network.PostForward): if an upstream returns a range-too-large error (ErrCodeEndpointRequestTooLarge or JsonRpcErrorEvmLargeRange) and traceFilterSplitOnError: true, eRPC bisects the request — halving block range first (splitting at midpoint fromBlock + (range/2)), then fromAddress list, then toAddress list. Returns an error if a single-block + single-address request still triggers the error. Both proactive and reactive splitting also reject fromBlock > toBlock immediately with -32600 (InvalidRequest) before dispatching sub-requests.

Sub-results are concatenated in index order with no deduplication. Any single sub-failure fails the entire merged response. The upstream post-forward hook normalizes null/""/{} results to [] — some older Parity/OpenEthereum nodes return null for empty ranges.

The effective proactive-split threshold is min across all upstreams with a positive TraceFilterAutoSplittingRangeThreshold. Upstreams with threshold=0 are ignored. If ALL upstreams have threshold=0, no proactive splitting occurs. Concurrency is capped by traceFilterSplitConcurrency (default 10). Source: architecture/evm/trace_filter.go:L175-L187 (opens in a new tab).

eth_query* (Monad MIP-16). Five methods — eth_queryBlocks, eth_queryTransactions, eth_queryLogs, eth_queryTraces, eth_queryTransfers — follow MIP-16 (opens in a new tab). One executor (erpc/query_executor.go) answers them for JSON-RPC and for the gRPC bds.evm.QueryService. The request and response codec is the manifesto's: evm.Query*RequestFromJsonRpc parses params strictly (one request object; unknown keys, unknown fields, empty fields arrays, target: "0x0", bad filter values → -32602), and evm.Query*ResponseToJsonRpc renders exactly the selected fields. Monad chains (143, 10143) use the MIP-16 Appendix 1 schema; other chains accept every field of the proto selections.

JSON-RPC entry: Network.Forward hands an eth_query* request to the executor right after static responses and safe-block routing, before multiplexing, cache and per-upstream dispatch. The HTTP server's auth, project rate limit and project allowMethods/ignoreMethods run first like for any request; the network rate limit is taken in forwardQuery; HandleNetworkPostForward and the project metrics run on the result. One JSON call returns one page.

Execution order:

  1. Range. Omitted fromBlock is earliest (asc) or latest (desc); omitted toBlock is latest (asc) or earliest (desc). Each tag resolves once per request (latest → EvmHighestLatestBlockNumber, finalized → EvmHighestFinalizedBlockNumber, safe → finalized else latest, earliest → 0), so two equal tags name the same block. In desc order fromBlock is the upper bound. An inverted range fails with -32602 (evm.ValidateQueryRange); bounds are never swapped.
  2. Method served. No native QueryService upstream that allows the method and no upstream whose evm.queryShim allows it → -32004.
  3. Availability. A range end above the network's highest known block, or a block outside the union of the availability windows (blockAvailability bounds) of the upstreams that can serve the method natively or through the shim sub-requests → -32001, before any scan. For the shim, an upstream counts only when its method allow/ignore config accepts every sub-request the request will issue (ShouldHandleMethod returns true without an error): eth_getBlockByNumber always, plus eth_getLogs for eth_queryLogs, trace_block or debug_traceBlockByNumber for traces and transfers, and eth_getBlockReceipts when the selection reads receipt fields; none counting → -32004.
  4. Native pipe-through. Native upstreams are tried in selection-policy order with the MIP-16 proto request unchanged. An upstream that fails before its first page (for example UNIMPLEMENTED for eth_queryTraces on a reader without traces) falls through to the next one, then to the shim. After a page went out, a stream error ends the request: no other upstream continues it. A JSON call takes the first page and cancels the stream; that page's cursorBlock is the page end.
  5. Shim. The shim builds pages from eth_getBlockByNumber, eth_getBlockReceipts, eth_getLogs and trace_block / debug_traceBlockByNumber sub-requests, each sent through Network.Forward (cache, failsafe, upstream selection) with the request id as parent id. The sub-requests of one page prefer the upstream that served the first one (UseUpstream set to its id), for methods that upstream accepts (ShouldHandleMethod true without an error); others go unpinned. A pinned sub-request that fails is sent once more unpinned, and one answered with null (a lagging upstream that does not have the block yet) is asked once more of every other upstream that takes the method (!<id>), so a lagging upstream never turns an available block into -32001; that answer stands, an error included, except an error saying the block is missing (ErrEndpointMissingData, ErrUpstreamBlockUnavailable), which keeps the null and so -32001. The upstream that answers with data becomes the page's pin; a pin that answered null where no other upstream had data is dropped, so later sub-requests of the page go unpinned. A sub-request no upstream serves (eth_getBlockReceipts, or both trace methods) fails with -32004.

Shim pages. A page scans blocks from its fromBlock in traversal order, in chunks fetched concurrently (4, doubling to 64; 16 sub-requests in flight), and ends at the first of: toBlock; the block that brings the primary count to target or more (the whole block is in the page); the budget; a break in chain consistency. A block is complete once its header, its primary objects and the transactions they join (with receipts when selected) are fetched, all of it inside the page's time budget, so the budget bounds every sub-request of the page. The budget is queryShim.maxBlocksPerPage blocks (default 1000) and queryShim.maxPageDuration (default 10s), the smallest across the upstreams that enable the shim for the method. A page that hits the time budget ends at the last complete block; if fromBlock itself did not complete, the request fails with -32005. Only the page's own deadline ends a page early: a sub-request that times out or fails on its own fails the request. fromBlock and cursorBlock are the verified headers of the page's first and last block; toBlock is the header of the resolved toBlock, fetched once more only when the page ends before it. Related objects (transactions, blocks) appear only when selected, once each, only for blocks and transactions a primary object references; they do not count toward target. They come from the block already fetched during the scan (eth_getBlockByNumber with full transactions when transactions are selected), never from a second fetch; logs without blockTimestamp take the scanned header's timestamp. Transactions carry receipt fields merged from eth_getBlockReceipts (evm.MergeReceipt) whenever the selection reads one of status, gasUsed, cumulativeGasUsed, effectiveGasPrice, contractAddress, logsBloom. Primary and related arrays are sorted by the MIP-16 ordering keys and reversed as a whole in desc order, including objects inside one block.

Shim chain consistency. One response never mixes blocks from before and after a reorg (MIP-16 Chain consistency). Each scanned block is checked against its own header: every log, receipt, transaction and trace_block frame must carry the header's blockHash, each trace_block frame with a transactionHash must name one of the block's transactions, and each callTracer result's txHash must be the block's transaction at its index (callTracer results carry no block hash). Consecutive headers must link by parentHash in traversal order (in desc, the earlier block's parentHash is the next block's hash). At the first break, the scan stops; the blocks from the one before the break are scanned once more with cache reads skipped (skipCacheRead) and no pin, and the longer consistent run wins. The page ends at the last consistent block, so the next page starts on whichever chain the upstreams then serve; clients detect the switch by comparing fromBlock.parentHash with the previous cursorBlock.hash. If fromBlock itself is still inconsistent after the re-scan, the request fails as transient: erpc's retryable server error -32603 on JSON-RPC, UNAVAILABLE on gRPC; retry.

Shim filters. null and [] place no constraint; an array matches any element; fields combine with AND. selector matches only inputs of 4 bytes or more. eth_queryLogs passes address and topics to eth_getLogs (one call per chunk), so topic semantics are exactly eth_getLogs'. eth_queryBlocks filters on miner.

Shim traces and transfers. trace_block frames convert with evm.TraceFromParity and evm.PropagateParityReverted (reward frames are dropped); when trace_block is unsupported, debug_traceBlockByNumber with callTracer converts per transaction with evm.TraceFromGethDebug. The network remembers which method answered and asks it directly afterwards. Transfers are evm.NativeTransfersFromTraces. Frames with reverted: true are dropped unless filter.includeReverted is true. System transactions and precompile calls are kept.

Errors. JSON-RPC errors carry the MIP-16 code in error.code: -32602 invalid params, -32001 range outside availability, -32004 method not served, -32005 budget exceeded before fromBlock completed. gRPC returns INVALID_ARGUMENT, OUT_OF_RANGE, UNIMPLEMENTED and RESOURCE_EXHAUSTED for the same conditions. fromBlock not consistent across sub-requests (reorg in progress, lagging upstream) is transient and no MIP-16 condition: -32603 on JSON-RPC, UNAVAILABLE on gRPC. A sub-request failure that has no MIP-16 meaning (upstream error, timeout) passes through erpc's usual error normalization on JSON-RPC, and erpc's gRPC status mapping on gRPC (DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, UNAVAILABLE, ...; see gRPC & BDS). A native upstream's own error is unwrapped from the stream wrapper, so its gRPC status keeps its MIP-16 code on both transports (OUT_OF_RANGE → -32001, and so on) when no shim takes over.

Config schema

Network-level fields (networks[].evm.*)

FieldTypeDefaultBehavior / footguns
integrity.enforceHighestBlock*booltrueEnables highest-block enforcement for eth_blockNumber and eth_getBlockByNumber. Also propagated to DirectiveDefaults.EnforceHighestBlock. Deprecated path — new canonical path is networks[].directiveDefaults. Source: common/config.go:L2114, common/defaults.go:L1458-1459
integrity.enforceNonNullTaggedBlocks*booltrueWhen true, null responses for tagged block queries ("latest", "finalized") trigger ErrEndpointMissingData. Propagated to DirectiveDefaults.EnforceNonNullTaggedBlocks. Source: common/config.go:L2116, common/defaults.go:L1464-1465
integrity.enforceGetLogsBlockRange*booltrueEnables upstream-level block range availability checking for trace_filter/arbtrace_filter and eth_getLogs. Source: common/config.go:L2115, common/defaults.go:L2121-2122
emptyResultConfidenceAvailabilityConfidenceblockHeadConfidence head for emptyResultBeyondConfidence: blockHead uses EvmHighestLatestBlockNumber; finalizedBlock uses EvmHighestFinalizedBlockNumber. Source: common/config.go:L2250, common/defaults.go:L2075-2078
markEmptyAsErrorMethods[]stringsee default listMethods for which null/empty upstream responses become ErrEndpointMissingData. nil = use default list. [] (empty slice) = disable entirely. Footgun: setting a custom list does NOT merge with defaults — the default list is fully replaced. Source: common/config.go:L2218, common/defaults.go:L2111-2113
idempotentTransactionBroadcast*booltrue (nil = enabled)Enables idempotency handling for eth_sendRawTransaction. Set false to disable. Source: common/config.go:L2239
traceFilterSplitOnError*boolnil (off)Enables reactive bisection of trace_filter on too-large errors. Intentionally off by default. Source: common/config.go:L2197, common/defaults.go:L2103-2104
traceFilterSplitConcurrencyint10Max in-flight sub-requests during trace_filter splitting. Source: common/config.go:L2200, common/defaults.go:L2105-2106

Default markEmptyAsErrorMethods (common/defaults.go:L2044-2064): eth_blockNumber, eth_getBlockByNumber, eth_getTransactionByHash, eth_getTransactionByBlockHashAndIndex, eth_getTransactionByBlockNumberAndIndex, eth_getUncleByBlockHashAndIndex, eth_getUncleByBlockNumberAndIndex, debug_traceTransaction, trace_transaction, trace_block, trace_get. Excluded by design: eth_getBlockByHash (subgraphs return empty legitimately), eth_getTransactionReceipt (null for pending is valid), eth_getBlockReceipts (empty array for 0-tx blocks is valid).

Per-method fields (networks[].methods.definitions.<method>.*)

FieldTypeDefaultBehavior / footguns
translateLatestTag*boolnil (enabled)When nil or true, "latest" in this method's block param is replaced with the concrete hex from EvmHighestLatestBlockNumber. Footgun: disabling causes the cache key to omit a concrete block number, breaking cache deduplication with concrete-block requests for the same block. Source: common/config.go:L307-309, architecture/evm/json_rpc.go:L130
translateFinalizedTag*boolnil (enabled)Same as above for "finalized" and EvmHighestFinalizedBlockNumber. Both flags are evaluated independently. Even when disabled, seenFinalized is still recorded so EvmBlockRef metadata is still written. Source: common/config.go:L310-312, architecture/evm/json_rpc.go:L131

Upstream-level fields (upstreams[].evm.*)

FieldTypeDefaultBehavior / footguns
traceFilterAutoSplittingRangeThresholdint640 (disabled)Max block range per chunk for proactive trace_filter splitting at this upstream. Effective threshold is min across all selected upstreams with positive values. Zero = this upstream doesn't constrain. Source: common/config.go:L1134, common/defaults.go:L1546-1547
queryShim.enabled*boolnil (disabled)This upstream may serve the shim sub-requests of eth_query*. At least one upstream must enable it (or serve QueryService natively) for a method, else the method returns -32004. Source: common/config.go:L1373-1383
queryShim.allowedMethods[]string[] (all allowed)Wildcard-matched eth_query* methods the shim serves through this upstream. Empty = all five. Source: common/config.go:L1404-1419
queryShim.maxBlocksPerPageint641000Budget: blocks one shim page may scan. The smallest value across shim upstreams wins. Source: erpc/query_executor.go:L48-53
queryShim.maxPageDurationDuration10sBudget: time one shim page may scan. The page ends at the last complete block; -32005 when fromBlock did not complete. The smallest value across shim upstreams wins. Source: erpc/query_executor.go:L48-53

Worked examples

1. Indexer with wide trace_filter ranges. Proactive splitting per upstream keeps trace requests within each node's limit. The min-threshold is 200 blocks across upstreams, but different providers can declare different constraints and eRPC uses the most restrictive:

projects[].networks[].evm + upstreams[].evm
erpc.yaml
# Network level — reactive fallback if proactive missesnetworks:  - architecture: evm    evm:      traceFilterSplitOnError: true      traceFilterSplitConcurrency: 20
# Upstream level — proactive split threshold per providerupstreams:  - id: quicknode    evm:      traceFilterAutoSplittingRangeThreshold: 200  - id: alchemy    evm:      traceFilterAutoSplittingRangeThreshold: 500

2. Broadcast-heavy app (wallets, bots). With idempotent broadcast enabled, retried sends that arrive with "already known" or "nonce too low" from one provider are converted to synthetic success rather than surfacing confusing errors. No config change needed — it is on by default. Disable only if your app has its own nonce tracking and does not want eRPC to probe on its behalf:

projects[].networks[].evm
erpc.yaml
networks:  - architecture: evm    evm:      idempotentTransactionBroadcast: false  # disable if app handles nonces

3. Finality-sensitive data pipeline. When your application must never read data above the finalized head (e.g. a settlement system), set emptyResultConfidence: finalizedBlock. Blocks between finalized and latest become "beyond confidence" — empty results from that range are returned truthfully rather than retried:

projects[].networks[].evm
erpc.yaml
networks:  - architecture: evm    evm:      emptyResultConfidence: finalizedBlock      integrity:        enforceHighestBlock: true        enforceNonNullTaggedBlocks: true

4. eth_query* (MIP-16) from an archive node. Enable the shim on an archive upstream that serves blocks, receipts, logs and traces to expose the five MIP-16 methods. Leave allowedMethods empty to allow all five; lower the budget to keep each page short:

upstreams[].evm.queryShim
erpc.yaml
upstreams:  - id: archive-node    evm:      queryShim:        enabled: true        # at most 500 blocks or 5s of scanning per page        maxBlocksPerPage: 500        maxPageDuration: 5s

Request/response behavior

Best practices

  • Enable traceFilterSplitOnError: true in production even if you also set traceFilterAutoSplittingRangeThreshold per upstream — reactive splitting is your safety net when a new upstream joins that doesn't advertise its limit.
  • Never add eth_getBlockReceipts to markEmptyAsErrorMethods: empty arrays are the correct response for 0-transaction blocks and inclusion will cause retry storms against the outer network timeout.
  • If you set a custom markEmptyAsErrorMethods list, the default list is fully replaced — copy the defaults from common/defaults.go:L2044-L2064 and then add your additions.
  • Keep idempotentTransactionBroadcast: true (the default). The only reason to disable it is if your application manages nonces explicitly and treats "already known" / "nonce too low" as signals for its own retry logic.
  • Use emptyResultConfidence: finalizedBlock for settlement or audit workloads where serving un-finalized data would be a correctness violation. The default blockHead is appropriate for all other use cases.
  • Avoid setting translateLatestTag: false or translateFinalizedTag: false per-method unless you have a specific reason — disabling tag translation breaks cache deduplication: "latest"-tagged and concrete-block requests for the same block will generate different cache keys.
  • Set traceFilterSplitConcurrency conservatively (start at 10, the default) and watch erpc_network_evm_trace_filter_split_failure_total before raising it — each concurrent sub-request is a separate upstream call.

Edge cases & gotchas

  1. eth_call / eth_estimateGas missing block param auto-injection — a client sending only the call object (no block tag) gets "latest" silently appended. Source: architecture/evm/eth_call.go:L18-30.
  2. eth_chainId skip-cache-read bypasses all synthetic levels — x-erpc-skip-cache-read: true causes all three hook levels to fall through to real upstreams. Source: architecture/evm/eth_chainId.go:L26.
  3. eth_blockNumber from-cache bypass — cached responses skip the highest-block comparison; TTL is the correctness control for stale cached data. Source: architecture/evm/eth_blockNumber.go:L40-48.
  4. enforceHighestBlock excludes stale upstream only when block was extracted — if the upstream returned a JSON-RPC error, the extracted block number is 0 and the upstream is NOT excluded from the re-forward. Source: architecture/evm/eth_getBlockByNumber.go:L196-202.
  5. pickHighestBlock safety — always compares old vs new response rather than blindly returning the re-fetched block, guarding against corrupt poller state. If one response is null/empty, returns the other; if both are null/empty, returns the error from the re-forward rather than a null block. Source: architecture/evm/eth_getBlockByNumber.go:L335-415.
  6. eth_getBlockReceipts excluded from default markEmptyAsErrorMethods — empty arrays are correct for 0-tx blocks; inclusion causes retry storms. Source: common/defaults.go:L2054-2064.
  7. Phantom transactions (Polygon PoS / BSC / HyperEVM) — blocks whose transactions are all system/phantom txs legitimately have transactionsRoot = emptyTrieRoot on a non-empty array; the integrity module's transactionsRootConsistency check skips the inconsistency error for these. Polygon PoS / BSC system txs use from=0x0, gas=0x0; HyperEVM native/L1 system txs instead carry a real-looking sender and non-zero gas and are marked by a synthetic signature r=0x1 with zero gasPrice (which also covers the HYPE bridge case where s=1 ≠ from). Source: architecture/evm/integrity/checks_block.go.
  8. trace_filter null-to-empty normalization — older Parity/OpenEthereum nodes return null for empty ranges; ["result": null] is converted to []. Source: architecture/evm/trace_filter.go:L361-374.
  9. trace_filter proactive split min-threshold across upstreams — all-zero thresholds means no proactive splitting occurs. Source: architecture/evm/trace_filter.go:L175-187.
  10. eth_sendRawTransaction independent probe context — the parent context is always expired when the network-level hook fires; context.WithoutCancel is required for the probe to run. Source: architecture/evm/eth_sendRawTransaction.go:L346-347.
  11. eth_sendRawTransaction hash mismatch refuses synthetic success — prevents a byzantine upstream from faking a successful broadcast. Source: architecture/evm/eth_sendRawTransaction.go:L375-385.
  12. queryShim allowedMethods narrows the shim, not the network — a method that no shim upstream allows and no native upstream serves returns -32004; it is never forwarded raw. Source: erpc/query_executor.go.
  13. eth_queryTraces trace_block → debug fallback — -32601/unsupported trace_block switches the network to debug_traceBlockByNumber (callTracer); if both are unsupported the request returns -32004. Source: erpc/query_shim.go.
  14. eth_queryLogs reads one eth_getLogs per scan chunk — chunks start at 4 blocks and double to 64, so a node's eth_getLogs range limit above 64 blocks never bites; eth_getLogs also applies the address/topics filter. Source: erpc/query_shim.go.
  15. 200-OK ABI-selector revert detection — responses whose result starts with 0x08c379a0 (Error(string) selector) are converted to ErrEndpointExecutionException. Applies to all methods that can trigger EVM execution, not just eth_call. Source: architecture/evm/error_normalizer.go:L609-624.
  16. trace/debug timeout detection via raw-byte scan — responses from trace_*/debug_*/eth_trace methods can exceed 50 MB; the normalizer uses strings.Contains(dt, "execution timeout") instead of JSON parsing to detect server-side timeouts. Source: architecture/evm/error_normalizer.go:L626-652.
  17. validateBlock requires non-nil directives — all upstream-level block validation for eth_getBlockByNumber is skipped when rq.Directives() is nil. Source: architecture/evm/eth_getBlockByNumber.go:L449-454.
  18. translateLatestTag=false breaks cache deduplication — disabling tag translation causes cache keys to omit a concrete block number; "latest"-tagged and concrete-block requests for the same block produce different cache keys. Source: architecture/evm/json_rpc.go:L147-153.
  19. integrity fields are deprecated — networks[].evm.integrity.* is deprecated; values are migrated to networks[].directiveDefaults during SetDefaults. Source: common/defaults.go:L1957-1964.
  20. ErrEndpointClientSideException returns HTTP 200 for revert-class codes — JsonRpcErrorEvmReverted (3), JsonRpcErrorCallException (-32000), and JsonRpcErrorTransactionRejected (-32003) return HTTP 200, not 400; callers must check the JSON-RPC error code to distinguish execution failures from malformed requests. Source: common/errors.go:L1894-1905.
  21. markEmptyAsErrorMethods requires RetryEmpty=true to fire — methods in the list only have their null/empty results converted to ErrEndpointMissingData when the RetryEmpty directive is true on the request. When RetryEmpty=false, null/empty passes through unchanged regardless of the list. Methods NOT in the list are never converted. Source: architecture/evm/common.go:L62-89.
  22. trace_filter fromBlock > toBlock returns -32600 immediately — both the proactive network pre-forward hook and the upstream pre-forward block-range check reject requests where fromBlock > toBlock with a -32600 (InvalidRequest) error before any sub-requests are dispatched. Source: architecture/evm/trace_filter.go:L122-200.
  23. eth_sendRawTransaction upstream probe errors return the original error — if the eth_getTransactionByHash probe issued during nonce_too_low handling itself fails (network error, timeout, etc.), the original nonce error is returned unchanged. The probe is a best-effort check, not a retry gate. Source: architecture/evm/eth_sendRawTransaction.go:L51-270.
  24. NormalizeHttpJsonRpc skips top-level object params — when a ReqRefs path points to a map/object param (e.g., the call object in eth_call), the function does not attempt to replace it with a hex block number even if the path resolves to a numeric value. Only scalar leaf-level block references within nested objects are replaced. Source: architecture/evm/json_rpc.go:L168-194.
  25. eth_query* "pending" is invalid — only latest, earliest, safe, finalized and QUANTITY are accepted (-32602). "safe" resolves to the finalized block when known, else latest; "earliest" is block 0, which the availability check requires the union of the serving upstreams' windows to cover. Source: erpc/query_executor.go.
  26. eth_query* tries native QueryService upstreams first — gRPC BDS upstreams that serve bds.evm.QueryService and allow the method are tried before the shim, in selection-policy order. A native upstream needs allowMethods to admit the method (or eth_query*). On Monad, Goldsky's endpoint serves eth_queryLogs and eth_queryTransactions natively; see Goldsky cache. Source: erpc/query_executor.go.
  27. eth_query* JSON calls skip the cache and multiplexer — each JSON call runs the executor; only the shim's sub-requests are cached. Source: erpc/networks.go.
  28. eth_query* shim pages stop at a reorg — a page whose next block does not link to the previous one by parentHash, or whose objects carry another block hash than their header, ends at the last consistent block after one cache-skipping re-scan; a short page near the tip is normal, and -32603 (gRPC UNAVAILABLE) means even fromBlock was inconsistent. Retry, or page from "finalized". Source: erpc/query_shim.go.

Observability

MetricTypeLabelsWhen it fires
erpc_upstream_stale_latest_block_totalCounterproject, vendor, network, upstream, categoryUpstream returned an older block than the network's known highest; category = "eth_blockNumber" or "eth_getBlockByNumber"
erpc_upstream_stale_finalized_block_totalCounterproject, vendor, network, upstreamUpstream returned an older finalized block in eth_getBlockByNumber
erpc_network_latest_block_timestamp_distance_secondsGaugeproject, network, origineth_getBlockByNumber(latest) response received with origin="network_response"
erpc_network_evm_trace_filter_range_requestedHistogramproject, network, method, user, finalityAny trace_filter/arbtrace_filter request with resolvable block bounds
erpc_network_evm_trace_filter_forced_splits_totalCounterproject, network, method, dimension, user, agent_nameEach split by dimension (block_range, from_address, to_address)
erpc_network_evm_trace_filter_split_success_totalCounterproject, network, method, user, agent_nameEach successful sub-request in a split execution
erpc_network_evm_trace_filter_split_failure_totalCounterproject, network, method, user, agent_nameEach failed sub-request in a split execution

Trace span names (for distributed tracing / APM):

  • Project.PreForwardHook — parent span for all project-level hooks
  • Project.PreForwardHook.eth_chainId, Project.PreForwardHook.eth_blockNumber
  • Network.PreForwardHook, Network.PreForwardHook.eth_chainId
  • Network.PostForward.eth_getBlockByNumber, Network.PostForward.eth_sendRawTransaction
  • Upstream.PreForwardHook, Upstream.PreForwardHook.eth_chainId, Upstream.PreForwardHook.trace_filter
  • Upstream.PostForwardHook.eth_getBlockByNumber, Upstream.PostForwardHook.eth_getBlockReceipts, Upstream.PostForwardHook.eth_sendRawTransaction
  • Evm.PickHighestBlock, extractTxHashFromSendRawTransaction, createSyntheticSuccessResponse, verifyAndHandleNonceTooLow

Notable log messages (for debugging):

  • "interpolated block tag to concrete block number" (debug) — NormalizeHttpJsonRpc resolved "latest" or "finalized" to a concrete hex.
  • "passed through block tag" (trace) — "safe", "pending", etc. left unchanged.
  • "upstream returned older block than we known, falling back to highest known block" (debug) — eth_blockNumber and eth_getBlockByNumber stale enforcement.
  • "skipping enforcement of highest block number as response is from cache" (trace) — cache bypass in eth_blockNumber and eth_getBlockByNumber.
  • "converting 'already known' error to idempotent success" (info) — eth_sendRawTransaction upstream hook.
  • "tx FOUND on-chain - converting 'nonce too low' to idempotent success" (info) — eth_sendRawTransaction nonce-too-low probe success.
  • "exhausted error overridden: tx found in network, returning synthetic success" (info) — eth_sendRawTransaction network-level hook.
  • "verification response carries a different tx hash than submitted — refusing synthetic success" (warn) — hash mismatch guard.

Source code entry points

Related pages

  • EVM network config — where networks[].evm.* fields live alongside chain ID, polling, and block tracker settings.
  • Upstream config — upstreams[].evm.* fields including queryShim and traceFilterAutoSplittingRangeThreshold.
  • Failsafe — the retry/timeout/hedge loop that wraps the hooks; method handlers fire inside this loop.
  • Cache — block-tag interpolation (handled here) directly controls cache key stability.
  • Matcher — controls which requests reach which upstreams before hook logic runs.
  • Survive provider outages — how idempotent broadcast and block enforcement combine during failover.