Skip to main content
This page describes the complete lifecycle for an agent submitting a trade on Base, with decision points at each stage.

Overview: Transaction Lifecycle

1. SIMULATE    → eth_call / eth_simulateV1    (catch reverts before spending gas)
2. ESTIMATE    → eth_estimateGas              (get accurate gas limit)
3. FEE CALC    → eth_feeHistory + GasPriceOracle (compute maxFeePerGas)
4. SIGN        → local or remote signer
5. SUBMIT      → eth_sendRawTransaction
6. MONITOR     → eth_getTransactionReceipt (poll) or eth_subscribe newHeads (WS)
7. RECOVER     → replace (same nonce, higher fees) or cancel (zero-value self-send)
The most important step is simulation. Agents that simulate before submitting eliminate the largest class of wasted gas: transactions that were always going to revert.

Step 1 — Simulate

Always simulate before signing. Simulation detects:
  • Insufficient token allowance
  • Slippage bounds that will fail at current price
  • Contract logic errors (incorrect calldata, wrong function selector)
  • Balance/state conditions not met

Standard simulation with eth_call

{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "params": [
    {
      "from": "0xYourAgentAddress",
      "to": "0xTargetContract",
      "data": "0xEncodedCalldata",
      "gas": "0x186A0"
    },
    "latest"
  ],
  "id": 1
}
If the call succeeds, result contains the return data. If it reverts, the error contains data with the revert payload — decode it as described in Transaction Error Handling.

Simulation against preconfirmed state (Flashblocks)

For trades where current mempool state matters (e.g., a swap whose price depends on other pending transactions), simulate against the "pending" block via a Flashblocks endpoint:
{
  "jsonrpc": "2.0",
  "method": "eth_call",
  "params": [
    {
      "from": "0xYourAgentAddress",
      "to": "0xUniswapRouter",
      "data": "0xEncodedSwapCalldata"
    },
    "pending"
  ],
  "id": 1
}
This uses the Flashblocks endpoint (https://mainnet-preconf.base.org) and reflects state from the most recent Flashblock — up to 200ms fresher than "latest".

Multi-call simulation with eth_simulateV1

eth_simulateV1 lets you test a sequence of calls atomically with optional state overrides — useful when your trade involves multiple steps (approve + swap):
{
  "jsonrpc": "2.0",
  "method": "eth_simulateV1",
  "params": [
    {
      "blockStateCalls": [
        {
          "calls": [
            {
              "from": "0xYourAddress",
              "to": "0xUSDC",
              "data": "0x095ea7b3000000000000000000000000UniswapRouter...ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
            },
            {
              "from": "0xYourAddress",
              "to": "0xUniswapRouter",
              "data": "0xEncodedSwapCalldata"
            }
          ]
        }
      ],
      "traceTransfers": true,
      "validation": false
    },
    "pending"
  ],
  "id": 1
}
Set validation: false to test state mutations without requiring valid signatures.

Step 2 — Estimate Gas

After confirming the simulation succeeds, get an accurate gas limit:
{
  "jsonrpc": "2.0",
  "method": "eth_estimateGas",
  "params": [
    {
      "from": "0xYourAddress",
      "to": "0xContract",
      "data": "0xCalldata"
    }
  ],
  "id": 1
}
eth_estimateGas reflects state at the time of the call. By the time your transaction executes, state may have changed (e.g., price moved, other swaps executed). Always add a gas buffer.
Recommended gas buffer by transaction type:
Transaction typeBuffer
Simple ETH transfer0% (fixed 21,000 gas)
ERC-20 transfer10%
DEX swap (Uniswap V3)20%
DEX swap (multi-hop)25%
Complex DeFi (multi-step)30%
const gasEstimate = await client.estimateGas({ account, to, data });
const gasLimit = (gasEstimate * 120n) / 100n; // 20% buffer

Step 3 — Compute Fees

See Fee Estimation for Agents for the full guide. Quick reference:
// Fetch next base fee and priority fee percentiles
const feeHistory = await client.getFeeHistory({ blockCount: 5, rewardPercentiles: [50, 90] });
const nextBaseFee = feeHistory.baseFeePerGas[feeHistory.baseFeePerGas.length - 1]!;

// For standard inclusion: 50th percentile priority fee, 2x base fee buffer
const maxPriorityFeePerGas = median(feeHistory.reward!.map(r => r[0]));
const maxFeePerGas = nextBaseFee * 2n + maxPriorityFeePerGas;

Step 4 — Submit

const hash = await walletClient.sendTransaction({
  to: contractAddress,
  data: calldata,
  gas: gasLimit,
  maxFeePerGas,
  maxPriorityFeePerGas,
  nonce, // always set explicitly; see Nonce Management
  chainId: 8453, // Base Mainnet; 84532 for Sepolia
});
Always set chainId explicitly. An incorrect chain ID produces an invalid signature that will be rejected with invalid sender.

Step 5 — Monitor

Option A: Polling (HTTP)

async function waitForReceipt(
  hash: `0x${string}`,
  timeoutMs = 30_000,
  pollIntervalMs = 500
): Promise<TransactionReceipt> {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const receipt = await client.getTransactionReceipt({ hash }).catch(() => null);
    if (receipt) return receipt;
    await sleep(pollIntervalMs);
  }
  throw new Error(`Transaction ${hash} not confirmed after ${timeoutMs}ms`);
}
Recommended polling interval: 500ms (aligns with Flashblocks cadence, won’t miss inclusion).

