Confirmations
Understanding blockchain confirmations and the four-stage state machine.
Blockchain confirmations are the process by which transactions become increasingly secure and irreversible. StableOps tracks payments through four distinct confirmation stages to balance speed with security.
Overview
When a payment is sent on a blockchain, it goes through multiple stages before it's considered final:
DETECTED → CONFIRMED → FINALIZED
↓ ↓
REVERTED REVERTEDEach stage represents a different level of confidence that the payment is permanent:
- DETECTED: Transaction seen in a queryable on-chain block
- CONFIRMED: Transaction has required confirmations (chain-specific)
- FINALIZED: Transaction reached StableOps' configured final confirmation depth
- REVERTED: Transaction was reversed due to reorg or failure
Why Confirmations Matter
Blockchains are distributed systems where multiple nodes compete to add blocks. This can lead to:
- Blockchain reorganizations (reorgs): A competing chain becomes longer, reversing recent blocks
- Transaction failures: Smart contract execution fails or runs out of gas
- Double-spend attempts: Malicious actors try to reverse transactions
Confirmations protect against these risks by waiting for the transaction to be buried deep enough in the blockchain that reversal becomes computationally infeasible.
Confirmation Stages
DETECTED
The transaction has been included in a block that StableOps can scan and match to a payment order. StableOps does not promote orders from mempool data alone.
On most chains this is effectively "first block seen". On some networks, StableOps may intentionally
scan slightly behind the head to avoid RPC log-index lag, so detected should be treated as
"seen on-chain" rather than "exactly 0 confirmations".
Confidence Level: Low (5-10%)
Risk:
- Transaction could be replaced (if using RBF)
- Block could be orphaned in a reorg
- Transaction could fail execution
When to Use:
- Show "Payment received, confirming..." in UI
- Update order status to "pending"
- Do not fulfill the order yet
CONFIRMED (Chain-Specific Confirmations)
The transaction has received the required number of confirmations for the specific blockchain.
Confidence Level: High (95-99%)
Times below are estimates derived from StableOps' current default thresholds and approximate block times. They are not protocol guarantees and should not be treated as an SLA.
Confirmation Requirements by Chain:
| Chain | Confirmations | Time | Rationale |
|---|---|---|---|
| Base | 2 blocks | ~4 seconds | Optimistic rollup, low reorg risk |
| Optimism | 2 blocks | ~4 seconds | Optimistic rollup, low reorg risk |
| Ethereum | 6 blocks | ~1.2 minutes | Standard for exchanges |
| Arbitrum | 1 block | ~0.3 seconds | Optimistic rollup |
| Polygon | 20 blocks | ~40 seconds | Higher reorg risk |
| BNB Chain | 7 blocks | ~21 seconds | PoSA + BEP-126 Fast Finality |
| TRON | 1 block | ~3 seconds | DPOS consensus |
| Solana | 1 slot | ~0.4 seconds | Uses confirmed slot |
Solana and Solana Devnet currently use the same thresholds: 1 slot to enter confirmed, 32
slots to enter finalized.
Risk:
- Deep reorgs are still theoretically possible
- Very rare in practice (< 0.01% of transactions)
When to Use:
- Low-value transactions (< $100)
- Digital goods delivery
- Account credits
- Time-sensitive fulfillment
FINALIZED (Final Confirmation Depth Reached)
The transaction has reached StableOps' configured final confirmation depth. StableOps treats
FINALIZED as a terminal order state and recommends it for production fulfillment, but this
depth-based product threshold is not a guarantee of protocol-level or L1 finality.
Confidence Level: Very high
Times below are estimates derived from StableOps' current default thresholds and approximate block times. Real-world timing can vary with network conditions and RPC behavior.
Final Confirmation Depths by Chain:
| Chain | Depth | Time |
|---|---|---|
| Base | 18 blocks | ~36 seconds |
| Optimism | 18 blocks | ~36 seconds |
| Ethereum | 64 blocks | ~13 minutes |
| Arbitrum | 20 blocks | ~5 seconds |
| Polygon | 128 blocks | ~4.3 minutes |
| BNB Chain | 21 blocks | ~63 seconds |
| TRON | 19 blocks | ~1 minute |
| Solana | 32 slots | ~13 seconds |
Risk: Low residual chain, RPC, and operational risk remains; use your own controls for high-value or irreversible fulfillment.
When to Use:
- High-value transactions (> $100)
- Physical goods shipment
- Irreversible actions (account upgrades, subscriptions)
- Recommended for all production fulfillment
REVERTED (Transaction Reversed)
The transaction was reversed due to a blockchain reorganization or execution failure.
Causes:
- Blockchain reorg: A competing chain became longer
- Transaction failure: Smart contract execution failed
- Insufficient gas: Transaction ran out of gas
- Receipt not found: Transaction disappeared from blockchain
Frequency: Very rare (< 0.01% of confirmed transactions)
When This Happens:
- StableOps detects the reorg or failure
- Order status changes to
REVERTED payment.revertedwebhook is sent- Address is released back to the pool (becomes
AVAILABLEfor reuse)
See the Payment Events API reference for the full payload schemas.
How to Handle:
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
if (event.type === 'payment.reverted') {
const orderId = event.data.payment_order_id
// 1. Reverse any fulfillment (if already done)
await reverseOrderFulfillment(orderId)
// 2. Notify customer
await sendEmail(customer, 'Payment failed, please try again')
// 3. Update your database
await db.orders.update({
id: orderId,
status: 'payment_failed',
reason: event.data.reason,
})
// 4. Create new payment order (optional)
const newOrder = await stableops.paymentOrders.create({
merchantOrderId: `${orderId}_retry`,
amount: originalAmount,
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
// ...
})
}
res.sendStatus(200)
})Choosing the Right Confirmation Level
Decision Matrix
| Transaction Value | Fulfillment Type | Recommended Stage | Rationale |
|---|---|---|---|
| < $10 | Digital goods | CONFIRMED | Fast, low risk |
| $10 - $100 | Digital goods | CONFIRMED | Balanced speed/security |
| $100 - $1,000 | Digital goods | FINALIZED | Higher security needed |
| > $1,000 | Any | FINALIZED | Maximum security |
| Any | Physical goods | FINALIZED | Irreversible shipping |
| Any | Account upgrades | FINALIZED | Irreversible changes |
| Any | Subscriptions | FINALIZED | Recurring billing |
Risk Tolerance
Low Risk Tolerance (Financial services, high-value goods):
- Always wait for FINALIZED
- Never fulfill on DETECTED
- Consider additional fraud checks
Medium Risk Tolerance (E-commerce, SaaS):
- FINALIZED for > $100
- CONFIRMED for < $100
- DETECTED only for UI updates
High Risk Tolerance (Gaming, low-value digital goods):
- CONFIRMED for most transactions
- FINALIZED for account changes
- Accept occasional reversals
Monitoring Confirmations
Real-Time Updates
StableOps continuously monitors the blockchain and sends webhooks as confirmations increase:
// Timeline of webhooks for a single payment
12:00:00 - payment.detected
12:00:12 - payment.confirmed
12:13:00 - payment.finalizedPolling Alternative
If you prefer polling over webhooks:
const checkPaymentStatus = async (orderId: string) => {
const order = await stableops.paymentOrders.retrieve(orderId)
switch (order.status) {
case 'detected':
console.log('Payment seen, waiting for confirmations...')
break
case 'confirmed':
console.log('Payment confirmed, waiting for finality...')
break
case 'finalized':
console.log('Payment finalized, safe to fulfill!')
await fulfillOrder(order)
break
case 'reverted':
console.log('Payment reverted, handle failure')
await handleRevert(order)
break
}
}
// Poll every 5 seconds
const interval = setInterval(() => checkPaymentStatus(orderId), 5000)Blockchain Reorganizations
What is a Reorg?
A blockchain reorganization occurs when a competing chain becomes longer than the current chain, causing recent blocks to be replaced.
Before Reorg:
Block 100 → Block 101 → Block 102 (your transaction)
↘ Block 102' (competing chain)
After Reorg:
Block 100 → Block 101 → Block 102' → Block 103'
✗ Block 102 (orphaned)Reorg Frequency by Chain
| Chain | Reorg Depth | Frequency | Notes |
|---|---|---|---|
| Ethereum | 1-2 blocks | Daily | Usually harmless |
| Ethereum | > 3 blocks | Rare | Requires investigation |
| Base | 1 block | Occasional | L2 reorgs are rare |
| Optimism | 1 block | Occasional | L2 reorgs are rare |
| Polygon | 1-5 blocks | Common | Higher reorg risk |
| BNB Chain | 1-3 blocks | Occasional | PoSA, deep reorgs rare |
| TRON | 1 block | Rare | DPOS consensus |
| Solana | 1–2 slots | Occasional | Fast confirmation |
How StableOps Handles Reorgs
- Continuous Monitoring: StableOps re-checks the transaction receipt on every confirmation; chains that expose
blockHashadditionally compare block hashes - Reorg Detection: A changed
blockHash, or a missing/failed receipt, is treated as a revert - Status Update: Order status changes to
REVERTED - Webhook Notification:
payment.revertedwebhook is sent - Address Release: Address is released back to the pool (
AVAILABLE)
Reorg Protection
StableOps implements multiple layers of reorg protection:
- Block hash verification: Compare stored
blockHashwith current chain (not used on TRON / Solana, which don't populate this field) - Receipt validation: Verify transaction receipt still exists
- Confirmation counting: Only count blocks on the canonical chain
- Final confirmation depth: Wait for the configured chain-specific depth
Best Practices
1. Always Wait for FINALIZED
// ✅ Good - wait for finality
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
if (event.type === 'payment.finalized') {
await fulfillOrder(event.data.payment_order_id)
}
res.sendStatus(200)
})
// ❌ Bad - fulfill on detection
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
if (event.type === 'payment.detected') {
await fulfillOrder(event.data.payment_order_id) // Risky!
}
res.sendStatus(200)
})2. Handle All States
const handlePaymentWebhook = async (event: WebhookEvent) => {
switch (event.type) {
case 'payment.detected':
await updateUI('Payment received, confirming...')
break
case 'payment.confirmed':
await updateUI('Payment confirmed, finalizing...')
break
case 'payment.finalized':
await fulfillOrder(event.data.payment_order_id)
break
case 'payment.reverted':
await handleRevert(event.data.payment_order_id)
break
}
}3. Show Progress to Users
// Update UI based on confirmation stage
const PaymentStatus = ({ order }) => {
switch (order.status) {
case 'detected':
return (
<div>
<Spinner />
<p>Payment received, confirming...</p>
</div>
)
case 'confirmed':
return (
<div>
<Spinner />
<p>Payment confirmed, finalizing...</p>
</div>
)
case 'finalized':
return (
<div>
<CheckIcon />
<p>Payment complete! Your order is being processed.</p>
</div>
)
}
}4. Log Everything
// Log all confirmation events for debugging
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
await db.webhookLogs.create({
type: event.type,
orderId: event.data.payment_order_id,
fromStatus: event.data.from_status,
toStatus: event.data.to_status,
timestamp: new Date(),
})
// Process event...
res.sendStatus(200)
})5. Test Reorg Scenarios
// Simulate reorg handling in tests
describe('Payment reorg handling', () => {
it('should reverse fulfillment on reorg', async () => {
// 1. Create order
const order = await createOrder()
// 2. Simulate payment detected
await handleWebhook({ type: 'payment.detected', data: order })
// 3. Simulate payment confirmed
await handleWebhook({ type: 'payment.confirmed', data: order })
// 4. Simulate reorg
await handleWebhook({ type: 'payment.reverted', data: order })
// 5. Verify fulfillment was reversed
const dbOrder = await db.orders.findOne({ id: order.id })
expect(dbOrder.status).toBe('payment_failed')
})
})Common Patterns
Progressive Fulfillment
Fulfill parts of the order at different confirmation stages:
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
const orderId = event.data.payment_order_id
switch (event.type) {
case 'payment.confirmed':
// Grant temporary access
await grantTrialAccess(orderId)
break
case 'payment.finalized':
// Upgrade to full access
await grantFullAccess(orderId)
break
case 'payment.reverted':
// Revoke all access
await revokeAccess(orderId)
break
}
res.sendStatus(200)
})Conditional Fulfillment
Choose confirmation level based on transaction value:
const shouldFulfill = (order: PaymentOrder): boolean => {
const amount = parseFloat(order.amount)
if (amount < 100) {
// Low value: fulfill on confirmed
return order.status === 'confirmed' || order.status === 'finalized'
} else {
// High value: wait for finalized
return order.status === 'finalized'
}
}Next Steps
- Payment Orders - Understanding payment orders
- Webhooks - Receiving real-time notifications
- Integration Guides - Framework-specific examples
How is this guide?
Last updated