Run an RPC Node
To read Espresso settlement in your application, you run an RPC Node. To understand the role it plays, see Reading from Espresso.
What it does
The RPC Node (Espresso Rollup Node Proxy) is a Go service that sits between clients and a rollup's full node and enforces Espresso finality. It does not replace or modify the full node — it proxies JSON-RPC traffic in front of an unmodified node.
It has three parts:
Proxy — Intercepts every JSON-RPC request (including batch requests) before forwarding it to the full node. Any occurrence of the Espresso block tag (default
"espresso") in the request params is replaced with the current Espresso-finalized L2 block number, then the request is forwarded transparently.Verifier — A background loop that continuously compares data received from Espresso against the corresponding blocks on the full node. On a match it advances the finalized position and persists it; on a mismatch it retries at the next interval.
Store — Persists the Espresso-finalized L2 block number and the HotShot fallback height to a JSON file so state survives restarts. On restart the verifier resumes from both — the L2 block number is where verification left off, and the HotShot height is where to resume reading from Espresso.
The "espresso" tag always resolves to max(espresso finalized, eth finalized). Until the verifier confirms its first batch, the Espresso-finalized position has not advanced, so the tag returns that parent blockchain's finalized block; once batches start being confirmed, it reflects Espresso's faster finality whenever it is ahead. The Espresso-finalized block number is monotonically increasing — it never moves backwards across restarts, parent blockchain reorgs, or sequencer reorgs.
The proxy supports both OP Stack (--mode op) and Arbitrum Nitro / Orbit (--mode nitro) rollups. Clients integrate by pointing their RPC URL at the proxy and (optionally) using the "espresso" tag in their calls.
Prerequisites
The proxy runs in front of an existing, fully synced full node — it does not run one for you. Stand up the full node first, then point full_node_execution_rpc (and, for WebSockets, ws_full_node_execution_rpc) at it.
Arbitrum Nitro / Orbit: see Run a full node in the Arbitrum docs. Point
nitro.feed_urlat the full node's feed. You will need to enable and configure this on the full node.OP Stack: see Node operators overview in the Optimism docs. The proxy sits in front of
op-geth's JSON-RPC.
Architecture