Option B: WebSocket subscription

const unwatch = client.watchBlockNumber({
  onBlockNumber: async (blockNumber) => {
    const receipt = await client.getTransactionReceipt({ hash }).catch(() => null);
    if (receipt) {
      unwatch();
      handleReceipt(receipt);
    }
    if (blockNumber > submittedAtBlock + 10n) {
      unwatch();
      handleStuckTransaction(hash, lastKnownNonce);
    }
  },
});

Interpreting the receipt

if (receipt.status === 'success') {
  // Transaction included and executed successfully
} else {
  // status === 'reverted': transaction included but execution failed
  // Re-simulate at the inclusion block number to get the revert reason
  const revertData = await client.call({
    ...originalTxParams,
    blockNumber: receipt.blockNumber,
  });
}

Step 6 — Recovery

Replace a pending transaction (speed up or modify)

Submit a new transaction with the same nonce and fees increased by at least 10%:
async function replaceTransaction(
  originalNonce: number,
  newTxParams: TransactionParams,
  currentMaxFeePerGas: bigint,
  currentMaxPriorityFeePerGas: bigint
) {
  const bumpedMaxFeePerGas = (currentMaxFeePerGas * 115n) / 100n;         // +15%
  const bumpedPriorityFee = (currentMaxPriorityFeePerGas * 115n) / 100n; // +15%

  return walletClient.sendTransaction({
    ...newTxParams,
    nonce: originalNonce,           // same nonce replaces the original
    maxFeePerGas: bumpedMaxFeePerGas,
    maxPriorityFeePerGas: bumpedPriorityFee,
  });
}

Cancel a pending transaction

Send a zero-value self-transfer with the same nonce to effectively cancel:
async function cancelTransaction(nonce: number, agentAddress: `0x${string}`) {
  // Use high fees to ensure this cancellation lands before the original
  const { maxFeePerGas, maxPriorityFeePerGas } = await computeOptimalFees('urgent', 0);

  return walletClient.sendTransaction({
    to: agentAddress,           // self-transfer
    value: 0n,
    nonce,                      // same nonce as the tx to cancel
    maxFeePerGas: (maxFeePerGas * 150n) / 100n,
    maxPriorityFeePerGas: (maxPriorityFeePerGas * 150n) / 100n,
    gas: 21000n,                // minimum for ETH transfer
  });
}

Flashblocks Timing Model

Understanding when your transaction will land:
Time 0ms:    Flashblock N seals — transactions in it are preconfirmed
Time 0–200ms: New Flashblock N+1 being built — submit now for N+1
Time 200ms:  Flashblock N+1 seals
...
Time 2000ms: Full block seals, containing all 10 Flashblocks
Key implication: A transaction submitted immediately after a Flashblock seals has ~200ms to land in the next one. A transaction submitted 150ms into the window may miss the next Flashblock if processing is delayed. For latency-critical agents, maintain a WebSocket connection and submit immediately after observing a new Flashblock via newFlashblocks subscription.
// Subscribe to Flashblock events on a Flashblocks-aware endpoint
const ws = new WebSocket('wss://mainnet-preconf.base.org');
ws.onopen = () => {
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'eth_subscribe',
    params: ['newFlashblocks'],
  }));
};
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.method === 'eth_subscription') {
    // New Flashblock sealed — window opens for next one
    onFlashblockSeal(msg.params.result);
  }
};

Complete Agent Decision Tree

START


SIMULATE tx via eth_call("pending")

  ├─ REVERT: slippage → re-quote, adjust amountOutMin, retry
  ├─ REVERT: allowance → approve(), then retry
  ├─ REVERT: other → log selector, abort


ESTIMATE gas + add buffer


CHECK L1 fee ratio (via GasPriceOracle)

  ├─ L1 fee > 80% of total AND trade < threshold → DEFER


COMPUTE fees (eth_feeHistory, urgency level)


SUBMIT eth_sendRawTransaction

  ├─ RPC ERROR → see error handling guide


MONITOR (poll every 500ms, timeout after 30s)

  ├─ receipt.status = success → DONE
  ├─ receipt.status = reverted → re-simulate at block, log reason
  ├─ timeout → REPLACE with +15% fees, same nonce
  │            └─ second timeout → CANCEL (self-transfer, same nonce)


END