Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides13 min read

Monad Gas Price Estimation: Dual Pricing over RPC

Monad prices execution gas and calldata on two independent axes; learn how to read both over JSON-RPC and combine them into one expected-cost figure.

TL;DR

Monad prices a transaction on two independent axes: an execution gas price and a separate data (calldata) price. The Ethereum JSON-RPC methods eth_gasPrice and eth_estimateGas each speak to only one axis, so a client that multiplies gasUsed by a single suggested price will misprice data-heavy calls. The correct estimate reads the suggested execution price, reads the current data price, estimates execution gas, sizes the calldata, and combines them with an explicit buffer. This article explains the mechanism, shows runnable Node.js estimators, and gives a results table so you can measure against your own endpoint rather than trusting a hard-coded constant.

Monad's Two-Axis Pricing Model in Operational Terms

Monad's documentation on gas pricing states that the network charges on two independent axes: an execution gas price and a separate data (calldata) price. Execution gas covers the compute a transaction consumes; the data price covers the bytes the transaction carries. A transaction's effective cost is therefore a function of both the gas used and the size of its calldata, not a single gasUsed × gasPrice product.

This is the single fact that breaks a reader arriving with an Ethereum mental model. On Ethereum L1 the fee is essentially one-dimensional: gasUsed × (baseFee + priorityFee). On Monad, a naive reader who multiplies gasUsed by one suggested price will under- or over-estimate depending on how data-heavy the call is. A simple value transfer and a large calldata contract call can consume similar execution gas yet carry very different data costs.

Operationally, the model tells you which part of your transaction each axis penalises. If your call is compute-bound (loops, storage writes), the execution axis dominates. If your call is data-bound (large ABI-encoded arguments, batch payloads, calldata-heavy contract deployments), the data axis dominates. Reading both axes is what lets you reason about which one to optimise.

  • Execution axis: priced per unit of execution gas consumed.
  • Data axis: priced per byte of calldata the transaction carries.
  • Effective cost: a combination of both, not a single gasUsed × gasPrice product.
  • Practical consequence: data-heavy calls need a different estimate than compute-heavy calls.

What eth_gasPrice Returns on Monad and What It Omits

The Ethereum JSON-RPC specification defines eth_gasPrice as returning a suggested gas price in wei. On Monad, treat this as the suggested execution gas price, not a total cost. It is one input to a two-axis calculation, and the exact response fields are documented / varies by node version, so verify against the current payload rather than hard-coding assumptions.

The omission matters. A client that uses eth_gasPrice as its sole fee input will misprice any transaction whose cost is dominated by calldata. The method does not encode the data price, and it does not know how many bytes your transaction will carry. It answers a narrower question: what execution price is currently suggested.

If you are connecting through a managed endpoint, the same method semantics apply regardless of provider; what varies is the suggested value the node returns and any provider-side caching. For endpoint selection and provider-specific behaviour, see the Monad RPC provider guide and the Monad network page.

  • Returns: a suggested execution gas price in wei.
  • Does not return: the data price, or a total transaction cost.
  • Risk: using it as the only fee input misprices data-heavy calls.
  • Verify: response fields are documented / varies by node version.

Why eth_estimateGas Answers Fit, Not Cost

The specification describes eth_estimateGas as returning an estimate of the gas a transaction needs to execute. That is a gas limit, expressed in gas units, and it speaks to the execution axis only. It does not return a price, and it does not account for the data axis.

Conflating the limit with the price is a common bug. A developer reads the number from eth_estimateGas, multiplies it by the number from eth_gasPrice, and believes they have a cost. They have an execution-axis estimate. The data axis is still unpriced, and on a calldata-heavy transaction that gap can be the larger part of the bill.

The correct reading is: eth_estimateGas tells you whether the transaction will fit within a gas limit and roughly how much execution it consumes. It is a sizing tool, not a pricing tool. Pair it with a price read for each axis before you present a cost to a user.

  • eth_estimateGas returns a gas limit (execution axis), not a price.
  • Multiplying it by eth_gasPrice yields an execution-only estimate.
  • Data-heavy transactions remain unpriced by this pair alone.
  • Use it to size, then price both axes separately.

