Examples
End-to-end walkthroughs: run a node, deposit/withdraw, use precompiles, and inspect protocol state.
Third-party documentation. This is independently authored analysis of the public Arbitrum Nitro codebase — not the official docs, and not reviewed or endorsed by the Arbitrum Nitro team.
Examples
The patterns below cover the two things almost every Arbitrum builder does: move assets across the bridge, and inspect the L2's L1-aware state. They use the @arbitrum/sdk and ethers v6, and assume an L1 and L2 RPC URL.
Bridging ETH and ERC-20s
The bridge objects collapse the multi-step flows (approval, inbox submission, retryable execution, outbox redemption) into a few awaited calls. The examples in this module's manifest show the deposit, the direct retryable, and the withdrawal; all three use the same provider/signer conventions.
Running nodes and inspecting state
For infrastructure work, the single nitro-node binary and its env-var configuration make it straightforward to run a full node or a devnet. Combined with the arb_* RPC extensions and the precompiles, it's possible to observe exactly what the sequencer and validators are doing in real time.
A note on precompiles
Several "L2-native" behaviors (reading the L2 block number anchored to L1, estimating L1 data fees, sending to L1) are one call to a precompile away. They are the closest thing Arbitrum has to a system API, and they are stable across versions — worth learning even if the SDK handles most high-level flows.
Examples
Run a Nitro full node
# clone and buildgit clone https://github.com/OffchainLabs/nitro.gitcd nitrodocker build . -t nitro-node# configure via env vars (single binary = sequencer/validator/relayer)docker run --rm -p 8547:8547 -p 8548:8548 \-e CHAIN_ID=42161 \-e L1_URL=https://ethereum-rpc.publicnode.com \-e L2_URL=http://arb1.arbitrum.io/rpc \-e STEPS_PER_LEVEL=3 \-e VALIDATOR__ENABLE=false \-e SEQUENCER__ENABLE=false \nitro-node --parent-chain.connection.url https://ethereum-rpc.publicnode.com# node exposes eth JSON-RPC on 8547 and feed on 8548
One binary runs all roles; you enable/disable them with env flags. A full node needs no validator/sequencer roles.
Read L2 block data with eth_getBlockByNumber
const l2 = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");const block = await l2.getBlock("latest");console.log("number:", block.number);console.log("timestamp:", block.timestamp);// Arbitrum L2s use a l1BlockNumber field; every L2 block is anchored to L1.const raw = await l2.send("eth_getBlockByNumber", ["latest", false]);console.log("l1BlockNumber:", raw.l1BlockNumber);
Nitro exposes an l1BlockNumber per block, useful for timestamp/sequencing reasoning.
Query the ArbGasInfo precompile
const ARB_GAS_INFO = "0x000000000000000000000000000000000000006c";const abi = ["function getPricesInWei() view returns (uint256, uint256, uint256, uint256, uint256, uint256)"];const l2 = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");const gasInfo = new ethers.Contract(ARB_GAS_INFO, abi, l2);const [l1Price, , , , , ] = await gasInfo.getPricesInWei();console.log("L1 data price (wei):", l1Price);// estimate the L1 cost of a call:const calldata = "0x1234";const l1Fee = await gasInfo.getL1FeesApproximate(calldata, 1n, 0n, 0n, 0n, 0n);console.log("approx L1 fee:", l1Fee.toString());
Arbitrum separates L2 execution gas from L1 data gas; ArbGasInfo exposes the underlying prices.
Edge Cases
- When running a node, never set both SEQUENCER__ENABLE and VALIDATOR__ENABLE unless you know what you're doing — the two roles conflict over block production.
- The feed (8548) and RPC (8547) use different ports; tools that need the fastest updates subscribe to the feed, not the RPC.
- A validator that enables staking starts posting assertions and can be slashed for faulty ones — run validators read-only (enable=false) until you understand the challenge protocol.
