eth_gasPrice vs baseFeePerGas: Which to Use on L2s

·10 min read

You need to estimate how much a transaction will cost on Base (or any L2). You call the standard method to get the current gas price. It returns 0.12 gwei.

You multiply: 400,000 gas * 0.12 gwei = $0.10.

Then you check what actual transactions are paying on the block explorer. They're paying 0.002 gwei. Your estimate was 60x too high.

Why does the "get gas price" method return a value so different from what transactions actually pay?

Table of Contents

  1. Quick Background: Gas and Gas Price
  2. How You Get Gas Price in Code
  3. The Problem: eth_gasPrice Returns Inflated Values
  4. What eth_gasPrice Actually Returns
  5. The Fix: Use block.baseFeePerGas
  6. The Other Half: L1 Data Fees
  7. Real Example: Liquidation Bot
  8. Takeaways

Quick Background: Gas and Gas Price

Every transaction on Ethereum (and L2s like Base, Optimism, Arbitrum) costs "gas." Gas is a unit measuring computational work.

  • A simple ETH transfer costs ~21,000 gas
  • A complex smart contract call might cost 200,000-500,000 gas

Gas price is how much you pay per unit of gas, measured in gwei (1 gwei = 0.000000001 ETH).

Transaction cost = gas used × gas price

If your transaction uses 400,000 gas at 0.01 gwei:

400,000 × 0.01 gwei = 4,000 gwei = 0.000004 ETH ≈ $0.008 at $2,000/ETH

To estimate costs before sending a transaction, you need to know the current gas price. That's where eth_gasPrice comes in.

How You Get Gas Price in Code

Every Ethereum node exposes an RPC method called eth_gasPrice. You call it, it returns the current gas price.

Direct RPC call:

curl -X POST https://mainnet.base.org \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":1}'

# Response: {"jsonrpc":"2.0","id":1,"result":"0x1bf08eb00"}
# That hex = 7,500,000,000 wei = 7.5 gwei

In JavaScript/TypeScript with ethers.js:

import { ethers } from 'ethers';

// Connect to Base
const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');

// Get fee data (wraps multiple RPC calls including eth_gasPrice)
const feeData = await provider.getFeeData();

console.log(feeData.gasPrice);      // The value from eth_gasPrice
console.log(feeData.maxFeePerGas);  // For EIP-1559 transactions
console.log(feeData.maxPriorityFeePerGas); // Priority fee (tip)

provider.getFeeData() is an ethers.js convenience method. Under the hood, it calls eth_gasPrice and other RPC methods, then returns them in a structured object.

Most developers use feeData.gasPrice to estimate transaction costs:

const feeData = await provider.getFeeData();
const gasEstimate = 400000n; // Expected gas for your transaction
const estimatedCost = gasEstimate * feeData.gasPrice;

This works fine on Ethereum mainnet. On L2s, it breaks.

The Problem: eth_gasPrice Returns Inflated Values

Here's what happens on Base:

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const feeData = await provider.getFeeData();

console.log(`eth_gasPrice: ${Number(feeData.gasPrice) / 1e9} gwei`);
// Output: eth_gasPrice: 0.1175 gwei

The RPC says 0.1175 gwei. But if you look at actual transactions on Basescan, they're paying 0.001-0.003 gwei.

Why the 50-100x difference?

What eth_gasPrice Actually Returns

Before 2021, Ethereum used a simple auction: you bid a gas price, miners picked the highest bids. eth_gasPrice returned a reasonable bid based on recent transactions.

Then came EIP-1559, which changed how fees work:

Old model: You set one gas price. That's what you pay.

EIP-1559 model: Two components:

  1. Base fee - Set by the protocol based on network congestion. Everyone pays this. It gets burned.
  2. Priority fee (tip) - Optional extra you pay to incentivize faster inclusion.

The actual price you pay = base fee + priority fee.

Here's the problem: after EIP-1559, eth_gasPrice doesn't return the base fee. It returns:

eth_gasPrice = last block's base fee + suggested priority fee

On OP Stack chains (Base, Optimism), the node's priority fee suggestion defaults to a fixed floor of 0.1 gwei when blocks have room, which they almost always do. It's not an average of recent tips. It's a hardcoded minimum in op-geth's gas price oracle.