Where eth_feeHistory Fits and Why Low Activity Makes It Noisy

The eth_feeHistory method returns historical base fee and priority fee data, often with percentile bands. On Ethereum L1, where fee variability is high, those percentile bands are informative. On a chain with less fee variability, the bands can be flat, and a flat band carries little signal about what to pay next.

Treat feeHistory as advisory on Monad. It is useful for sanity-checking whether the current suggested price is in a normal range, but it should not be the sole driver of your fee. Prefer the node's current suggested execution price plus an explicit buffer, and read the data price separately. For a deeper treatment of the method's semantics, see Ethereum fee history and priority fee estimation and the eth_feeHistory priority-fee percentile estimator.

A practical pattern is to use feeHistory as a guardrail: if your chosen price is far outside the recent range, log it and let the caller decide. Do not silently clamp to a historical percentile on a low-activity chain, because the percentile may reflect a period with almost no fee pressure.

  • feeHistory is advisory on Monad, not authoritative.
  • Flat percentile bands on low-activity chains carry little signal.
  • Prefer current suggested price plus an explicit buffer.
  • Use feeHistory as a range guardrail, not a hard clamp.

A Practical Two-Axis Estimator Recipe

The recipe has five reads and one combination. First, read the suggested execution price. Second, read the current data price. Third, estimate execution gas with eth_estimateGas. Fourth, size the calldata in bytes. Fifth, combine the two axes into a single expected-cost figure, and apply a settable buffer so the caller can trade cost against inclusion confidence.

The combination is the part readers get wrong. Expected cost is not gasUsed × gasPrice. It is (executionGas × executionPrice) + (calldataBytes × dataPrice), with a buffer applied to the total or to each axis depending on your risk preference. Keep the two components separate in your output so you can see which axis dominates for a given transaction.

Because the data price is not a constant a client should hard-code, read it at estimate time. If your node does not expose a dedicated data-price method, check the current Monad documentation and your provider's method list; the exact method name and response shape are documented / varies by node version. The API service and RPC pricing pages describe how managed access is structured if you are choosing an endpoint.

  • Read suggested execution price.
  • Read current data price.
  • Estimate execution gas (eth_estimateGas).
  • Size calldata in bytes.
  • Combine: (gas × execPrice) + (bytes × dataPrice), then apply buffer.

Runnable Node.js Estimator Against a Monad RPC Endpoint

The script below queries a Monad RPC endpoint for the suggested execution price, estimates gas for a sample transaction, sizes the calldata, and prints both axes plus a combined figure. It uses only standard JSON-RPC calls and Node's built-in fetch, so it runs without extra dependencies on a modern Node runtime.

Replace the endpoint URL and the sample transaction with your own. The data-price read is shown as a placeholder call because the exact method name is documented / varies by node version; check the current Monad documentation and your provider's method list, then wire the correct method in. The script deliberately prints the two axes separately so you can see which one dominates.

// monad-estimator.js — run with: node monad-estimator.js
const RPC_URL = process.env.MONAD_RPC_URL || 'https://your-monad-endpoint';
const BUFFER = 1.15; // 15% buffer, settable

async function rpc(method, params = []) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(method + ': ' + JSON.stringify(json.error));
  return json.result;
}

