Skip to main content
Transaction failures fall into three categories with distinct recovery strategies:
CategoryWhen it happensAgent action
RPC rejectionBefore broadcast — node rejects the txFix locally, resubmit (no gas wasted)
Mempool evictionAfter broadcast — tx dropped before inclusionResubmit with higher fee
EVM revertDuring execution — tx included but failedDiagnose revert, decide to retry or abort

RPC Rejection Errors

These are JSON-RPC errors returned synchronously from eth_sendRawTransaction. No gas is consumed.
Error messageCodeCauseAgent action
nonce too low-32000A tx with this nonce already existsFetch nonce with eth_getTransactionCount(addr, "pending"), rebuild tx
nonce too high-32000Nonce is too far ahead of current stateWait for pending txs to confirm; check for nonce gaps
replacement transaction underpriced-32000Replacing an existing tx with same nonce requires ≥10% higher feeIncrease maxFeePerGas and maxPriorityFeePerGas by ≥10%, resubmit
intrinsic gas too low-32000Gas limit below minimum for tx typeIncrease gas limit (minimum: 21,000 for ETH transfers; higher for contract calls)
transaction underpriced-32000maxFeePerGas is below the current network base feeRe-fetch base fee, set maxFeePerGas = baseFee * 1.5 + priorityFee
exceeds maximum per-transaction gas limit-32000Gas limit exceeds Base’s 25M gas capReduce gas limit or split operation
insufficient funds for gas * price + value-32000Account ETH balance too low to cover gas + valueCheck balance, reduce value or use lower fees
transaction indexing is in progress-32000Node is still indexing — try again shortlyRetry after 1–2 seconds
invalid sender-32000Signature invalid or chain ID mismatchVerify chain ID (Base Mainnet: 8453, Sepolia: 84532) and re-sign

Mempool Eviction and Stuck Transactions

A transaction accepted into the mempool can still be evicted or stuck if fees fall below the base fee after submission. Detection: Poll eth_getTransactionReceipt. If null after N blocks (use N=10 for standard, N=3 for urgent), assume the tx is stuck. Resolution: Submit a replacement transaction with the same nonce and fees increased by ≥10%:
// Replacement rules (EIP-1559):
// newMaxFeePerGas >= oldMaxFeePerGas * 1.10
// newMaxPriorityFeePerGas >= oldMaxPriorityFeePerGas * 1.10

const newMaxFeePerGas = (oldMaxFeePerGas * 110n) / 100n;
const newMaxPriorityFeePerGas = (oldMaxPriorityFeePerGas * 110n) / 100n;
On Base, with Flashblocks at 200ms intervals and 2-second block times, a transaction not landing within 3–5 blocks (6–10 seconds) likely has insufficient priority fee for current conditions.

EVM Revert Errors

When eth_sendRawTransaction succeeds but the transaction reverts on-chain, the receipt will have status: "0x0". The revert reason is encoded in the logs or retrievable via eth_call replay.

Decoding the revert reason

Most standard reverts encode a reason string using the Error(string) ABI selector 0x08c379a0:
function decodeRevertReason(revertData: `0x${string}`): string | null {
  // Standard Error(string) revert
  if (revertData.startsWith('0x08c379a0')) {
    const encoded = revertData.slice(10); // remove selector
    const decoded = decodeAbiParameters([{ type: 'string' }], `0x${encoded}`);
    return decoded[0] as string;
  }

  // Custom error: first 4 bytes are the selector
  const selector = revertData.slice(0, 10);
  return `Custom error selector: ${selector}`;
}
To get revert data from a failed transaction, replay it via eth_call:
{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "params": [
    {
      "from": "0xYourAddress",
      "to": "0xContractAddress",
      "data": "0xOriginalCalldata",
      "gas": "0x...",
      "maxFeePerGas": "0x...",
      "maxPriorityFeePerGas": "0x..."
    },
    "latest"
  ],
  "id": 1
}
The response will contain the revert reason in the error.data field.

Common DeFi Revert Patterns

Uniswap V3 / V4

