Transaction failures fall into three categories with distinct recovery strategies:
| Category | When it happens | Agent action |
|---|
| RPC rejection | Before broadcast — node rejects the tx | Fix locally, resubmit (no gas wasted) |
| Mempool eviction | After broadcast — tx dropped before inclusion | Resubmit with higher fee |
| EVM revert | During execution — tx included but failed | Diagnose revert, decide to retry or abort |
RPC Rejection Errors
These are JSON-RPC errors returned synchronously from eth_sendRawTransaction. No gas is consumed.
| Error message | Code | Cause | Agent action |
|---|
nonce too low | -32000 | A tx with this nonce already exists | Fetch nonce with eth_getTransactionCount(addr, "pending"), rebuild tx |
nonce too high | -32000 | Nonce is too far ahead of current state | Wait for pending txs to confirm; check for nonce gaps |
replacement transaction underpriced | -32000 | Replacing an existing tx with same nonce requires ≥10% higher fee | Increase maxFeePerGas and maxPriorityFeePerGas by ≥10%, resubmit |
intrinsic gas too low | -32000 | Gas limit below minimum for tx type | Increase gas limit (minimum: 21,000 for ETH transfers; higher for contract calls) |
transaction underpriced | -32000 | maxFeePerGas is below the current network base fee | Re-fetch base fee, set maxFeePerGas = baseFee * 1.5 + priorityFee |
exceeds maximum per-transaction gas limit | -32000 | Gas limit exceeds Base’s 25M gas cap | Reduce gas limit or split operation |
insufficient funds for gas * price + value | -32000 | Account ETH balance too low to cover gas + value | Check balance, reduce value or use lower fees |
transaction indexing is in progress | -32000 | Node is still indexing — try again shortly | Retry after 1–2 seconds |
invalid sender | -32000 | Signature invalid or chain ID mismatch | Verify 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 message | Cause | Agent action |
|---|
Too little received | Slippage exceeded — output amount below amountOutMinimum | Increase slippage tolerance OR re-quote and resubmit |
Too much requested | Slippage exceeded — input amount above amountInMaximum | Increase slippage tolerance OR re-quote and resubmit |
Transaction too old | deadline timestamp has passed | Set deadline = block.timestamp + 60 when building tx; re-sign |
STF (Swap: Too Few) | Liquidity too low for the requested output | Reduce trade size or split into smaller swaps |
SPL (Swap: Price Limit) | sqrtPriceLimitX96 constraint hit | Remove price limit or adjust to current pool price |
LOK | Pool is locked (reentrancy guard) | Retry immediately — transient condition |
ERC-20
| Revert message | Cause | Agent action |
|---|
ERC20: transfer amount exceeds balance | Insufficient token balance | Check balance before trade, reduce size |
ERC20: transfer amount exceeds allowance | Insufficient approval | Submit approve() tx first, then retry trade |
ERC20: approve to the zero address | Approval target is zero address | Fix contract address in approval call |
USDC (Base Native)
| Revert message | Cause | Agent action |
|---|
FiatTokenV2_2: transfer amount exceeds balance | Insufficient USDC | Check balance |
FiatTokenV2_2: transfer amount exceeds allowance | USDC allowance too low | Re-approve spender |
Blacklistable: account is blacklisted | Address is blacklisted | Cannot be resolved programmatically |
Pausable: paused | USDC contract is paused | Monitor for unpause; do not retry automatically |
Generic contract errors
| Selector | Error | Meaning |
|---|
0x08c379a0 | Error(string) | Standard string revert message |
0x4e487b71 | Panic(uint256) | Arithmetic overflow/underflow, array out of bounds. Code 0x11 = overflow, 0x12 = div by zero |
0xd93c0665 | Reentrancy | Reentrancy 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`);
}