eth_gasPrice vs baseFeePerGas: Which to Use on L2s

·6 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. Real Example: Liquidation Bot
  7. 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 + average priority fee

It's a "recommendation" for what to bid, not the minimum you'd pay.

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. Transactions get included immediately with minimal or zero tip.

But eth_gasPrice still adds an average priority fee to its response. On an L2 where the base fee is 0.002 gwei and average priority fee is 0.1 gwei, you get:

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

The actual cost is 0.002 gwei. The reported cost 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 accurate gas cost estimation on L2s, use block.baseFeePerGas:

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

Real Example: Liquidation Bot

This isn't theoretical. Here's how this bug manifested in a real system.

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> {
  const block = await provider.getBlock('latest');
  const gasEstimate = 420000n;
  const gasCost = gasEstimate * (block.baseFeePerGas ?? 50000000n); // ✅ Actual

  const liquidationBonus = calculateBonus(loan);
  return liquidationBonus > gasCost;
}

Now the bot correctly identifies profitable opportunities.

Takeaways

eth_gasPrice returns a recommendation, not the minimum. After EIP-1559, it returns base fee + average priority fee. This made sense when priority fees mattered.

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 a priority fee component.

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

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

// ✅ Accurate on L2s
const block = await provider.getBlock('latest');
const gasPrice = block.baseFeePerGas;

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} (recommendation)`);
console.log(`baseFeePerGas: ${block.baseFeePerGas} (actual)`);

If the ratio is 10x+, you've found your problem.