On this page
Key takeaways
- "Confirmed" is a policy your business defines, not a fact the network hands you.
- The right threshold depends on the network's finality model, the value at risk, and what confirmation releases.
- Make the policy explicit per network — and per amount tier when the stakes vary.
- Only enable networks the software actually implements and tests.
Ask three engineers how many confirmations make a payment safe and you will get four numbers. The disagreement is not ignorance. It is that the question is underspecified: safe for what, on which network, for how much?
In Vellum's deployment, confirmation policy was made explicit per network. This note explains why that was the right move and how to reason about it.
Confirmed is a decision
A transaction appears on a network, then becomes progressively harder to reverse. Somewhere along that curve your business decides to treat it as paid. That point is a policy, and it trades speed against risk.
- Too early and you release goods for a payment that could still be reorganised away.
- Too late and customers wait, support tickets rise, and the checkout feels broken.
Threshold too early
- Goods released for a payment that could still reverse
- Loss lands on the business
- Fast, until it is expensive
Threshold too late
- Customers wait and support tickets rise
- Checkout feels broken
- Safe, until the customer leaves
Three variables
| Variable | Why it matters | Typical question |
|---|---|---|
| Network finality model | Networks differ in how and when a transaction becomes hard to reverse | What does finality mean on this network? |
| Value at risk | A small purchase tolerates more risk than a large settlement | What is the worst loss if this reverses? |
| What confirmation releases | Digital goods are hard to recall; a pending payout is easy to hold | What happens irreversibly when we mark it paid? |
Tiers, not a single number
A single global threshold is convenient and usually wrong. A more honest design has tiers by amount, per network. The numbers below are placeholders to show the shape — your values come from your own risk assessment and the network's characteristics.
type ConfirmationPolicy = {
network: string;
tiers: { upToAmount: number; confirmations: number }[];
};
const policy: ConfirmationPolicy = {
network: "example-network",
tiers: [
{ upToAmount: 500, confirmations: 1 },
{ upToAmount: 10_000, confirmations: 3 },
{ upToAmount: Infinity, confirmations: 6 },
],
};
export function requiredConfirmations(amount: number) {
return policy.tiers.find((t) => amount <= t.upToAmount)!.confirmations;
}Separate the states
A payment should not jump from "not seen" to "paid". Distinct states let each part of your business act at the right moment:
- Seen. The transaction is visible. Show the customer progress.
- Confirming. Confirmations are accumulating. Hold fulfilment.
- Confirmed. Policy is met. Emit the signed event; fulfil.
- Settled. Funds are where treasury expects them.
Why it matters operationally
When the states are separate, a late confirmation and a missed webhook look different on the dashboard, and the runbook can treat them differently.
Only enable what is implemented
StablePay lets you connect the chains, tokens, and RPC providers you operate, but support is limited to what is implemented and tested. We would rather ship a short list that is true than a long list that is aspirational. Anything not shipped is marked planned.
Operating the policy
- Write the policy down and get sign-off from treasury, not just engineering.
- Review it when you change networks, tokens, or typical transaction size.
- Monitor RPC health: a lagging provider looks like a slow network.
- Record which policy version applied to each payment, for later audit.
This is risk guidance from an engineering studio, not financial or legal advice. Set thresholds with your own risk owners.
Policy sign-off
- Treasury, not only engineering, has approved the thresholds.
- Each enabled network has its own written policy.
- Amount tiers are defined where the stakes vary.
- RPC health is monitored, so lag is not mistaken for a slow network.
- Each payment records which policy version applied.
"Confirmed" is a promise your business makes. Make it deliberately.
How finality actually works
"Confirmed" hides two very different ideas of finality. Understanding which one your network uses is the foundation of a sensible policy.
| Model | How it works | What a confirmation means | Example |
|---|---|---|---|
| Probabilistic finality | The chain with the most accumulated work or weight is canonical. Recent blocks can be replaced by a longer competing chain | Each additional block on top makes reversal exponentially less likely, but never strictly impossible [1] | Proof-of-work chains such as Bitcoin |
| Economic or deterministic finality | Validators vote to finalise checkpoints. Once finalised, reverting requires a large, provable economic penalty [2] | A finalised block is treated as irreversible under the protocol's assumptions | Proof-of-stake chains such as Ethereum |
On probabilistic chains, the original Bitcoin paper [1] shows that the probability of an attacker catching up falls exponentially with the number of confirmations. That is why confirmation counts are a policy: you choose the residual risk you are willing to carry for a given amount. On proof-of-stake networks, finality is a protocol event, and Ethereum's documentation [2] describes checkpoints becoming finalised after validators attest across epochs.
Read the chain's own finality signal
Where a network exposes explicit tags — for example Ethereum's
safeandfinalizedblock tags in its JSON-RPC interface [3] — a policy can key on the tag instead of counting blocks. Use whatever the network's own documentation says is the strongest available signal.
Reorganisations: designing for the block that disappears
A reorganisation happens when the network switches to a different chain tip, replacing recent blocks. A transaction that was included may be dropped and re-included later, or not at all. A payment system that never expects this will, eventually, mark something paid that is no longer there.
- Store the block hash, not just the number. A number can point to different blocks over time; the hash identifies the exact one you observed.
- Re-verify before acting. Before fulfilling, confirm the transaction is still in the canonical chain at the required depth.
- Make transitions reversible until final. A
confirmingpayment can move back toseen. Only your policy's final state is one-way. - Alert on rollbacks. A reorg that touches a payment you have already acted on is a rare, high-value signal.
async function stillCanonical(p: { blockNumber: bigint; blockHash: string }, rpc: Rpc) {
const block = await rpc.getBlockByNumber(p.blockNumber);
return block?.hash === p.blockHash; // same hash at that height => not reorganised away
}
async function advance(p: Payment, rpc: Rpc, policy: Policy) {
if (!(await stillCanonical(p, rpc))) return rollback(p); // demote to seen/unseen
const depth = (await rpc.latestBlockNumber()) - p.blockNumber;
if (depth >= BigInt(policy.requiredConfirmations(p.amount))) return confirm(p);
}Do not trust a single RPC provider
Your view of the chain is only as good as the node answering your requests. A provider that is lagging, on a minority fork, or simply wrong will make your policy wrong with it. Treat providers as unreliable inputs to be cross-checked.
| Failure | Effect on payments | Mitigation |
|---|---|---|
| Provider lags behind the head | Confirmations appear late | Compare head height across providers; alert on divergence |
| Provider returns stale data | A payment looks unconfirmed | Query a second provider before escalating |
| Provider is briefly on a fork | A transaction flickers in and out | Require agreement or use a finality tag before acting |
| Rate limits or outage | Indexer stalls | Failover to a secondary provider; alert on indexer lag |
| Compromised endpoint | False data | Run your own node for high-value networks; prefer diversity |
For higher-value flows, consider a quorum: act only when two independent providers agree on the block hash and depth. It costs an extra request and removes a whole class of failure.
Policy as code, with an audit trail
A confirmation policy written in a wiki drifts from the one running in production. Encode it as configuration in the deployment, review changes like code, and stamp every payment with the policy version that applied to it. Months later, when someone asks why a payment was released at three confirmations, the answer is a lookup, not a memory.
{
"version": "2026-09-01",
"networks": {
"example-network": {
"signal": "confirmations",
"tiers": [
{ "upToAmount": "500", "confirmations": 1 },
{ "upToAmount": "10000", "confirmations": 3 },
{ "upToAmount": null, "confirmations": 6 }
],
"providers": { "minAgreeing": 2 }
}
}
}Change control for a policy
- Changes go through review by treasury as well as engineering.
- The version is recorded on every payment that it governed.
- A new policy applies only to payments created after it takes effect.
- Loosening a threshold requires a recorded reason.
- The previous version is kept for audit.
Testing a policy before it meets real money
- 01
Unit-test the tiers
Assert the required confirmations at each boundary amount, including exactly-on-the-line values.
- 02
Replay recorded chain data
Feed historical blocks, including known reorganisations, through the indexer and assert the resulting states.
- 03
Run on a test network
Send real transactions on a public test network and watch each state transition on the dashboard.
- 04
Inject faults
Slow or block a provider and confirm that failover and alerts behave as documented.
- 05
Go live with low limits
Start with capped amounts, then raise them once a full reconciliation cycle has closed cleanly.
This is engineering guidance on system behaviour, not financial or legal advice. The thresholds that suit your business are a risk decision for your own risk owners.
Closing
"Confirmed" is a promise your business makes. Make it deliberately, per network and per amount, and write it where the next engineer can find it.
References & further reading
- 1Bitcoin: A Peer-to-Peer Electronic Cash System — Satoshi Nakamoto, 2008Section 11 calculates the probability of an attacker overtaking the honest chain.
- 2Proof-of-stake and finality (Gasper) — ethereum.org developer documentation
- 3JSON-RPC API — ethereum.org developer documentationSee the block parameter options, including the safe and finalized tags.
The product behind this post
StablePay
Self-hosted stablecoin payment infrastructure.
From $4,800 one-time license
About the authors
Layla Haddad
Principal Engineer, Payments Infrastructure
Designs the gateway architecture behind StablePay: state machines, indexing, and signing boundaries.
Mohamed Saad
Smart Contract Developer
Works on the on-chain side of payments: token transfers, event interfaces, and contract review for products that touch a chain.
Product behaviour described here reflects what is implemented and tested; anything else is marked as planned. Code samples are illustrative.
All writing