Where does it stop?
The node compares your fee cap with the base fee of the block. If the cap is lower, the transaction is refused at once. There is no hash, nothing reaches a block, and no gas is spent. If the base fee rises only after a transaction was accepted, it is not refused: it waits, and looks stuck pending.
What must the cap clear?
EIP-1559 says a transaction must be willing to pay at least the base fee: max_fee_per_gas has to be greater than or equal to the block’s base_fee_per_gas. The cap is the most you are willing to pay per gas, and the base fee moves with every block.
The base fee can rise by up to 12.5% from one block to the next, so a cap that only just clears it can be too low a few blocks later. A cap of 1.2 times the base fee covers about one block of the largest rise.
Why does it happen?
- A hardcoded fee. The number was right on the day you wrote it. The base fee has moved since.
- A unit slip.
maxFeePerGas: 2nis 2 wei, not 2 gwei. The number looks small and sensible and is far below any base fee. - A number nobody checked. A fee that comes from a config file or from a model’s tool argument is not compared with the chain.
- Settings from another chain. A cap copied from a chain with a cheaper base fee.
How do I fix it?
- Leave the fee out and let the client choose. With a local account, viem estimates the fees when you do not give them.
- If you must set it, derive it from the chain, and keep the units straight:
import { createPublicClient, createWalletClient, http, parseGwei } from 'viem';import { mainnet } from 'viem/chains';
declare const account: `0x${string}`;const publicClient = createPublicClient({ chain: mainnet, transport: http() });const wallet = createWalletClient({ account, chain: mainnet, transport: http() });
// Ask the client for a fee that fits the chain now, instead of writing a number.const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas();await wallet.sendTransaction({ to: account, value: 1n, maxFeePerGas, maxPriorityFeePerGas });
// Units: parseGwei('2') is 2 gwei. A bare 2n is 2 wei, far below any base fee.const twoGwei = parseGwei('2');- If your agent or tool takes a fee as an argument, check it before sending: compare it with the current base fee and return a clear message instead of the raw error.
What does it look like in a trace?
Like every refusal by the node, it is a send span that ends with an error and no transaction hash:
- error.type
- FeeCapTooLowError
- exception.message
- The fee cap (`maxFeePerGas` = 0.999999999 gwei) cannot be lower than the block base fee.
- blockchain.tx.hash
- not set
By default only the error type is recorded, and for this refusal it is FeeCapTooLowError. Nonce too low shows how to read the node’s wording too. The part of a fee that is not the base fee, the L1 data fee, is explained in what is the L1 data fee.