Skip to main content
Base transactions have two independent fee components that agents must compute correctly to avoid failed or stuck transactions:
ComponentWhat it pays forVaries with
L2 execution feeExecuting opcodes on the L2 sequencerL2 network congestion
L1 data availability feePublishing transaction data to EthereumEthereum L1 base fee + blob fee market
Agents that only set L2 fees will underestimate total transaction cost, particularly during Ethereum congestion when the L1 component can exceed the L2 component.

L2 Fee Estimation

Base uses EIP-1559. Every transaction requires:
  • maxFeePerGas — the maximum total fee per gas unit you will pay (base fee + priority fee)
  • maxPriorityFeePerGas — the tip paid to the sequencer on top of the base fee

Step 1 — Fetch priority fee history

Use eth_feeHistory to compute priority fee percentiles from recent blocks:
Request
{
  "jsonrpc": "2.0",
  "method": "eth_feeHistory",
  "params": [10, "latest", [10, 50, 90]],
  "id": 1
}
Response (abbreviated)
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "baseFeePerGas": [
      "0x4a817c800",
      "0x4b5765e00",
      "0x4c6b4e000"
    ],
    "gasUsedRatio": [0.12, 0.45, 0.89],
    "reward": [
      ["0x3b9aca00", "0x77359400", "0xee6b2800"],
      ["0x3b9aca00", "0x77359400", "0x12a05f200"],
      ["0x59682f00", "0xee6b2800", "0x1dcd65000"]
    ]
  }
}
Each inner array in reward contains [10th, 50th, 90th] percentile priority fees (in wei) for that block. baseFeePerGas includes one extra entry — the next block’s predicted base fee.

Step 2 — Choose a priority fee

Select from the 90th-percentile column based on your urgency:
ScenariomaxPriorityFeePerGas
Standard (next 1–3 Flashblocks)median of last 10 blocks’ 50th percentile rewards
Fast (next Flashblock)median of last 10 blocks’ 90th percentile rewards
Urgent (current Flashblock, if still open)max of last 3 blocks’ 90th percentile rewards
function computePriorityFee(
  rewards: bigint[][],
  urgency: 'standard' | 'fast' | 'urgent'
): bigint {
  const percentileIdx = urgency === 'standard' ? 1 : 2; // 50th or 90th
  const window = urgency === 'urgent' ? rewards.slice(-3) : rewards;
  const fees = window.map(r => r[percentileIdx]).sort((a, b) => (a < b ? -1 : 1));
  return urgency === 'urgent'
    ? fees[fees.length - 1]                    // max of window
    : fees[Math.floor(fees.length / 2)];       // median of window
}

Step 3 — Set maxFeePerGas

Add a buffer to the next block’s base fee to absorb fee increases across multiple Flashblocks:
// nextBaseFee is baseFeePerGas[last] from eth_feeHistory
const buffer = 2n; // 2x buffer absorbs ~18 Flashblocks of 4% increases
const maxFeePerGas = nextBaseFee * buffer + maxPriorityFeePerGas;

Current EIP-1559 parameters

ParameterValueEffect
Minimum base fee5,000,000 wei (0.005 gwei)Floor; base fee never drops below this
Max base fee increase per block4%At max congestion, base fee doubles every ~36 seconds
Block time2 secondsBase fee update frequency
Elasticity multiplierBlocks can absorb 6× target gas before max fee increase

L1 Data Availability Fee Estimation

The L1 fee is calculated from your serialized transaction’s byte length and current Ethereum fees. Query the GasPriceOracle predeployment to estimate it before signing. GasPriceOracle address (same on all networks): 0x420000000000000000000000000000000000000F

Key methods

MethodReturnsUse
getL1Fee(bytes)WeiExact L1 fee estimate for a serialized (RLP-encoded) transaction
getL1FeeUpperBound(uint256 unsignedTxSize)WeiUpper bound estimate without full serialization
l1BaseFee()WeiCurrent Ethereum L1 base fee as seen by Base
blobBaseFee()WeiCurrent EIP-4844 blob base fee
baseFeeScalar()uint32Scalar applied to L1 base fee component
blobBaseFeeScalar()uint32Scalar applied to blob base fee component

Querying with eth_call

{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "params": [
    {
      "to": "0x420000000000000000000000000000000000000F",
      "data": "0x49948e0e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c002f87082849a82520894d3cda913deb6f4967b2ef66ae97de114a83bcc01880de0b6b3a764000080c001a0..."
    },
    "latest"
  ],
  "id": 1
}
The data field is the ABI-encoded call to getL1Fee(bytes) where bytes is your RLP-encoded unsigned transaction.

