Sizing a low-gas balance threshold for relayers on Base, Arbitrum and OP Mainnet

By Chainitor · Published

A relayer that runs out of gas doesn’t revert. Its transactions are rejected before they reach a block, so nothing shows up on an explorer and nothing appears in your contract logs. Users just see actions that never land. A low-balance alert prevents this, but only if the threshold leaves enough time to react. Set it too low and the alert arrives after the wallet is empty. Set it too high and it fires every day until people learn to ignore it.

This guide explains what a transaction actually costs on the three largest Ethereum L2s, how to measure what your relayer really burns, and a simple method for turning that into a floor. The method applies to any wallet that pays gas automatically: relayers, keepers, bots and deployers.

What an L2 transaction costs

On all three chains the gas token is ETH, but the bill has two parts: executing the transaction on the L2, and publishing its data to Ethereum. The chains differ in how they charge the second part.

Base and OP Mainnet (OP Stack)

Both chains run the OP Stack. The OP Stack fee documentation and Base’s fee documentation give the total as:

totalFee = gasUsed × (baseFee + priorityFee) + l1Fee + operatorFee
  • Execution fee: gasUsed × (baseFee + priorityFee), as on Ethereum. Base documents a minimum base fee of 0.005 gwei, and says the base fee usually sits at or near that minimum and rises under congestion.
  • L1 data fee: the cost of posting the transaction’s compressed bytes to Ethereum. It is priced from Ethereum’s base fee and blob base fee, scaled by per-chain parameters. It is charged automatically and deducted from the sender. It doesn’t appear in gasUsed and isn’t controlled by your maxFeePerGas.
  • Operator fee: a newer, optional component. getOperatorFee(gasUsed) on the GasPriceOracle predeploy returned 0 on both Base and OP Mainnet on 12 November 2026. Keep it in your formula anyway, in case that changes.

Receipts on both chains carry the L1 part in extra fields (l1Fee, l1GasUsed, l1GasPrice, l1BlobBaseFee). You can estimate it before sending through the GasPriceOracle predeploy at 0x420000000000000000000000000000000000000F:

# Upper-bound L1 fee, in wei, for a ~200-byte transaction
cast call 0x420000000000000000000000000000000000000F \
  "getL1FeeUpperBound(uint256)(uint256)" 200 -r https://mainnet.base.org

Which part dominates depends on the chain and the moment. Don’t assume a split. Measure it on your own transactions.

Arbitrum One

Arbitrum charges the parent-chain (L1) cost differently. It converts the estimated cost of posting the transaction into extra L2 gas units, according to Arbitrum’s gas and fees documentation. The receipt’s gasUsed already includes them, and gasUsedForL1 shows how many there were. So gasUsed × effectiveGasPrice is the whole fee. A consequence is that the L1 part counts against your gas limit, so a gas limit hard-coded from an Ethereum estimate can run out on Arbitrum. To estimate before sending, the NodeInterface virtual contract at 0xc8 exposes gasEstimateComponents, which splits out the L1 part.

The L2 base fee has a protocol minimum that you can read from ArbGasInfo:

cast call 0x000000000000000000000000000000000000006C \
  "getMinimumGasPrice()(uint256)" -r https://arb1.arbitrum.io/rpc
# 20000000 (0.02 gwei) on 12 November 2026

The common factor: Ethereum

All three chains price their L1 component from Ethereum’s fee market, so a congestion event on Ethereum raises your costs on every L2 at the same time. If one treasury tops up relayers on several chains, size it for all of them drawing down together.

Measure the burn rate, don’t estimate it

A threshold is only as good as its burn-rate input, and real costs are easier to measure than to model. There are two ways.

From receipts. Your relayer already knows the hashes it sent. Sum the fees from the raw receipts. ethers’ TransactionReceipt drops chain-specific fields such as l1Fee, so ask the node directly:

import { ethers } from 'ethers'; // ethers v6