Revert messageCauseAgent action
Too little receivedSlippage exceeded — output amount below amountOutMinimumIncrease slippage tolerance OR re-quote and resubmit
Too much requestedSlippage exceeded — input amount above amountInMaximumIncrease slippage tolerance OR re-quote and resubmit
Transaction too olddeadline timestamp has passedSet deadline = block.timestamp + 60 when building tx; re-sign
STF (Swap: Too Few)Liquidity too low for the requested outputReduce trade size or split into smaller swaps
SPL (Swap: Price Limit)sqrtPriceLimitX96 constraint hitRemove price limit or adjust to current pool price
LOKPool is locked (reentrancy guard)Retry immediately — transient condition

ERC-20

Revert messageCauseAgent action
ERC20: transfer amount exceeds balanceInsufficient token balanceCheck balance before trade, reduce size
ERC20: transfer amount exceeds allowanceInsufficient approvalSubmit approve() tx first, then retry trade
ERC20: approve to the zero addressApproval target is zero addressFix contract address in approval call

USDC (Base Native)

Revert messageCauseAgent action
FiatTokenV2_2: transfer amount exceeds balanceInsufficient USDCCheck balance
FiatTokenV2_2: transfer amount exceeds allowanceUSDC allowance too lowRe-approve spender
Blacklistable: account is blacklistedAddress is blacklistedCannot be resolved programmatically
Pausable: pausedUSDC contract is pausedMonitor for unpause; do not retry automatically

Generic contract errors

SelectorErrorMeaning
0x08c379a0Error(string)Standard string revert message
0x4e487b71Panic(uint256)Arithmetic overflow/underflow, array out of bounds. Code 0x11 = overflow, 0x12 = div by zero
0xd93c0665ReentrancyReentrancy guard triggered — retry after the conflicting tx confirms

Retry Policy by Error Type

REVERT REASON                         → RETRY?   WAIT      ACTION
─────────────────────────────────────────────────────────────────
Slippage exceeded                     → YES       0s        Re-quote, widen slippage by 0.1%, resubmit
Deadline passed                       → YES       0s        Rebuild tx with fresh deadline
Transaction too old                   → YES       0s        Rebuild tx with fresh timestamp
Nonce too low                         → YES       0s        Fetch pending nonce, rebuild tx
Replacement tx underpriced            → YES       0s        Increase fees ≥10%, resubmit same nonce
Transaction underpriced               → YES       1s        Re-fetch base fee, rebuild tx
Intrinsic gas too low                 → YES       0s        Increase gas limit
Insufficient funds for gas            → NO        —         Acquire more ETH; alert operator
ERC20: allowance exceeded             → YES       ~12s      Submit approve() first, then retry
ERC20: balance exceeded               → NO        —         Reduce trade size; insufficient funds
USDC: paused                          → NO        60s+      Monitor chain; contract-level pause
Pool locked (LOK)                     → YES       200ms     Retry after one Flashblock
Custom error (unknown selector)       → NO        —         Log selector, alert operator
Panic (0x4e487b71)                    → NO        —         Logic error in contract; do not retry

Retry Implementation

interface RetryConfig {
  maxAttempts: number;
  initialDelayMs: number;
  feeIncreaseBps: number; // basis points, e.g. 1000 = 10%
}

const RETRY_DEFAULTS: RetryConfig = {
  maxAttempts: 3,
  initialDelayMs: 500,
  feeIncreaseBps: 1100, // 10% increase
};

async function submitWithRetry(
  buildTx: () => Promise<`0x${string}`>,
  config = RETRY_DEFAULTS
): Promise<`0x${string}`> {
  for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
    try {
      const rawTx = await buildTx();
      const hash = await client.sendRawTransaction({ serializedTransaction: rawTx });
      return hash;
    } catch (err: any) {
      const msg: string = err?.message ?? '';

      if (msg.includes('nonce too low')) {
        // Refetch nonce — do not increment attempts, just rebuild
        continue;
      }
      if (msg.includes('replacement transaction underpriced') ||
          msg.includes('transaction underpriced')) {
        // Increase fees and retry
        await sleep(config.initialDelayMs * (attempt + 1));
        continue;
      }
      if (msg.includes('insufficient funds') ||
          msg.includes('Pausable: paused')) {
        // Non-retryable
        throw err;
      }

      // Unknown error — exponential backoff
      await sleep(config.initialDelayMs * 2 ** attempt);
    }
  }
  throw new Error(`Transaction failed after ${config.maxAttempts} attempts`);
}