Skip to main content
Every Ethereum account has a nonce — a counter incremented with each confirmed transaction. The Base sequencer rejects any transaction whose nonce doesn’t match what it expects for your account. For agents submitting multiple transactions, incorrect nonce handling is the most common cause of stuck or cascading failures.

The Two Nonces You Need to Understand

Nonce typeeth_getTransactionCount tagReflects
Confirmed nonce"latest"Transactions included in finalized blocks
Pending nonce"pending"Confirmed + all transactions currently in the mempool
Always use "pending" when building a new transaction. Using "latest" when there are unconfirmed transactions in the mempool will assign a nonce that’s already claimed, causing a nonce too low rejection.
{
  "jsonrpc": "2.0",
  "method": "eth_getTransactionCount",
  "params": ["0xYourAddress", "pending"],
  "id": 1
}

Single-Thread Agent (Sequential Submissions)

For agents that submit one transaction at a time and wait for confirmation before the next, the simplest approach is always fetching the pending nonce fresh:
async function sendNext(txParams: TxParams) {
  const nonce = await client.getTransactionCount({
    address: agentAddress,
    blockTag: 'pending',
  });
  return walletClient.sendTransaction({ ...txParams, nonce });
}
Timing on Base: With Flashblocks at 200ms and 2-second block times, a transaction typically confirms within 2–4 seconds under normal conditions. A sequential agent can safely fetch a fresh nonce before each submission.

Multi-Thread / Concurrent Agent (Parallel Submissions)

When an agent needs to submit multiple transactions without waiting for each to confirm (e.g., multiple position adjustments), fetch-nonce-per-call fails because all calls will return the same pending nonce. Use a local nonce manager that increments atomically:
class NonceManager {
  private nonce: bigint | null = null;
  private lock = false;
  private queue: Array<() => void> = [];

  async acquire(address: `0x${string}`): Promise<bigint> {
    // Initialize from chain if not set
    if (this.nonce === null) {
      this.nonce = BigInt(
        await client.getTransactionCount({ address, blockTag: 'pending' })
      );
    }
    return this.nonce++;
  }

  // Call after a tx is confirmed to sync if drift is detected
  async sync(address: `0x${string}`) {
    const chainPending = BigInt(
      await client.getTransactionCount({ address, blockTag: 'pending' })
    );
    // Only advance forward, never backward
    if (chainPending > (this.nonce ?? 0n)) {
      this.nonce = chainPending;
    }
  }
}

const nonceManager = new NonceManager();

// Submit multiple transactions concurrently
const [hash1, hash2, hash3] = await Promise.all([
  walletClient.sendTransaction({ ...tx1, nonce: Number(await nonceManager.acquire(address)) }),
  walletClient.sendTransaction({ ...tx2, nonce: Number(await nonceManager.acquire(address)) }),
  walletClient.sendTransaction({ ...tx3, nonce: Number(await nonceManager.acquire(address)) }),
]);
Never decrement a local nonce counter. If a transaction is rejected, the nonce it was assigned may or may not need to be reused depending on the error. See the recovery section below.

Nonce Gaps and Queue Stalls

A nonce gap occurs when a transaction with nonce N is missing (failed, never submitted, or dropped), while transactions with nonces N+1, N+2, … are waiting in the mempool. The sequencer will not process N+1 until N is resolved. Detecting a gap:
async function detectNonceGap(address: `0x${string}`) {
  const confirmed = await client.getTransactionCount({ address, blockTag: 'latest' });
  const pending = await client.getTransactionCount({ address, blockTag: 'pending' });

  // If pending > confirmed, check if there's a tx for each nonce in between
  // In practice: if a tx hasn't confirmed after N blocks, treat it as a gap
  return { confirmed, pending, gap: pending > confirmed };
}
Resolving a gap: Submit a new transaction at the missing nonce. This can be a zero-value self-transfer or the original transaction with adjusted fees:
async function fillNonceGap(
  missingNonce: number,
  address: `0x${string}`
) {
  const { maxFeePerGas, maxPriorityFeePerGas } = await computeOptimalFees('urgent', 0);

  return walletClient.sendTransaction({
    to: address,             // self-transfer to fill the gap
    value: 0n,
    nonce: missingNonce,
    maxFeePerGas: (maxFeePerGas * 150n) / 100n,    // high fee to land quickly
    maxPriorityFeePerGas: (maxPriorityFeePerGas * 150n) / 100n,
    gas: 21000n,
  });
}
Once the gap is filled, all queued transactions (N+1, N+2, …) will process in sequence.

Error Recovery by Nonce Error Type

ErrorMeaningAction
nonce too lowThe nonce you used is already confirmed or claimed by a higher-fee txFetch fresh pending nonce, rebuild and resubmit
nonce too highYour nonce is too far ahead (gap in sequence)Submit a tx at the missing nonce first, then retry
replacement transaction underpricedA tx with this nonce exists and your fees aren’t 10% higherIncrease fees by ≥10%, resubmit with same nonce
Transaction stuck (no receipt after 10+ blocks)Tx may be in mempool but underprioritizedReplace with same nonce + ≥15% higher fees

After a nonce too low rejection

async function handleNonceTooLow(address: `0x${string}`, manager: NonceManager) {
  // Sync local nonce manager to chain state
  await manager.sync(address);
  // Rebuild and resubmit with the new nonce
  const newNonce = Number(await manager.acquire(address));
  return walletClient.sendTransaction({ ...originalTxParams, nonce: newNonce });
}

Nonce Management with Account Abstraction

If your agent uses an ERC-4337 smart account, nonce management works differently:
  • The smart account has its own nonce tracked by the EntryPoint contract, not eth_getTransactionCount
  • UserOps are submitted via eth_sendUserOperation to a bundler, which handles sequencing
  • Multiple UserOps can share a 2D nonce (key + sequence) for parallel submission lanes
For concurrent UserOp submission, use different nonce keys (upper 192 bits) to create independent queues:
Lane 0: nonce = (0 << 64) | sequenceInLane0
Lane 1: nonce = (1 << 64) | sequenceInLane1
See Account Abstraction for more on ERC-4337 on Base.

Monitoring Nonce Health

For long-running agents, periodically check that the local nonce is in sync with chain state:
setInterval(async () => {
  const chainPending = BigInt(
    await client.getTransactionCount({ address: agentAddress, blockTag: 'pending' })
  );
  const localNonce = nonceManager.peek(); // current value without incrementing

  if (chainPending > localNonce) {
    console.warn(`Nonce drift detected: chain=${chainPending}, local=${localNonce}`);
    await nonceManager.sync(agentAddress);
  }
}, 30_000); // every 30 seconds