Build
Prebuilt container images are published to ghcr.io/espressosystems/espresso-rollup-node-proxy.
Network endpoints and contracts
Two of the required settings depend on which Espresso network you point at: the query service URL (query_service_url, both modes) and the light client contract address (op.light_client_address, OP mode). The current values are published on the Networks page:
Network
Query service URL (query_service_url)
Light Client address (op.light_client_address)
Mainnet
0x95ca91cea73239b15e5d2e5a74d02d6b5e0ae458 (Ethereum)
Decaf Testnet
See the Networks page for the full, authoritative list of endpoints and contract addresses (including the Stake Table, ESP Token, Fee, and Reward Claim contracts).
Configuration
The proxy is configured via CLI flags or a JSON config file (or both — flags override file values). Pass --config <path> to load a file first, then apply any additional flags on top.
The examples below use placeholders such as <espresso-query-service-url> and <light-client-contract-address> — see Network endpoints and contracts above for the Mainnet and Decaf values to fill in. Replace <l2-chain-id> with your rollup's chain ID (see Finding the chain ID): namespace, eth_rpc, and query_service_url are now top-level settings required in both OP and Nitro modes. For <initial-hotshot-height>, see Initial HotShot height.
Example config file (OP Stack):
Example config file (Nitro):
Run with a config file:
Run with flags only (OP Stack):
Run with flags only (Nitro):
Example Config: ApeChain (Arbitrum Orbit)
ApeChain is a live Espresso-integrated Arbitrum Orbit chain (operated by Caldera), so it makes a useful concrete reference for filling in the Nitro placeholders above. The values below are public — the chain ID, Caldera's public RPC and sequencer feed, the Bridge contract on Arbitrum One, and the batch poster's signing key address:
A few notes for adapting this to your own chain:
namespaceis the chain's own chain ID (ApeChain Mainnet is33139).full_node_execution_rpcis the full node's EVM JSON-RPC endpoint — the same endpoint clients would otherwise query directly (the proxy sits in front of it). ApeChain documents how to run your own node at docs.apechain.com/run-node.eth_rpcmust be your own Arbitrum One Mainnet RPC endpoint (ApeChain's parent chain) — point it at whichever provider or node you run.feed_urlis the message feed exposed by the Nitro full node you run in front of — point it at that node's feed, not at a separate service.bridge_addressis the chain's Bridge contract on Arbitrum One, available from the chain's public documentation. ApeChain publishes its contract addresses at docs.apechain.com.valid_signing_key_addressesis either the chain's batch poster address or its sequencer signing key address, depending on the deployment. If you're unsure which applies to your chain, check with the Espresso team.initial_hotshot_heightis only used on the very first run, before a state file exists. The value shown is the height this deployment started streaming from — choose your own per Initial HotShot height.eth_log_scan_block_rangeis optional and defaults to10000. It caps how many Ethereum blocks the verifier scans pereth_getLogsquery when fetching delayed messages — lower it if your Arbitrum One RPC provider rejects wide log ranges.Optional settings —
listen_addr,espresso_tag,store_file_path, andverification_intervalare all optional; they appear above only to show concrete example values (verification_intervalhere is50msrather than the10msdefault). Anything you omit falls back to its default. This example also runs HTTP-only — the WebSocket proxy stays off unless you add bothws_listen_addrandws_full_node_execution_rpc(see WebSockets). For every optional setting and its default, see the Optional reference table.
Docker
Run with a config file:
Mount a volume for --store-file-path so the persisted state survives container restarts.
Example docker-compose.yml (OP Stack):
Clients should point at the proxy (http://localhost:8080) rather than directly at the full node.
Configuration reference
The Required column shows ✅ for settings required in all modes, and ✅ OP / ✅ Nitro for settings required only in that mode. Everything else is optional and falls back to the listed default.
Required
--mode
mode
✅
Verifier mode: op or nitro
--namespace
namespace
✅
Espresso namespace; must equal the chain's own chain ID
--full-node-execution-rpc
full_node_execution_rpc
✅
Rollup execution layer RPC URL (the full node the proxy sits in front of)
--eth-rpc
eth_rpc
✅
Ethereum / parent-chain RPC URL
--query-service-url
query_service_url
✅
Espresso query service URL
--op.light-client-address
op.light_client_address
✅ OP
Espresso light client contract address on Ethereum
--op.batcher-address
op.batcher_address
✅ OP
OP batcher address
--op.batch-authenticator-address
op.batch_authenticator_address
✅ OP
Batch Authenticator contract address on Ethereum
--nitro.feed-url
nitro.feed_url
✅ Nitro
Nitro full node feed WebSocket URL
--nitro.bridge-address
nitro.bridge_address
✅ Nitro
Nitro Bridge contract address on Ethereum
--nitro.valid-signing-key-addresses
nitro.valid_signing_key_addresses
✅ Nitro
Valid signing key addresses (at least one)
Optional
--listen-addr
listen_addr
:8080
Address the proxy listens on
--ws.listen-addr
ws_listen_addr
—
WebSocket listen address; set together with ws.full-node-execution-rpc to enable the WebSocket proxy
--ws.full-node-execution-rpc
ws_full_node_execution_rpc
—
Execution layer WebSocket RPC URL; required to enable the WebSocket proxy
--espresso-tag
espresso_tag
espresso
JSON-RPC block tag to intercept; set to finalized to back the standard finality tag with Espresso
--store-file-path
store_file_path
espresso_store.json
Path to the state persistence file
--verification-interval
verification_interval
10ms
How often the verifier polls for new confirmed batches
--finality-poll-interval
finality_poll_interval
1s
How often the finality poller queries the full node for the latest finalized block
--initial-hotshot-height
initial_hotshot_height
0
HotShot height to start streaming from; used only on first run, when no state file exists. Must be non-zero on a fresh start — the proxy exits otherwise (see Initial HotShot height)
--max-batch-size
max_batch_size
1000
Maximum requests in a JSON-RPC batch (0 = unlimited)
--max-request-body-size
max_request_body_size
5242880
Maximum request body size in bytes (0 = unlimited)
--log-level
log_level
info
Log level (debug, info, warn, error)
--log-format
log_format
json
Log output format (text or json)
--track-batch-latency
track_batch_latency
false
Log per-batch and average latency from HotShot finalization to verification
--nitro.wait-for-eth-finality
nitro.wait_for_eth_finality
false
Wait for Ethereum block finalization before fetching delayed messages
--nitro.eth-log-scan-block-range
nitro.eth_log_scan_block_range
10000
Max Ethereum blocks scanned per eth_getLogs query when fetching delayed messages; lower it for RPC providers that cap log ranges (0 uses the default)
Finding the chain ID
The namespace setting must equal the chains own chain Id. If you don't already know it, query the full node's EVM JSON-RPC endpoint (the same URL you set as full_node_execution_rpc) with eth_chainId:
The result is hex-encoded, so convert it to decimal for the namespace value — for example "0x8173" (33139) is ApeChain Mainnet.
If you have Foundry's cast installed, it returns the decimal chain ID directly:
Initial HotShot height
initial_hotshot_height (--initial-hotshot-height) sets the HotShot block height the verifier starts streaming from. It is only used on the first run, when no state file exists. Once the proxy has written its state file (store_file_path), every restart resumes from the persisted position and this setting is ignored — changing it later has no effect unless you delete the state file.
On a fresh start (no state file) you must set a non-zero value — the proxy refuses to start with initial_hotshot_height at its 0 default, because streaming the entire Espresso blockchain from genesis is not intended and would take a very long time.
Set it to a HotShot height whose block timestamp is before the current Ethereum-finalized L2 block's timestamp. Starting from before that point ensures the verifier streams forward without leaving a gap between the Espresso-confirmed data it reads and what the full node already treats as finalized. Until the verifier catches up, the "espresso" tag safely falls back to Ethereum-finalized state through the max(espresso finalized, eth finalized) resolution, so erring earlier is harmless.
To find the current HotShot block height, query the status/block-height endpoint of your query_service_url:
(Swap in the Mainnet query service URL from Network endpoints and contracts as needed.)
WebSockets
To enable the WebSocket proxy, two optional settings must both be present — the WebSocket listening address and the execution WebSocket RPC URL:
--ws.listen-addr
ws_listen_addr
--ws.full-node-execution-rpc
ws_full_node_execution_rpc
If one or both are missing, the WebSocket port is not enabled. The startup logs indicate whether the WebSocket proxy is active or why it was skipped.
Verify the proxy is running
The proxy forwards traffic transparently to the full node from the moment it starts. The "espresso" tag resolves to max(espresso finalized, eth finalized), so until the verifier confirms its first batch it returns the chain's Ethereum-finalized block; once batches start being confirmed, it reflects Espresso's faster finality whenever it is ahead. The startup logs report the listening addresses, verifier progress, and whether the WebSocket proxy is active.
Point the examples below at your proxy by setting its URL once — use the proxy's listen address, which defaults to http://localhost:8080:
To confirm the proxy is up and reachable, query it the same way you would the underlying full node — for example, fetch the latest block:
Once the verifier has confirmed its first batch, the "espresso" tag begins tracking the Espresso-finalized L2 block (before that it returns the Ethereum-finalized block). The value is monotonically increasing — re-running this query over time should show it advance and never move backwards:
If the "espresso" tag returns an error or the same block indefinitely, check that the verifier is making progress in the logs and that --query-service-url and the mode-specific settings (--op.* / --nitro.*) are correct.
Using the proxy from a client
Clients integrate by pointing their RPC URL at the proxy (its listen address, e.g. http://localhost:8080) instead of directly at the full node. No other code changes are required — the proxy speaks the standard JSON-RPC API and forwards everything transparently.
To opt into Espresso finality, use the "espresso" block tag anywhere a client would normally pass a block tag such as "latest" or "finalized":
The proxy resolves the tag to max(espresso finalized, eth finalized), so clients always get Espresso's faster finality when it is ahead while safely falling back to Ethereum finality.
If you want existing clients to benefit without any code changes, run the proxy with --espresso-tag finalized. The proxy then backs the standard "finalized" tag with Espresso finality, so anything already requesting "finalized" transparently gets faster, Espresso-backed finality.
Troubleshooting
Common symptoms and what to check. For how the proxy behaves under outages, verification mismatches, reorgs, and force inclusion, see Edge cases and failure behavior.
Proxy exits at startup with "no state file found and
--initial-hotshot-heightis 0". On a fresh start you must set a non-zeroinitial_hotshot_height— see Initial HotShot height. It is ignored once a state file exists.Proxy exits at startup with "invalid configuration". A required setting is missing or malformed; each offending field is logged on its own line. Check the Required settings for your
mode.Proxy exits at startup in OP mode while setting up the light client. Confirm
eth_rpcis reachable andop.light_client_addressis correct for your network (see Network endpoints and contracts).The
"espresso"tag returns an error or never advances. The verifier isn't making progress. At the defaultinfolog level a healthy verifier emits an advance line each time it moves the position forward —Successfully verified and advanced OP batches(OP) orsuccessfully verified and advanced nitro messages(Nitro), each including the newhotshot_height. If you don't see these recurring, check the logs for verifier errors and confirmquery_service_urland the mode-specific settings (op.*/nitro.*) are correct.The
"espresso"tag is stuck at the Ethereum-finalized block. Expected before the verifier confirms its first batch, or while Espresso is unreachable — the tag falls back to Ethereum finality and resumes once the verifier catches up. On startup you should seeStarting OP Verifier/Starting Nitro Verifier(logging the position it resumed from), followed by the advance line above appearing repeatedly as it catches up.The WebSocket endpoint isn't reachable. Both
ws_listen_addrandws_full_node_execution_rpcmust be set; otherwise the WebSocket proxy is skipped. The startup logs say whether it is active or why it was skipped (see WebSockets). The WebSocket endpoint must also be enabled on the full node itself —ws_full_node_execution_rpchas to point at a node that actually serves WebSockets.Nitro: errors fetching delayed messages, or the Eth RPC endpoint rejects wide
eth_getLogsranges. Lowernitro.eth_log_scan_block_rangebelow your RPC provider's log-range cap.
Restart and crash recovery
All verifier progress lives in store_file_path (the Espresso-finalized block number and the HotShot fallback position). On restart the proxy loads this file and resumes from the persisted position, so a crash or restart never replays already-verified work and never moves the tag backwards. Remember that initial_hotshot_height is honored only on the very first run, when no state file exists — after that it is ignored (see Initial HotShot height).
Last updated

