2026-10-27

How Many Confirmations Does a Crypto Payment Need? Why Our Checkout Shows Three Steps

A customer pays, the transaction shows up on a block explorer two seconds later, and the merchant wants to know why the order isn't marked as paid yet.

The short answer: seeing a transfer isn't the same as it being final. The longer answer is about confirmation depth, and about showing that wait to a person without making it feel broken.


Seen is not final

When a transfer lands in a block, that block can still be replaced. On most chains a short reorganization, where the network settles on a different version of the last few blocks, is rare but real. Base, Optimism and Arbitrum are L2s with a single sequencer, and short reorgs during a sequencer hiccup have actually happened. Polygon and BNB Chain have both had deep ones.

If a payment provider marks a charge as paid on the first sighting, it opens a door. Build a clean history, then send the one payment that gets reorganized away, after the goods are already out the door.

So Klap doesn't credit a transfer until it's buried under enough blocks. Until then it's recorded as a sighting, not a payment. If its block disappears in a reorg, the sighting just stops showing up in later scans and is never credited. No reversal logic needed.


How deep is deep enough

There's no single right number, because block times vary wildly between chains. Klap starts from a target safety window (about 30 seconds on the L2s) and divides it by each network's own block time, measured live rather than assumed.

Network Default depth (blocks) Why
Arc 1 BFT consensus: a committed block is final
Avalanche 5 Snowman reaches finality in about a second
Ethereum 12 PoS L1, the common convention
Base 15 Single-sequencer L2, ~30s window
Optimism 15 Same architecture as Base
BNB Chain 70 Conservative after a real 900+ block reorg in 2021
Arbitrum 120 Same ~30s window, much faster blocks
Polygon 150 Conservative given past deep reorgs

Arc is the outlier. It runs Malachite, a Tendermint-style BFT consensus, so a block is final the moment two thirds of validators commit to it. Circle's own integration docs say one confirmation is enough, and that's what Klap requires. That's a big part of why we added Arc.

Those are defaults. When a transfer is detected, Klap also looks at the paying wallet's history, and a wallet with enough of it can get a shorter wait. Never below a per-network floor, though: 5 blocks on Base, 40 on Arbitrum, 50 on Polygon. Reorg risk belongs to the block, not to whoever sent the transfer, so no amount of trust removes the check.

Test charges use a flat 1 block on every network. There's no real money on a testnet for a reorg to steal, and fast feedback matters more while you're building an integration.


Streaming the wait instead of hiding it

A 150-block wait on Polygon is a few minutes. That's fine if people can see it moving, and terrible if all they see is a spinner.

Klap's charge event stream (GET /v1/charges/{id}/events, Server-Sent Events) sends a second kind of event while a detected transfer is still short of its depth:

import { ConfirmationProgressSchema } from '@klappay/types'
// event: confirmation_progress
// data: { "network": "base", "blocksSeen": 6, "blocksRequired": 15, "percent": 40 }
ConfirmationProgressSchema.parse(JSON.parse(data))

One detail matters here: percent never reaches 100. The moment a transfer is deep enough, it's credited and the next event: charge arrives with the new status. That event is the "done" signal, not the progress bar.


Three steps, one at a time

The hosted checkout and Klap One now turn those signals into three steps, side by side:

  1. Sending → Sent. The wallet returned a transaction hash. A manual "I already paid" has no hash, so this step stays active until Klap sees the transfer.
  2. Verifying → Verified. Klap saw the transfer on-chain, checked the paying wallet's history, and reported its first confirmation progress.
  3. Confirming → Confirmed. The bar fills from percent, and the step flips only when the charge is actually confirmed.

Each step is driven by a real event from Klap, not a timer pretending to be progress.

The first version had a problem on fast networks. On Arc, where one block is final, there's no progress event at all: the charge goes straight to confirmed, and two bars would load at the same time, which looked like a glitch. Now the checkout tracks what Klap reported separately from what it's showing, and plays the steps in order. Each step stays active for at least 600ms, fills to 100%, and only then hands off to the next. A slow network shows the real wait, and a fast one still reads as three distinct steps.


If you build your own checkout

  • Treat the first sighting as "processing", never as "paid".
  • Subscribe to the event stream instead of polling, and render confirmation_progress as a progress bar per network.
  • Wait for event: charge with a confirmed status before you fulfill the order. That, or the charge.confirmed webhook, is the only signal that counts.

Confirmations are the part of crypto payments that feel slow for a good reason. The fix isn't to skip them. It's to show them honestly.

The charges docs cover the event stream in full, and checkout-kit is the place to start if you're building your own payment UI.