// Total wei a transaction cost its sender on Base, OP Mainnet or Arbitrum One.
export async function feePaid(provider, txHash) {
  const r = await provider.send('eth_getTransactionReceipt', [txHash]);
  const execution = BigInt(r.gasUsed) * BigInt(r.effectiveGasPrice); // Arbitrum: includes the L1 part
  const l1 = r.l1Fee ? BigInt(r.l1Fee) : 0n;                          // OP Stack: separate field
  return execution + l1; // add the operator fee here if your chain starts charging one
}

From balances. If you don’t have the hashes, compare the wallet’s balance a day apart and add back any top-ups it received in between:

RPC=https://mainnet.base.org
RELAYER=0xYourRelayer
THEN=$(cast find-block $(( $(date +%s) - 86400 )) -r $RPC)   # block from 24 h ago
cast balance $RELAYER --block $THEN -r $RPC --ether
cast balance $RELAYER -r $RPC --ether

cast find-block resolves the block from a timestamp. That matters on Base, which moved from 2-second to 200 ms blocks on 5 November 2026. Any “last N blocks” window you used before then now covers a tenth of the time. Reading a balance at an old block also requires a node that still serves that historical state. Some public endpoints refuse anything that isn’t recent.

Collect at least a week of data, more if your traffic is weekly or campaign-driven. Keep the busiest hour as well as the daily total, because the threshold has to survive your worst hour, not your average day.

Turning burn rate into a threshold

The floor is the gas the wallet will burn between crossing the threshold and being refilled, plus headroom:

floor = peak burn rate × (lead time + check interval) × spike factor
  • Peak burn rate: ETH per hour during your busiest measured hour.
  • Lead time: how long it takes, in the worst realistic case, from an alert firing to funds arriving. That covers someone seeing it outside working hours, getting treasury approval (perhaps several multisig signatures) and bridging or sending the ETH. Ask the people who do it. It’s usually hours, not minutes.
  • Check interval: how often the balance is read. The wallet keeps burning between checks.
  • Spike factor: headroom for fee spikes, mostly from the Ethereum-priced L1 component. Set it from the worst ratio in your own measurements between a busy day’s average cost per transaction and a normal day’s. If you have no history yet, 2 is a reasonable starting point to revisit.

Worked example

These inputs are illustrative. Replace them with your own measurements.

InputValue
Peak hourly burn (busiest hour in a week of receipts)0.003 ETH
Lead time (night-time page, two of three treasury signers, a transfer on Base)4 hours
Check interval5 minutes (0.083 h)
Spike factor2
floor = 0.003 × (4 + 0.083) × 2 = 0.0245 ETH  →  round up to 0.025 ETH

Three refinements:

  • Cover the next transaction up front. A node rejects a transaction unless the balance covers gasLimit × maxFeePerGas plus value, and the OP Stack L1 fee comes on top. The relayer stalls once the balance drops below that, even if it isn’t technically empty. That’s usually far below the floor, but check it if you send large transactions or set generous maxFeePerGas caps.
  • Use two levels. A warning floor that goes to a chat channel during working hours, and a lower critical floor (lead time = the fastest possible refill) that pages someone. These are two monitors with different thresholds and channels.
  • Refill well above the floor. Topping up to just over the threshold means another alert tomorrow. Refill to the floor plus several days of normal burn, so alerts mean something.

Re-measure after anything that changes cost per transaction: a contract upgrade, a new transaction type, or a chain upgrade like Base’s.

Setting it up in Chainitor

Chainitor’s native balance monitor reads the wallet’s ETH balance on each check and alerts once when it falls below your threshold, then sends a recovery notice when the wallet is refilled. Create one monitor per wallet per chain, since the same address has a separate balance on Base, Arbitrum and OP Mainnet. The low gas balance alerts page walks through the setup.

Balances are checked every 5 minutes on Starter and every minute on Pro, which is the check interval in the formula above. Every alert channel, email, Slack, Telegram, PagerDuty and webhook, is on every plan. A webhook can trigger your own refill job. Chainitor never holds keys or sends transactions, so the refill itself stays with you.

Put a floor under every relayer

A native balance monitor per wallet and chain, with your threshold, an alert when it is crossed and a recovery notice when it is refilled. Balance monitors are on every plan.

Start monitoring free

7-day free trial • No immediate charge