On Base (OP-Stack), a transaction's total cost is the sum of an L2 execution fee and an L1 data fee. The L1 data fee prices the bytes the transaction publishes to Ethereum L1, using a gas-price-related scalar and a compressed-size term rather than the L1 gas price directly. This article explains the two-part cost model, shows where each input comes from over RPC, and provides a reconciliation method to verify the charged L1 data fee against a transaction receipt. It includes a runnable Node.js snippet and a troubleshooting checklist for common discrepancies.
The Two-Part Cost Model on OP-Stack Chains
On Base and other OP-Stack chains, the total cost of a transaction is not just the gas you see in your wallet. It is the sum of two distinct fees: the L2 execution fee and the L1 data fee. The L2 execution fee is computed as gas used multiplied by the effective L2 gas price, following the same EIP-1559 shape as Ethereum. The L1 data fee, by contrast, prices the data that the transaction publishes to Ethereum L1, and it is calculated from a gas-price-related scalar and a compressed-size term, not directly from the L1 gas price.
This separation matters because the two fees are quoted in different units and must not be summed as if they were a single gas number. The L2 execution fee is in L2 gas units, while the L1 data fee is derived from the size of the transaction's calldata after compression, multiplied by a scalar that reflects L1 gas costs. Mixing them leads to systematic under- or over-estimation. For a deeper look at how Base handles finality and block tags, see Base OP-Stack finality and block tags.
The OP-Stack specification and Base documentation describe this model, but the exact scalar and oracle contract parameters are versioned and chain-specific. Always treat them as 'documented / varies by chain' and consult the chain's own documentation for the current values.
- L2 execution fee = gas used × effective L2 gas price (EIP-1559 style).
- L1 data fee = compressed transaction size × L1 gas price scalar (from an oracle).
- The two fees are in different units; do not add them as a single gas amount.
Why eth_estimateGas and eth_gasPrice Only Cover Execution
The RPC methods eth_estimateGas and eth_gasPrice are designed to estimate the L2 execution side only. eth_estimateGas simulates the transaction to determine how much L2 gas it will consume, and eth_gasPrice returns a suggested L2 gas price. Neither method accounts for the L1 data fee, which depends on the size of the transaction's calldata and the current L1 gas price scalar.
If you build a total cost estimate using only these methods, you will systematically under-predict what the user actually pays. This is especially true for calldata-heavy transactions such as contract deployments, batch transfers, or transactions with long revert reasons. For a broader discussion of fee estimation on Ethereum, see Estimating gas price with eth_feeHistory.
To get a complete picture, you must also query the chain's gas-price-oracle contract, typically via eth_call, to read the L1 scalar and the L1 base-fee component. The oracle's ABI and address are chain-specific and must be taken from that chain's documentation rather than hard-coded.
eth_estimateGasreturns L2 gas used only.eth_gasPricereturns an L2 gas price suggestion only.- The L1 data fee requires a separate oracle call.
RPC Sources for Each Fee Component
To reconstruct the total cost of a transaction, you need data from several RPC sources. The transaction receipt itself contains the charged amounts: gasUsed, effectiveGasPrice, and often a field for the L1 fee (the exact field name is chain and client dependent). For the execution side, eth_feeHistory and eth_gasPrice provide base fee and priority fee information. For the L1 data fee, you must call the chain's gas-price-oracle contract, usually via eth_call, to read the L1 scalar and the L1 base-fee component.
The oracle contract address and ABI are not universal. On Base, for example, the address is documented in Base's developer resources. On Optimism, it may differ. Always verify from the chain's official documentation. If you are using a provider like OnFinality, ensure your endpoint supports eth_call and eth_getTransactionReceipt for the chain in question. See RPC endpoints guide (RPC Assistant) for endpoint capabilities.
When reading historical transactions, pin the block number. Using a latest tag for a historical reconstruction will give you current oracle values, not the values at the time of the transaction, leading to incorrect L1 fee calculations.
- Receipt:
gasUsed,effectiveGasPrice, and L1 fee field (if present). - Execution side:
eth_feeHistory,eth_gasPrice. - L1 data fee: gas-price-oracle contract via
eth_call(address and ABI chain-specific).
Reconciling the Receipt Against Computed Fees
The reconciliation method turns a vague 'fees are high' complaint into evidence. Start by fetching the receipt for a known transaction using eth_getTransactionReceipt. Read the gasUsed and effectiveGasPrice fields, then compute the L2 execution fee as gasUsed * effectiveGasPrice. Next, look at the total value change reported by the receipt—this is typically the difference between the sender's balance before and after the transaction, or a field like l1Fee if present. Subtract the L2 execution fee from the total value change; the remainder is the L1 data fee for that transaction.
Note that the receipt's exact field naming for the L1 portion is chain and client dependent. Some clients include a l1Fee field, others may not. Confirm against the chain's own documentation. If the receipt does not expose the L1 fee directly, you can still compute it from the oracle values and the transaction's calldata size, but this requires knowing the compression algorithm and scalar.
For a practical example of tracking cross-chain events, see Tracking Base cross-chain events.
- Fetch receipt:
eth_getTransactionReceipt. - Compute L2 execution fee =
gasUsed * effectiveGasPrice. - Subtract from total value change to isolate L1 data fee.
- Verify receipt field names against chain docs.
Runnable Node.js Example: Reading Receipt and Oracle
The following Node.js script connects to an RPC endpoint, fetches a transaction receipt, and calls the gas-price-oracle contract to read the L1 fee scalar and base fee. It then prints a table of components. Replace the placeholder oracle address with the correct one from your chain's documentation. This script uses ethers.js for simplicity, but you can adapt it to web3.js or raw JSON-RPC calls.
Ensure your RPC endpoint supports eth_call and eth_getTransactionReceipt. For a list of supported methods, see RPC endpoints guide (RPC Assistant).
const { ethers } = require('ethers');
// Replace with your RPC endpoint (e.g., from OnFinality)
const RPC_URL = 'https://base-mainnet.example.com';
// Replace with the gas-price-oracle address from the chain's docs
const ORACLE_ADDRESS = '0x0000000000000000000000000000000000000000';
// Minimal ABI for the oracle (check chain docs for exact function names)
const ORACLE_ABI = [
'function l1BaseFee() view returns (uint256)',
'function scalar() view returns (uint256)'
];
async function main() {
const provider = new ethers.JsonRpcProvider(RPC_URL);
const txHash = '0x...'; // Replace with a known transaction hash
const receipt = await provider.getTransactionReceipt(txHash);
if (!receipt) {
console.error('Receipt not found');
return;
}
const gasUsed = receipt.gasUsed;
const effectiveGasPrice = receipt.effectiveGasPrice;
const l2ExecutionFee = gasUsed * effectiveGasPrice;
const oracle = new ethers.Contract(ORACLE_ADDRESS, ORACLE_ABI, provider);
const l1BaseFee = await oracle.l1BaseFee();
const scalar = await oracle.scalar();
console.log('Component | Value');
console.log('-------------------------|--------------------------');
console.log(`Gas Used | ${gasUsed.toString()}`);
console.log(`Effective Gas Price | ${effectiveGasPrice.toString()}`);
console.log(`L2 Execution Fee (wei) | ${l2ExecutionFee.toString()}`);
console.log(`L1 Base Fee (wei) | ${l1BaseFee.toString()}`);
console.log(`L1 Scalar | ${scalar.toString()}`);
// Note: L1 data fee calculation requires compressed size; see chain docs.
}
main().catch(console.error);Bytes Sensitivity: Why Calldata-Heavy Transactions Pay More L1 Fee
The L1 data fee is bytes-sensitive. It prices the data that the transaction publishes to Ethereum L1, so transactions with large calldata—such as contract deployments, batch transfers, or transactions with long revert reasons—pay far more L1 fee than a plain transfer, even when their execution gas is similar. This is because the L1 data fee is proportional to the compressed size of the transaction's calldata, not to the L2 gas used.
This has implications for batching and for storing data on chain. If you are batching many transfers, the calldata size grows linearly with the number of transfers, and so does the L1 data fee. Storing large data blobs on chain will incur significant L1 fees. Developers should consider off-chain storage or compression techniques to mitigate costs.
For a deeper understanding of how Base handles block tags and finality, see Base OP-Stack finality and block tags.
- L1 data fee scales with compressed calldata size.
- Batch transfers and deploys pay more L1 fee than simple transfers.
- Consider off-chain storage or compression for large data.
Troubleshooting Fee Discrepancies
When your computed fee disagrees with the receipt, check these common failure modes. First, verify the oracle address and scalar. Using a wrong oracle address or a stale scalar will produce incorrect L1 fee values. Second, ensure you are reading the receipt from a node that is fully synced; a lagging node may return outdated or missing data. Third, avoid using a latest tag for historical reconstruction—always pin the block number to the transaction's block. Fourth, check if the chain had subsidised the fee; some chains or applications may cover part of the L1 fee, so the receipt may show a lower amount than the raw calculation.
Additionally, confirm that you are not summing a fee that the chain had already subsidised. If the receipt includes an L1 fee field, use that as the authoritative charged amount. If not, compute it from oracle values but be aware of potential subsidies.
For latency-related issues that could affect receipt retrieval, see Base RPC latency measurement.
- Wrong oracle address or stale scalar.
- Receipt read through a lagging node.
- Using
latesttag for historical reconstruction. - Summing a fee that was already subsidised.
Limitations and Tradeoffs
The L1 data fee model has inherent limitations. Scalars and oracles change over time, so any hard-coded values will become outdated. Some receipt fields are implementation-defined and may vary across clients or chains. A reproducible fee reproduction must pin the block number as well as the transaction hash, because oracle values are block-dependent. Finally, the exact compression algorithm and scalar application are not always fully documented, making independent verification challenging.
These tradeoffs mean that while you can reconcile fees for a specific transaction, building a general-purpose fee estimator requires ongoing maintenance and chain-specific configuration. Always refer to the chain's official documentation for the most current parameters.
- Scalars and oracles are versioned and chain-specific.
- Receipt field names vary by client and chain.
- Pin block number for reproducible results.
- Compression details may be undocumented.
Results Table: Measure Against Your Own Endpoint
To validate your understanding and your RPC endpoint's behavior, create a results table for a set of known transactions. For each transaction, record the transaction hash, block number, gas used, effective gas price, computed L2 execution fee, receipt-reported L1 fee (if available), and your computed L1 fee from oracle values. This will help you identify discrepancies and confirm that your endpoint returns consistent data.
Use the following template to fill in your own measurements. This is a verified-by-the-reader method; no benchmark numbers are provided here because they depend on your specific endpoint and chain state.
- Transaction Hash | Block Number | Gas Used | Effective Gas Price | L2 Execution Fee | Receipt L1 Fee | Computed L1 Fee | Notes
- Fill each row with data from your own RPC calls.
- Compare receipt L1 fee with computed L1 fee to spot discrepancies.
Next Steps: Integrating Fee Awareness into Your Application
Now that you can read and reconcile the L1 data fee, consider integrating fee awareness into your application. Display both the L2 execution fee and the L1 data fee separately to users, so they understand the total cost. When estimating fees for a transaction, query the oracle contract to include the L1 component. For batch operations, calculate the expected calldata size and its L1 fee impact.
For production use, choose a reliable RPC provider. OnFinality offers API service with endpoints for Base and other networks. You can also explore RPC pricing to find a plan that fits your needs. For a full list of supported networks, see networks/base.
Finally, keep an eye on the OnFinality Learn hub for more guides on OP-Stack and Ethereum RPC.
- Display L2 and L1 fees separately to users.
- Query oracle for L1 fee in estimates.
- Consider calldata size for batch operations.
- Choose a reliable RPC provider like OnFinality.