async function main() {
  // 1. Suggested execution gas price (wei)
  const execPriceHex = await rpc('eth_gasPrice');
  const execPrice = BigInt(execPriceHex);

  // 2. Current data price (wei per byte). Method name varies by node version.
  //    Check current Monad docs / provider method list and wire it here.
  let dataPrice = 0n;
  try {
    const dataPriceHex = await rpc('eth_dataPrice'); // placeholder name
    dataPrice = BigInt(dataPriceHex);
  } catch (e) {
    console.warn('data price method unavailable on this endpoint:', e.message);
  }

  // 3. Sample transaction
  const tx = {
    from: '0x0000000000000000000000000000000000000001',
    to: '0x0000000000000000000000000000000000000002',
    value: '0x0',
    data: '0x' + 'ab'.repeat(200) // 200 bytes of calldata
  };

  // 4. Estimate execution gas
  const gasHex = await rpc('eth_estimateGas', [tx]);
  const gas = BigInt(gasHex);

  // 5. Size calldata
  const calldataBytes = BigInt((tx.data.length - 2) / 2);

  // 6. Combine both axes
  const execCost = gas * execPrice;
  const dataCost = calldataBytes * dataPrice;
  const total = execCost + dataCost;
  const buffered = (total * BigInt(Math.round(BUFFER * 100))) / 100n;

  console.log('execution gas      :', gas.toString());
  console.log('exec price (wei)   :', execPrice.toString());
  console.log('calldata bytes     :', calldataBytes.toString());
  console.log('data price (wei/B) :', dataPrice.toString());
  console.log('exec cost (wei)    :', execCost.toString());
  console.log('data cost (wei)    :', dataCost.toString());
  console.log('total (wei)        :', total.toString());
  console.log('with buffer (wei)  :', buffered.toString());
}

main().catch(e => { console.error(e); process.exit(1); });

Reading the Two Axes Separately in Your Own Output

A single combined number hides the mechanism. Print the execution cost and the data cost as separate fields so that when a user asks why a transaction is expensive, you can point at the axis responsible. This also makes your estimator debuggable: if the combined figure looks wrong, you can see whether the execution read or the data read is the outlier.

The same separation helps with optimisation. If the data axis dominates, consider whether your calldata can be compressed, batched differently, or moved off-chain. If the execution axis dominates, look at the contract logic. Without the split, you are guessing.

Keep the buffer explicit and configurable. A buffer is a policy choice, not a protocol constant; expose it so callers can trade cost against inclusion confidence. Document the default you ship and why.

  • Print execution cost and data cost as separate fields.
  • Use the split to diagnose which axis is the outlier.
  • Expose the buffer as a configurable policy, not a hidden constant.
  • Log the raw reads so estimates are reproducible.
// split-axes.js — print each pricing axis as its own field, then a combined total.
// Run with: node split-axes.js
const RPC_URL = process.env.MONAD_RPC_URL || 'https://your-monad-endpoint';

async function rpc(method, params = []) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(method + ': ' + JSON.stringify(json.error));
  return json.result;
}

// Convert a wei value to a human-readable string without losing precision.
function wei(v) {
  const s = v.toString().padStart(19, '0');
  return s.slice(0, -18) + '.' + s.slice(-18);
}

async function report(tx) {
  const execPrice = BigInt(await rpc('eth_gasPrice'));
  const gas = BigInt(await rpc('eth_estimateGas', [tx]));
  const calldataBytes = BigInt((tx.data.length - 2) / 2);

  // The data axis is read separately; if the endpoint does not expose it,
  // report the gap explicitly instead of assuming a zero price.
  let dataPrice = null;
  try { dataPrice = BigInt(await rpc('eth_dataPrice')); }
  catch { console.warn('data-price method unavailable on this endpoint'); }

  const execCost = gas * execPrice;
  const dataCost = dataPrice === null ? null : calldataBytes * dataPrice;

  console.log(JSON.stringify({
    execution: { gas: gas.toString(), priceWei: execPrice.toString(), costWei: execCost.toString(), costMON: wei(execCost) },
    data:      { bytes: calldataBytes.toString(), priceWei: dataPrice === null ? null : dataPrice.toString(),
                 costWei: dataCost === null ? null : dataCost.toString() },
    combinedWei: dataCost === null ? null : (execCost + dataCost).toString(),
    note: dataCost === null ? 'data axis unavailable; execution-only estimate' : 'both axes priced'
  }, null, 2));
}

report({
  from: '0x0000000000000000000000000000000000000001',
  to:   '0x0000000000000000000000000000000000000002',
  value: '0x0',
  data: '0x' + 'ab'.repeat(200)
}).catch(e => { console.error(e.message); process.exit(1); });

Results Table: Measuring Against Your Own Endpoint

Do not trust a hard-coded constant or a number from someone else's endpoint. Fill the table below against your own Monad RPC endpoint, using the estimator script above or your own client. Run it at several times of day and for several transaction shapes so you can see how the two axes move.