On Ethereum mainnet, this is fine. Priority fees matter because blocks fill up. You need to tip to get included.

On L2s like Base? Blocks rarely fill up. Priority fees are nearly zero. BaseScan's gas tracker shows actual priority fees at 0 gwei. Transactions get included immediately with minimal or zero tip.

But eth_gasPrice still adds its suggested priority fee to the response. On an L2 where the base fee is 0.002 gwei and the suggested priority fee is the 0.1 gwei floor:

eth_gasPrice = 0.002 + 0.1 = 0.102 gwei (50x inflated)

The actual L2 execution cost is 0.002 gwei. The reported price is 0.102 gwei.

The Fix: Use block.baseFeePerGas

Every block has a baseFeePerGas field. This is the actual minimum gas price for that block, set by the protocol.

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');

// Get the latest block
const block = await provider.getBlock('latest');

console.log(`baseFeePerGas: ${Number(block.baseFeePerGas) / 1e9} gwei`);
// Output: baseFeePerGas: 0.002 gwei

Compare both values:

const feeData = await provider.getFeeData();
const block = await provider.getBlock('latest');

console.log(`eth_gasPrice: ${Number(feeData.gasPrice) / 1e9} gwei`);
console.log(`baseFeePerGas: ${Number(block.baseFeePerGas) / 1e9} gwei`);

// Output:
// eth_gasPrice: 0.1175 gwei (inflated recommendation)
// baseFeePerGas: 0.002 gwei (actual minimum)

For the L2 execution component of gas cost, use block.baseFeePerGas:

const block = await provider.getBlock('latest');
const gasEstimate = 400000n;
const l2ExecutionCost = gasEstimate * (block.baseFeePerGas ?? 0n);

But this is only half the picture on OP Stack L2s.

The Other Half: L1 Data Fees

On OP Stack chains (Base, Optimism, Zora, Mode), every transaction has two fee components:

  1. L2 execution fee = gas used x baseFeePerGas (what we just covered)
  2. L1 data fee = cost of posting your transaction's data to Ethereum L1 for security

The Base docs state: "Every Base transaction consists of two costs: an L2 (execution) fee and an L1 (security) fee." And: "Typically the L1 security fee is higher than the L2 execution fee."

That second line is important. For many transactions, the L1 data fee is the larger component. Using block.baseFeePerGas alone still underestimates the total cost because it only covers the L2 execution portion.

How the L1 Data Fee Works

Your transaction's calldata gets posted to Ethereum L1 (as blob data, since the EIP-4844 Dencun upgrade). The L1 data fee depends on:

  • Your transaction's size (more calldata = higher fee)
  • The current L1 base fee and blob base fee (relayed from Ethereum to the L2 every block)
  • Chain-specific scalars set by the chain operator

Neither eth_gasPrice nor block.baseFeePerGas includes this component. It's a separate charge deducted automatically from the sender.

Getting the L1 Data Fee

OP Stack chains expose a GasPriceOracle predeploy at 0x420000000000000000000000000000000000000F with methods to estimate the L1 portion:

// In Solidity: get the L1 data fee for a serialized unsigned transaction
uint256 l1Fee = IGasPriceOracle(0x420000000000000000000000000000000000000F)
    .getL1Fee(serializedUnsignedTx);

// Or get a gas-efficient upper bound (added in the Fjord upgrade, July 2024)
uint256 l1FeeUpper = IGasPriceOracle(0x420000000000000000000000000000000000000F)
    .getL1FeeUpperBound(txSizeInBytes);

From JavaScript, you can call this contract or use a library with native OP Stack support. viem has first-class support:

import { publicActionsL2 } from 'viem/op-stack';

const client = publicClient.extend(publicActionsL2());

// Estimate just the L1 data fee
const l1Fee = await client.estimateL1Fee({
  account: '0x...',
  to: '0x...',
  data: '0x...',
});

// Or estimate the total fee (L2 execution + L1 data)
const totalFee = await client.estimateTotalFee({
  account: '0x...',
  to: '0x...',
  data: '0x...',
});

ethers.js does not natively handle L1 data fees. getFeeData() returns only L2 execution fee data. If you're using ethers.js, you need to call the GasPriceOracle contract manually and add the result to your L2 execution estimate.

Putting It Together

The total transaction cost on an OP Stack L2:

