AgentCap

← Blog

When Does an Agent Payment Count? Finality, Reorgs, and Spend Tracking on Base and Solana

GetAgentCap team · published 9/28/2026

An agent's USDC payment can show up onchain within seconds and still not be final. A spend tracker has to decide how settled a transaction must be before it counts toward a budget. This post covers the commitment levels on Base and Solana, what can still change at each one, and how to build an ingest cursor that stays correct when a block gets replaced.

Seen, included, final: three different claims

A transaction that appears in a block explorer has been included in a block. It does not yet mean the block will stay in the canonical chain. Both Base and Solana show blocks to clients before those blocks are irreversible. Their RPC APIs make this explicit: you pick a commitment level or block tag, and what you get back depends on that choice.

For spend tracking this matters in both directions. Count too early and you may record a payment that later disappears. Count too late and your budget alerts run behind the agent that is spending the money.

Base: unsafe, safe, finalized

Base is an OP Stack rollup. The sequencer produces a block about every two seconds. Blocks it has produced but not yet posted to Ethereum are called unsafe. Nodes call the tip of those blocks the unsafe head, and the default 'latest' tag returns data from it.

Once the transaction data for those blocks is posted to Ethereum L1, they become safe. Once that L1 data is itself in a finalized Ethereum block, they become finalized. On Ethereum that usually takes about two epochs, roughly 13 minutes after inclusion, so a Base block usually takes somewhat longer than that to reach finalized.

Standard JSON-RPC exposes all three levels: eth_getBlockByNumber and eth_getLogs accept 'latest', 'safe' and 'finalized' as block tags. Reorgs of the unsafe head are rare in normal operation, but the protocol does not rule them out. A sequencer fault, or a batch that fails derivation, can replace unsafe blocks.

Solana: processed, confirmed, finalized

Solana RPC methods take a commitment parameter. 'processed' returns the node's most recent view, which may be on a fork the cluster abandons. 'confirmed' means a supermajority of stake has voted on the block, usually within a second or so of production. 'finalized' means the block has reached maximum lockout, with 31 or more confirmed blocks built on top of it, which typically takes around 13 seconds.

No confirmed block has been rolled back in Solana's history. Even so, 'confirmed' is an observed guarantee, not a protocol one. 'processed' can and does see transactions on forks that never finalize. A tracker reading token balance changes or transferChecked instructions at 'processed' will sometimes count payments that never settled.

Why this interacts badly with polling cursors

Most spend trackers ingest with a cursor: remember the last block or slot you processed, then ask for everything after it. If the cursor advanced past a block that was later replaced, the replacement block is never read. The orphaned transfer stays in your ledger, and a transfer that landed in the replacement block at a different height is missed.

The failure is silent. Totals look plausible, but they no longer match the chain. It usually shows up weeks later as an unexplained gap during reconciliation, and by then the RPC node may no longer serve the orphaned block for debugging.

Two-tier ingestion: provisional and settled

A practical design keeps two views. Read the head at a low commitment level ('latest' on Base, 'confirmed' on Solana) and write those rows as provisional. Separately, advance a second cursor at 'finalized' and promote rows to settled only when the finalized pass sees them.

Budget alerts can fire on provisional totals, because a false alarm costs much less than a late one. Invoicing, accounting exports and hard budget enforcement should read settled totals only. Any provisional row that the finalized pass never confirms gets dropped and logged, not silently kept.

Detecting a reorg instead of assuming none

On Base, store the block hash with every processed block number, not just the number. On each poll, check that the parent hash of the next block matches the hash you stored. On a mismatch, walk back until the hashes agree, delete the provisional rows above that point, and re-ingest. The same approach works on Solana with slot and blockhash, and with getBlock's parentSlot and previousBlockhash fields.

Log-based ingestion needs the same check. eth_getLogs results include blockHash and a removed flag. Subscription-based log streams mark reorged logs with removed: true. Treating those events as deletions is required for correctness, not an optimization.

Choosing lag per use case

Pick the commitment level per consumer, not once for the whole system. A runaway-spend alert on an x402 agent wants the lowest latency the chain offers and can accept an occasional provisional row that gets retracted. A monthly spend report wants finalized data and does not care about a 15-minute delay.

One number worth writing down for each chain: the worst-case gap between your alert view and your settled view. On Base, that is mostly how long it takes to reach Ethereum finality. On Solana, it is seconds. That gap is how long your alert totals and your settled totals can disagree, and your reconciliation job should allow for it before reporting drift.

What GetAgentCap does

GetAgentCap reads only public addresses, so every figure comes from chain data at a known commitment level. We treat head-of-chain transfers as provisional for alerting, count only finalized transfers in settled totals, and store block hashes with our cursors so a replaced block is re-read rather than skipped. If you run your own tracker, this is the smallest design we have found that stays correct when the chain changes under it.