Record the endpoint, the timestamp, the suggested execution price, the data price, the estimated gas, the calldata size, and the combined figure. If your endpoint does not expose a data-price method, record that as a gap and check the current Monad documentation and your provider's method list before concluding the axis is zero.

The table is a measurement method, not a benchmark. It produces values specific to your endpoint and your transactions; treat them as observations to verify, not as universal figures.

  • Columns: endpoint, timestamp, exec price, data price, gas, calldata bytes, combined cost, buffer applied.
  • Rows: one per transaction shape (value transfer, small call, large calldata call, deployment).
  • Repeat: run at multiple times to see axis movement.
  • Note gaps: record any method your endpoint does not expose.

Troubleshooting: When a One-Axis Mental Model Surfaces as an Error

The people-also-search-for query 'why is MetaMask saying insufficient gas' is exactly the symptom of pricing on one axis. A wallet that estimates execution gas and multiplies by a single suggested price can underprice a data-heavy transaction, and the shortfall surfaces as an insufficient-funds or insufficient-gas error rather than as a clear pricing message.

The diagnostic path is to separate the two axes. Read the suggested execution price, read the data price, estimate gas, and size the calldata. If the combined figure exceeds the balance or the fee cap the wallet applied, you have found the gap. If the endpoint does not expose a data-price method, that absence is itself a finding: your estimator is running on one axis.

Other common symptoms map cleanly. A transaction that fits under the gas limit but still fails on cost points at the data axis. A transaction that fails on the gas limit points at the execution axis. A transaction that succeeds but costs more than expected usually means the data axis was not included in the estimate. For execution-timing symptoms, see Monad transaction lifecycle and receipt status; for balance semantics, see Monad reserve balance and eth_getBalance.

  • Insufficient-gas errors often mean the data axis was omitted.
  • Fails on gas limit → execution axis.
  • Fails on cost but fits the limit → data axis.
  • Succeeds but costs more than expected → data axis not estimated.
  • Missing data-price method → estimator is running on one axis.

Limitations, Tradeoffs, and What the Estimator Cannot Promise

Monad's pricing parameters are protocol-defined and can change. The data price is not a constant a client should hard-code, and the exact method names and response fields are documented / varies by node version. Any estimator you ship should read the current values at estimate time and degrade gracefully when a method is unavailable.

An estimator is an estimate, not a guarantee. Execution gas can vary with state, and the suggested price can move between the estimate and the inclusion of the transaction. A buffer reduces the chance of a shortfall but does not eliminate it. Treat the combined figure as a planning number, not a contract.

There is also a tradeoff between accuracy and complexity. A one-axis estimate is simpler and may be adequate for compute-bound calls with small calldata. A two-axis estimate is more accurate for data-heavy calls but requires reading an additional value and handling its absence. Choose based on the transaction shapes your application actually sends, and document the choice.

  • Pricing parameters are protocol-defined and can change.
  • Data price is not a hard-codable constant.
  • Method names and fields are documented / varies by node version.
  • Estimates are planning numbers, not guarantees.
  • Two-axis accuracy costs an extra read and a fallback path.

Next Steps: Wiring Dual Pricing into Your Stack

Start by running the estimator against your endpoint and filling the results table for the transaction shapes you actually send. That tells you whether the data axis is material for your workload. If it is, wire the two-axis combination into your fee display and your preflight checks so users see a cost that reflects both axes.

Then decide how you will handle the data-price read when it is unavailable. A safe default is to log the gap and fall back to a conservative buffer on the execution axis, while flagging that the estimate is one-axis. Do not silently assume the data price is zero.

For endpoint selection and provider-specific behaviour, review the Monad RPC provider guide, the Monad network page, and the API service overview. For broader fee-estimation context, the OnFinality Learn hub collects the related Ethereum and Monad articles, and RPC pricing describes how managed access is structured.

  • Run the estimator and fill the results table for your transaction shapes.
  • Wire the two-axis combination into fee display and preflight checks.
  • Define a fallback for a missing data-price read; never assume zero.
  • Review provider and network pages before choosing an endpoint.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started