Research · Deep dive · Deposit finality

When is a stablecoin payment final?

A chain-by-chain guide for backend engineers: what “confirmed” really means on Ethereum, Base, Arbitrum and Solana, and a crediting policy you can defend to an auditor.

Every stablecoin deposit flow has to answer one question: when is this money ours? Answer too early and a reorg, a dropped transaction or a lagging RPC node reverses a payment you already honoured. Answer too late and customers stare at a spinner for half an hour. Most integrations pick a number of confirmations from an old blog post and never revisit it.

This guide replaces the magic number with a model: three states every deposit moves through, what each major chain actually guarantees, and a crediting policy you can defend to an auditor.

No. 01Three states, not one “confirmed”

“Confirmed” means different things on different chains, and different things to different RPC calls on the same chain. Model each deposit with three explicit states instead:

  • Seen. The transaction is in a block your node knows about. It can still be reorganised away or, on rollups, never make it to the base layer. Show it to the user as pending; never let it be spent.
  • Safe. The chain’s consensus has made reversal unlikely but not impossible: a justified checkpoint on Ethereum, batch data posted to L1 on a rollup, a supermajority vote on Solana. Good enough for low-value, low-risk actions if you cap your exposure.
  • Final. Reversal would require breaking the chain’s security assumptions: on Ethereum, burning at least a third of all staked ETH. Spendable balance, payouts and irreversible fulfilment belong here.

Store the state and the block hash you observed with every deposit. Transitions only move forward, and the block hash lets you detect when the block you saw is no longer canonical.

No. 02Ethereum mainnet

Ethereum proof-of-stake produces a slot every 12 seconds and groups 32 slots into an epoch. A checkpoint becomes justified once validators holding two-thirds of all staked ETH attest to it, and finalized once the next checkpoint is justified on top of it. In normal conditions a checkpoint finalizes about two epochs (12.8 minutes) after it is proposed. A transaction typically reaches finalized 13 to 19 minutes after inclusion, depending on where in the epoch it landed.

The JSON-RPC API exposes all three states directly as block tags:

  • latest: the head your node follows. Short reorgs of one or two blocks still happen here.
  • safe: the most recent justified block. Reorgs below it are very unlikely.
  • finalized: the most recent finalized block.

Use the tags rather than counting blocks. A fixed “wait 12 confirmations” rule has no relationship to Ethereum’s actual finality, and during a finality delay, like the two that hit mainnet on 11 and 12 May 2023, it will happily credit blocks that are not final.

Watch for

Finality can stall if too many validators go offline. Alert when the finalized block stops advancing for more than a few epochs, and queue deposits instead of auto-crediting while it does. This is not only an Ethereum problem: on 10 September 2025 a node bug left Polygon PoS’s fast-finality milestones lagging by 10 to 15 minutes until a hard fork fixed it.

No. 03Base, OP Stack and Arbitrum

Rollups make the three states explicit, because their fast blocks come from a single sequencer and only become trustworthy once they are anchored to Ethereum.

OP Stack and OP-Stack-compatible chains (OP Mainnet, Base and others)

Base left the OP Stack codebase in February 2026 for its own, but says it stays compatible with the OP Stack specification and its RPCs, so the same three states apply.

  • Unsafe: blocks the sequencer has produced and gossiped, every couple of seconds. They have no L1 backing yet. This is what latest returns. Base’s Flashblocks go one step earlier, with pre-confirmations about every 200 milliseconds. Treat those as seen at most.
  • Safe: blocks whose transaction data has been posted to Ethereum in a batch and derived from L1. Returned by the safe tag. A safe block can still be reorged if the L1 block holding its batch is reorged.
  • Finalized: safe blocks whose L1 batch is itself in a finalized Ethereum block. Returned by finalized. Expect tens of minutes in normal operation: batch posting interval plus Ethereum finality.

Arbitrum One

The same shape. The sequencer gives instant “soft” confirmations; transactions become safe and final as their batch is posted to and finalized on Ethereum, and Nitro nodes expose this through the same safe and finalized tags.

Don’t confuse finality with the withdrawal challenge window. The roughly one-week withdrawal period on optimistic rollups (about 7 days on OP Stack chains, about 6.4 days on Arbitrum One) governs moving funds back to L1; it does not affect whether a USDC transfer inside the rollup is final for your ledger.

Practical policy

On rollups the sequencer rarely reorders its own blocks, so many teams credit small amounts at unsafe with an exposure cap and move to spendable at finalized. That is a business decision, not a default: write it down and size the cap.

No. 04Solana

Solana produces a slot roughly every 400 milliseconds and lets you choose a commitment level on every RPC call:

  • processed: the node has seen the block. It may be on a fork that gets abandoned.
  • confirmed: a supermajority of stake has voted on the block (optimistic confirmation). Reversal would require a large share of validators to act maliciously.
  • finalized: the block is rooted: enough subsequent blocks have been voted on that it can no longer be rolled back. Typically reached in about 12.8 seconds under the current TowerBFT consensus. The Alpenglow upgrade, approved in 2025 and scheduled for mainnet in late 2026, targets finality around 150 milliseconds, so check current docs.