Simpler upper-bound approach

For a quick upper-bound estimate before you have the full signed transaction:
import { createPublicClient, http, parseAbi } from 'viem';
import { base } from 'viem/chains';

const client = createPublicClient({ chain: base, transport: http() });

const GAS_PRICE_ORACLE = '0x420000000000000000000000000000000000000F';
const abi = parseAbi(['function getL1FeeUpperBound(uint256 unsignedTxSize) view returns (uint256)']);

// Estimate tx size: roughly (calldata bytes) + 100 bytes overhead
const estimatedTxSize = BigInt(calldata.length / 2 + 100);
const l1FeeUpperBound = await client.readContract({
  address: GAS_PRICE_ORACLE,
  abi,
  functionName: 'getL1FeeUpperBound',
  args: [estimatedTxSize],
});

Agent Decision Logic

Should this trade execute now?

totalFeeEstimate = (gasLimit * maxFeePerGas) + l1FeeUpperBound

l1FeeRatio = l1FeeUpperBound / totalFeeEstimate

if l1FeeRatio > 0.8 AND tradeValueUSD < threshold:
    → DEFER: L1 fees dominate; wait for lower Ethereum congestion
    → Re-check after 60 seconds

if baseFee > normalBaseFee * 3:
    → HIGH CONGESTION: use 90th percentile priority fee, set 3x baseFee buffer
    → Consider splitting large trades into smaller batches

else:
    → PROCEED with standard fee settings

Detecting Ethereum congestion

const l1BaseFee = await client.readContract({
  address: GAS_PRICE_ORACLE,
  abi: parseAbi(['function l1BaseFee() view returns (uint256)']),
  functionName: 'l1BaseFee',
});

// Base's "normal" L1 base fee threshold — adjust based on observed history
const NORMAL_L1_BASE_FEE = 10_000_000_000n; // 10 gwei

const isHighL1Congestion = l1BaseFee > NORMAL_L1_BASE_FEE * 3n;

Flashblocks gas limit consideration

Transactions with gas limits exceeding 1/10 of the block gas limit (currently 14 million gas) cannot land in the first Flashblock and must wait for a later one with sufficient gas headroom. A transaction requiring 20M gas must wait for at least Flashblock 2.
If an agent’s transaction exceeds 14M gas and timing is critical, consider splitting the operation across multiple smaller transactions. Each can land in separate Flashblocks within the same 2-second block window.

Complete Fee Computation Example

import { createPublicClient, http, parseAbi, toHex } from 'viem';
import { base } from 'viem/chains';

const client = createPublicClient({ chain: base, transport: http() });
const GAS_PRICE_ORACLE = '0x420000000000000000000000000000000000000F';

async function computeOptimalFees(urgency: 'standard' | 'fast' | 'urgent', calldataBytes: number) {
  // 1. Fetch fee history
  const feeHistory = await client.getFeeHistory({
    blockCount: 10,
    rewardPercentiles: [10, 50, 90],
  });

  // 2. Compute priority fee
  const percentileIdx = urgency === 'standard' ? 1 : 2;
  const window = urgency === 'urgent'
    ? feeHistory.reward!.slice(-3)
    : feeHistory.reward!;
  const fees = window.map(r => r[percentileIdx]).sort((a, b) => (a < b ? -1 : 1));
  const maxPriorityFeePerGas = urgency === 'urgent'
    ? fees[fees.length - 1]
    : fees[Math.floor(fees.length / 2)];

  // 3. Compute maxFeePerGas with 2x base fee buffer
  const nextBaseFee = feeHistory.baseFeePerGas[feeHistory.baseFeePerGas.length - 1]!;
  const maxFeePerGas = nextBaseFee * 2n + maxPriorityFeePerGas;

  // 4. Estimate L1 fee
  const oracleAbi = parseAbi(['function getL1FeeUpperBound(uint256) view returns (uint256)']);
  const estimatedTxSize = BigInt(calldataBytes + 100);
  const l1FeeUpperBound = await client.readContract({
    address: GAS_PRICE_ORACLE,
    abi: oracleAbi,
    functionName: 'getL1FeeUpperBound',
    args: [estimatedTxSize],
  });

  return { maxFeePerGas, maxPriorityFeePerGas, l1FeeUpperBound, nextBaseFee };
}