/config/projects/static-responses.llms.txt
Static responses
Some chains deviate from standard EVM assumptions — a genesis block at a non-zero number,
a hardcoded eth_chainId, a known-null receipt for a synthetic transaction. Sending those
requests upstream returns errors or inconsistent data. Static responses short-circuit the
entire pipeline and return your configured answer in microseconds, consuming zero upstream
quota.
What you get
- Matched requests never touch an upstream or the cache
- Any JSON-RPC shape: success result, typed error, or
null - Full param matching with YAML/JSON type tolerance built in
- One Prometheus counter to confirm every match
Quick taste
Illustrative, not a tuned production config — serve block 0 from config:
projects: - id: main networks: - architecture: evm evm: { chainId: 1 } staticResponses: # short-circuit: matched requests never reach an upstream - method: eth_blockNumber params: [] response: result: "0x0"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: stub a chain that has no real genesis block
Some EVM chains don't have a real block at number 0x0 — querying it returns
an error or times out. Add a staticResponse to my eRPC network config that returns
a synthetic, well-formed genesis block object for eth_getBlockByNumber("0x0", false)
so clients never hit the upstream for this. Work with my existing eRPC config. Read the
full reference first:
https://docs.erpc.cloud/config/projects/static-responses.llms.txtPrompt Example #2: hardcode eth_chainId across all providers
Some of my upstream providers return inconsistent or wrong eth_chainId values for a custom network. Pin eth_chainId to the correct hex value in my eRPC config in my eRPC config using a staticResponse so no upstream is ever consulted for this method. Reference: https://docs.erpc.cloud/config/projects/static-responses.llms.txt
Prompt Example #3: return a typed error for unsupported methods
Certain JSON-RPC methods (e.g. eth_newFilter, debug_traceTransaction) are not supported by my network and return garbage from upstreams. Add staticResponse entries in my eRPC config that return a proper JSON-RPC -32601 methodNotFound error for each of those methods instead of forwarding to upstreams. Reference: https://docs.erpc.cloud/config/projects/static-responses.llms.txt
Prompt Example #4: debug why a static rule isn't matching
I configured a staticResponse in my eRPC config but requests are still reaching my upstream — the erpc_network_static_response_served_total counter stays at zero. Help me diagnose the mismatch: explain hex-string normalization gotchas, YAML int vs JSON float64 equivalence, and how to confirm the exact param form my client sends. Reference: https://docs.erpc.cloud/config/projects/static-responses.llms.txt
Static responses — full agent referenceExpand for every option, default, and edge case — or copy this entire section into your AI assistant.
How it works
Network.Forward checks for a static-response match immediately after extracting the
JSON-RPC method — before multiplexing, cache reads, and upstream selection
(erpc/networks.go:L973-981). A hit short-circuits the entire
pipeline and returns the configured reply in microseconds. A miss falls through to the
normal path unmodified.
Matching is performed by FindStaticResponseMatch (common/static_response.go:L14-24),
which iterates the staticResponses slice in declaration order and returns the first entry
whose method equals the request method (exact, case-sensitive string match) AND whose
params deep-equal the inbound params. First match wins; subsequent entries are never
evaluated.
On a hit, tryServeStaticResponse (erpc/networks_static_responses.go:L15-63)
builds a JsonRpcResponse with the inbound request's id (not any stored value), the
configured result or error object, and jsonrpc: "2.0". The response carries no
upstream metadata — Upstream() is nil and FromCache() is false.
Numeric type tolerance. YAML config deserializes integers as Go int; inbound JSON
deserializes them as float64. valueEqual normalizes both to float64, so a config
param of 0 matches an inbound 0 (or 0.0) without any special handling. All numeric
types (float32/64, int/8/16/32/64, uint/8/16/32/64) are converted to float64 before
comparison. Maps are compared key-count-first then recursively, order-independently.
Other types fall back to reflect.DeepEqual.
Hex strings are not normalized. "0x0" and "0x00" are different values. If your
client sends "0x00" but your config uses "0x0", no match occurs and the request falls
through to an upstream. Use the exact form your client sends.
No params wildcard. Every element in params must match exactly. To match a method
regardless of its params, configure an entry with an empty params array (which also
matches inbound null params and omitted params, since all three have len == 0).
No defaults applied. Neither StaticResponseConfig nor its sub-structs have any
SetDefaults wiring — all fields are fully explicit. The list itself defaults to nil
(feature off).
Config schema
All fields live under networks[*].staticResponses[*].
| Field | Type | Default | Behavior / footguns |
|---|---|---|---|
networks[*].staticResponses | []*StaticResponseConfig | nil (feature off) | Checked in declaration order; first match wins. Source: common/config.go:L2005 |
networks[*].staticResponses[*].method | string | required | Exact JSON-RPC method name; case-sensitive string equality. Empty string rejected at startup. Source: common/config.go:L2015, common/validation.go:L1282-1284 |
networks[*].staticResponses[*].params | []interface{} | nil | Params to match; nil and empty-slice are equivalent in matching (both len == 0). Omitting the key yields nil; params: [] yields non-nil empty slice — irrelevant to matching. Supports numbers, strings, booleans, arrays, maps. Source: common/config.go:L2016 |
networks[*].staticResponses[*].response | *StaticResponseBodyConfig | required | Exactly one of result or error must be set; nil rejected at startup. Source: common/config.go:L2017, common/validation.go:L1285-1298 |
networks[*].staticResponses[*].response.result | interface{} | nil | Any JSON-serializable value; set to serve a successful response. Mutually exclusive with error. result: null YAML is treated as set (non-nil interface wrapping nil). Source: common/config.go:L2023 |
networks[*].staticResponses[*].response.error | *StaticResponseErrorConfig | nil | Set to return a JSON-RPC error response. Mutually exclusive with result. Requires message to be non-empty. Source: common/config.go:L2024 |
networks[*].staticResponses[*].response.error.code | int | 0 (no range enforced) | JSON-RPC error code. Typical values: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error. Footgun: code 0 passes validation but is omitted from wire JSON because ErrJsonRpcExceptionExternal.Code uses json:"code,omitempty". Always set a non-zero value. Source: common/config.go:L2029, common/errors.go:L2226 |
networks[*].staticResponses[*].response.error.message | string | required when error is set | Human-readable error description. Empty string rejected at startup with "response.error.message is required". Source: common/config.go:L2030, common/validation.go:L1296-1298 |
networks[*].staticResponses[*].response.error.data | interface{} | nil | Optional extra data in the JSON-RPC error object. Any JSON-serializable value. Omitted from wire JSON when nil (omitempty). Source: common/config.go:L2031 |
Worked examples
All patterns below are distilled from real production fleets; comments explain the non-obvious choices.
1. Synthetic genesis block (canonical production pattern). HyperEVM, LightLink, Sei
testnet, and several other EVM chains have no real state at block 0x0 — their node RPCs
return an error or an empty response for eth_getBlockByNumber("0x0", false). Indexers and
client libraries assume a valid block always exists at that slot. The fix is a fully-formed
synthetic block object served straight from config; zero upstream calls are made:
staticResponses: - method: eth_getBlockByNumber # match the exact hex form clients send — "0x00" would NOT match "0x0" params: ["0x0", false] response: result: baseFeePerGas: "0x0" difficulty: "0x1" extraData: "0x" # gasLimit should be plausible for the chain; 10M is a safe default gasLimit: "0x989680" gasUsed: "0x0" # zero hash signals "synthetic" to any tooling that inspects it hash: "0x0000000000000000000000000000000000000000000000000000000000000000" logsBloom: "0x000...000" miner: "0x0000000000000000000000000000000000000000" mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000" nonce: "0x0000000000000000" number: "0x0" parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000" receiptsRoot: "0x0000000000000000000000000000000000000000000000000000000000000000" sha3Uncles: "0x0000000000000000000000000000000000000000000000000000000000000000" size: "0x1d01" stateRoot: "0x0000000000000000000000000000000000000000000000000000000000000000" timestamp: "0x0" totalDifficulty: "0x1" transactions: [] transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" uncles: []2. Hash-addressed genesis (pair with block-number entry). Some clients look up genesis
by its known zero hash (0x000...000) rather than by number. HyperEVM production adds a
second static entry for eth_getBlockByHash using the same synthetic object — declaration
order means the hash lookup is served before the catch-all, no upstream contact either way:
staticResponses: - method: eth_getBlockByNumber params: ["0x0", false] response: result: { number: "0x0", hash: "0x000...000" } # full block object - method: eth_getBlockByHash # clients that cache "genesis hash = all zeros" hit this instead of falling through params: ["0x0000000000000000000000000000000000000000000000000000000000000000", true] response: result: { number: "0x0", hash: "0x000...000" } # same synthetic block, with txs3. Hardcoded chain identity. Some chains return inconsistent eth_chainId values
across providers (e.g. decimal vs hex, or different values on archive vs realtime nodes).
Serve it from config to guarantee determinism — upstreams are never consulted for this
method, which also eliminates a round-trip on every new client connection:
staticResponses: - method: eth_chainId # empty params matches inbound null params, [], and omitted params — all equivalent params: [] response: # use the canonical hex form your chain uses; 0x539 = 1337 (local devnet) result: "0x539"4. Typed error for unsupported or dangerous methods. Return a proper JSON-RPC error instead of forwarding to an upstream that will reject the call anyway. Declaration order matters — put specific error stubs BEFORE any catch-all entries:
staticResponses: - method: eth_getBlockByNumber params: ["0x0", false] response: error: # -32602 = invalid params (genesis not available is a client input problem) code: -32602 message: "genesis block not available on this network" # optional structured data surfaced in the JSON-RPC error.data field data: { reason: "chain does not expose pre-migration history" }5. Known-null receipt for a synthetic transaction. Some bridge or L2 networks produce
transaction hashes that will never appear on-chain. Instead of falling through to an
upstream that returns an error, return null explicitly — sets result: null in the wire
JSON, which is the correct "not found" shape for receipt lookups:
staticResponses: - method: eth_getTransactionReceipt params: ["0xdeadbeef000000000000000000000000000000000000000000000000deadbeef"] response: # result: null is treated as "result is set to null" — valid wire JSON result: nullRequest/response behavior
- Static responses short-circuit
Network.Forwardbefore cache reads and upstream selection. The only things that run earlier are method extraction and OTel span setup. [erpc/networks.go:L973-981] - The inbound request's
idis always echoed in the response — not any stored value. String ids work correctly. [erpc/networks_static_responses.go:L45] - The response carries no upstream metadata:
Upstream()is nil andFromCache()is false. [erpc/networks_static_responses.go:L15] resultanderrorare mutually exclusive. Setting both or neither causes a startup validation error.- If
req.JsonRpcRequest()fails (malformed request), the function returns(nil, false)and the request falls through to normal handling — it is not aborted. [erpc/networks_static_responses.go:L23] - If
NewJsonRpcResponsefails during response construction (rare — only if result/error marshaling panics), the function logs an error ("failed to build static response") and returns(nil, false), falling through to normal handling. [erpc/networks_static_responses.go:L48] - OTel span attribute
static_response.hit=trueis set on theNetwork.Forwardspan on every hit. [erpc/networks.go:L978]
Best practices
- Match the exact param form your client sends. Hex strings are not normalized:
"0x0"and"0x00"are different. Run a quick test request and confirm the exact serialization before committing to config. - Use
params: [](empty array) to catch any params. There is no wildcard — the only way to match a method regardless of its params is an empty params array, which also matchesnulland omitted params. - Always set a non-zero error code. Error code
0passes startup validation but is silently dropped from the wire JSON due tojson:"code,omitempty". Clients will see nocodefield. Use standard JSON-RPC codes like-32602for invalid params. - Declaration order is priority order. If two entries could match the same request, put the more specific one first. The second entry is never evaluated once the first matches.
- Do not expect static responses to populate the cache. Matched requests bypass cache reads and writes entirely. If you also cache the same method, those entries come from real upstream responses, not your static config.
- Verify coverage with the Prometheus counter.
erpc_network_static_response_served_totalincrements on every match. If a counter you expect stays at zero, the params form in your config likely does not match what the client is sending.
Edge cases & gotchas
- Hex string case-sensitivity.
"0x0"and"0x00"are different param values — no hex normalization occurs. Match the exact form your clients send.common/static_response_test.go:L84(opens in a new tab) explicitly documents this. - YAML int vs JSON float64 tolerance. YAML
params: [0]storesint(0); inbound JSON storesfloat64(0).valueEqualnormalizes viatoFloat64— the match succeeds.common/static_response_test.go:L59(opens in a new tab) - nil and empty params are equivalent in matching. All three forms — omitted params key (nil),
params: [](non-nil empty), inbound"params":null— havelen == 0and match each other. The nil vs non-nil distinction only affects re-serialization viaomitempty. - Declaration order matters. If two entries share the same (method, params), only the first is served. The second is never evaluated.
- No params wildcard. There is no glob or
*support. To match a method regardless of params, add an entry withparams: []. - Static responses bypass the cache. A matched static response never reads from or writes to the cache. The cache is not populated even if caching is configured for the same method.
- Static responses bypass multiplexing. Matched requests never enter the mux leader/follower logic.
- Inbound id is always echoed. The
Paramsslice in config does not store an id. The id in the response comes fromreq.ID()at build time. String ids work correctly. - Exactly one of result/error enforced at startup. Setting both or neither causes a startup validation error.
result: nullYAML is treated as "result set" (non-nil interface). - Error code 0 omitted from wire JSON. Code
0passesValidate()butjson:"code,omitempty"causes it to be silently dropped during serialization. Clients see nocodefield. Always use a non-zero code.
Observability
| Metric | Type | Labels | When it fires |
|---|---|---|---|
erpc_network_static_response_served_total | counter | project, network, category | A request matched a static entry and was served without upstream contact; category = the JSON-RPC method name |
- OTel span attribute
static_response.hit=trueis set on theNetwork.Forwardspan on a hit. [erpc/networks.go:L978] - Debug log:
"served static response (no upstream contacted)"emitted with the matchedmethodfield. [erpc/networks_static_responses.go:L60-63] - Debug log on match-check failure:
"skipping static response: cannot inspect request". [erpc/networks_static_responses.go:L23] - Error log:
"failed to build static response"onNewJsonRpcResponsefailure (rare). [erpc/networks_static_responses.go:L48]
Source code entry points
erpc/networks_static_responses.go:L15-L63(opens in a new tab) —tryServeStaticResponse: params extraction, match call, response construction, metric emissioncommon/static_response.go:L14-L71(opens in a new tab) —FindStaticResponseMatch,paramsEqual,valueEqual,mapEqual,sliceEqual,toFloat64: pure matching logicerpc/networks.go:L973-L981(opens in a new tab) — call site inNetwork.Forward; placement relative to multiplexer and cachecommon/config.go:L2005-L2032(opens in a new tab) —StaticResponseConfig,StaticResponseBodyConfig,StaticResponseErrorConfigstruct definitionscommon/validation.go:L1270-L1300(opens in a new tab) —NetworkConfigiterates entries;StaticResponseConfig.Validate()enforces field requirementscommon/errors.go:L2225-L2231(opens in a new tab) —ErrJsonRpcExceptionExternal;Codefield usesjson:"code,omitempty"causing zero code to be dropped from wire JSONtelemetry/metrics.go:L419-L423(opens in a new tab) —MetricNetworkStaticResponseServedTotalcounter definitioncommon/static_response_test.go(opens in a new tab) — unit tests encoding hex case-sensitivity, numeric equivalence, nil/empty params, map ordering, validation ruleserpc/networks_static_responses_test.go(opens in a new tab) — integration tests: upstream not contacted on match, params fall-through, error-shaped stubs, string id echo
Related pages
- Networks —
staticResponsesis configured per network underprojects[].networks[]. - Cache — static responses fire before and bypass the cache entirely; they do not populate it.
- Failsafe — static matches skip failsafe processing; no retry or timeout applies.
- Rate limiters — matched static responses never consume upstream quota or rate-limit slots.