// L2 execution cost
const block = await provider.getBlock('latest');
const gasEstimate = 400000n;
const l2Cost = gasEstimate * (block.baseFeePerGas ?? 0n);

// L1 data cost (call the GasPriceOracle predeploy)
const gasPriceOracle = new ethers.Contract(
  '0x420000000000000000000000000000000000000F',
  ['function getL1Fee(bytes) view returns (uint256)'],
  provider
);
const l1Cost = await gasPriceOracle.getL1Fee(serializedUnsignedTx);

// Total
const totalCost = l2Cost + l1Cost;

Ignoring either component gives you the wrong number. eth_gasPrice overestimates the L2 execution price. block.baseFeePerGas alone underestimates the total by missing the L1 data fee.

Real Example: Liquidation Bot

A liquidation bot monitors loans on a lending protocol. When a loan becomes undercollateralized (borrower's collateral drops below the required threshold), anyone can liquidate it and receive a 5% bonus on the seized collateral.

The bot checks profitability before executing:

// Original code (broken)
async function isProfitable(loan: Loan): Promise<boolean> {
  const feeData = await provider.getFeeData();
  const gasEstimate = 420000n;
  const gasCost = gasEstimate * feeData.gasPrice; // ❌ Inflated!

  const liquidationBonus = calculateBonus(loan); // 5% of collateral
  return liquidationBonus > gasCost;
}

With feeData.gasPrice returning 0.12 gwei:

  • Gas cost estimate: 420,000 × 0.12 gwei = $0.10
  • Liquidation bonus on a $1 loan: $0.05
  • Verdict: Not profitable. Skip.

But actual transaction cost at 0.002 gwei:

  • Real gas cost: 420,000 × 0.002 gwei = $0.002
  • Liquidation bonus: $0.05
  • Real profit: $0.048

The bot skipped profitable liquidations because it overestimated gas costs by 50x.

The fix:

// Fixed code
async function isProfitable(loan: Loan): Promise<boolean> {
  // L2 execution cost
  const block = await provider.getBlock('latest');
  const gasEstimate = 420000n;
  const l2Cost = gasEstimate * (block.baseFeePerGas ?? 50000000n);

  // L1 data cost
  const l1Cost = await gasPriceOracle.getL1Fee(serializedLiquidationTx);

  const totalGasCost = l2Cost + l1Cost;
  const liquidationBonus = calculateBonus(loan);
  return liquidationBonus > totalGasCost;
}

The bot uses the actual L2 base fee (not the inflated eth_gasPrice) and accounts for the L1 data fee. Both components matter for an accurate profitability check.

Takeaways

eth_gasPrice returns a recommendation, not the minimum. After EIP-1559, it returns base fee + a suggested priority fee. On OP Stack L2s, that suggestion is a hardcoded 0.1 gwei floor, which inflates the response by 50-100x relative to the actual base fee.

On L2s, priority fees are near zero. Blocks don't fill up. Transactions get included immediately. But eth_gasPrice still inflates its response by adding the priority fee floor.

Use block.baseFeePerGas for the L2 execution gas price. This is the actual minimum price, straight from the block header.

// ❌ Inflated on L2s
const feeData = await provider.getFeeData();
const gasPrice = feeData.gasPrice;

// ✅ Accurate L2 execution price
const block = await provider.getBlock('latest');
const gasPrice = block.baseFeePerGas;

Don't forget the L1 data fee. On OP Stack chains, every transaction also pays an L1 data fee for posting to Ethereum. This is often the larger component. Use the GasPriceOracle predeploy (0x420...00F) to estimate it, or use viem's estimateTotalFee() for the complete picture. ethers.js does not handle this natively.

Total cost = L2 execution fee + L1 data fee. Getting only one right still gives you the wrong number.

Always log both when debugging gas issues. If your estimates don't match reality:

const feeData = await provider.getFeeData();
const block = await provider.getBlock('latest');
console.log(`eth_gasPrice: ${feeData.gasPrice} (inflated recommendation)`);
console.log(`baseFeePerGas: ${block.baseFeePerGas} (L2 execution minimum)`);

If the ratio is 10x+, you've found the eth_gasPrice inflation problem. If your total estimate is still wrong after fixing that, check whether you're accounting for the L1 data fee.