The common bug is not the choice of level but its inconsistency: a watcher reads balances at processed while the crediting logic assumes finalized. Pass the commitment explicitly on every call; don’t rely on the client library’s default.

getSignatureStatuses takes no commitment parameter. It reports how far the transaction has got. A status alone does not prove the deposit, so once it is final, fetch the transaction with getTransaction(signature, { commitment: "finalized", maxSupportedTransactionVersion: 0 }) and check the mint, destination and amount.

// @solana/web3.js v1 (@solana/kit: rpc.getSignatureStatuses(...).send())
const { value } = await connection.getSignatureStatuses([signature], {
  searchTransactionHistory: true,
});
const status = value[0];
if (!status) return "unknown"; // not landed yet: retry until the blockhash expires
if (status.err) return "failed";
// confirmationStatus is optional; treat undefined as "seen"
return status.confirmationStatus === "finalized" ? "final"
     : status.confirmationStatus === "confirmed" ? "safe"
     : "seen";

No. 05Polygon, Tron and everything else

Finality models change with protocol upgrades. Polygon PoS, for example, was known for reorgs more than 100 blocks deep in 2022 and 2023. Its Heimdall v2 upgrade (July 2025, moving to CometBFT) cut milestone finality to about 5 seconds, and the Rio hardfork (October 2025) changed block production to remove reorg risk. Tron treats a block as irreversible once it is “solidified” by a supermajority of its super representatives. Numbers that were right two years ago are wrong today.

For any chain you accept, answer three questions from current, primary documentation and record the answers in your crediting policy:

  1. What RPC call or tag returns only final blocks, and how is that finality achieved?
  2. What is the chain’s recent reorg and outage history?
  3. Who can halt, censor or reorder transactions: a sequencer, a small validator set, a foundation?

If you can’t answer the first question, you can’t safely credit deposits on that chain from your own node: let a provider take that risk, or don’t support the chain.

No. 06A crediting policy you can defend

Finality isn’t a single threshold; it’s a trade between user experience and the money you are prepared to lose to a reversal. Make that trade explicit:

ActionMinimum stateNotes
Show “payment received”SeenLabel it pending. Costs nothing if reversed.
Unlock a digital good or low-value orderSafeOnly within a per-user and global cap on unfinalized exposure.
Credit spendable balanceFinalAnything that can be withdrawn or paid onward.
Ship physical goods, release payoutsFinalIrreversible on your side, so irreversible on-chain first.

The exposure cap is the important part. Sum every amount you have honoured below final; if a reorg reversed all of it at once, could the business absorb it? If not, lower the cap or raise the state.

No. 07Providers, RPC nodes and who owns the risk

If you accept stablecoins through a payment provider and act only on its re-fetched payment status, the provider has already decided when the payment counts, and in most cases carries the loss if it was wrong. Don’t reimplement finality on top; do make sure you act on the provider’s final status, not an early “pending” event.

The moment you read the chain yourself (your own deposit addresses, your own watcher) finality is your problem, and so is your RPC provider:

  • Stale nodes. A node that has fallen behind still answers. Reject responses whose latest block timestamp is more than a minute or two old.
  • Inconsistent providers. Two providers can disagree about the finalized head for a short time. Use the lower of the two.
  • Single points of trust. For high-value deposits, require two independent providers to return the same receipt and block hash before crediting.
// ethers v6: credit only when the receipt is at or below the finalized head
// on at least two independent providers, its block is canonical at that
// height, and all providers agree on the block hash. Any provider error
// rejects the promise: catch it and treat the deposit as not final.
// status === 1 does not prove the right token reached the right address:
// validate the Transfer log separately.
async function isFinal(providers, txHash) {
  if (providers.length < 2) throw new Error("need at least two providers");
  const results = await Promise.all(providers.map(async (p) => {
    const [receipt, head] = await Promise.all([
      p.getTransactionReceipt(txHash),
      p.getBlock("finalized"),
    ]);
    if (!receipt || receipt.status !== 1 || !head
        || receipt.blockNumber > head.number) return null;
    const block = await p.getBlock(receipt.blockNumber);
    return block && block.hash === receipt.blockHash ? receipt.blockHash : null;
  }));
  return results.every((hash) => hash !== null && hash === results[0]);
}

ChecklistFinality, in eight checks

  • Every deposit is stored with an explicit state (seen, safe, final) and the block hash observed.
  • State transitions only move forward; a changed block hash triggers a reorg path, not a silent update.
  • Ethereum and rollups use the safe and finalized tags, not a confirmation count.
  • Solana calls pass an explicit commitment level everywhere.
  • Every supported chain has a written answer to the three questions above.
  • Unfinalized exposure is capped per user and globally, and the cap is monitored.
  • Stale RPC responses are rejected; high-value credits need two providers to agree.
  • An alert fires when the finalized head stops advancing.

Want a second pair of eyes?

I run this review on your code, before launch.

Fixed scope, fixed fee, a written report with fixes, and a re-test once you’ve shipped them. Most reviews take five to ten working days.