StableOps
Concepts

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    REVERTED

Each stage represents a different level of confidence that the payment is permanent:

  • DETECTED: Transaction seen on blockchain (0 confirmations)
  • CONFIRMED: Transaction has required confirmations (chain-specific)
  • FINALIZED: Transaction is irreversible (finality reached)
  • 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 (0 Confirmations)

The transaction has been broadcast and seen in the mempool or included in the latest block.

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

Example:

// Webhook: payment.detected
{
  "type": "payment.detected",
  "data": {
    "payment_order_id": "po_abc123",
    "from_status": "created",
    "to_status": "detected",
    "reason": "deposit_detected",
    "normalized_event_id": "evt_abc123",
    "amount": "10.00",
    "settlement_asset": "USDC",
    "metadata": {}
  }
}

CONFIRMED (Chain-Specific Confirmations)

The transaction has received the required number of confirmations for the specific blockchain.

Confidence Level: High (95-99%)

Confirmation Requirements by Chain:

ChainConfirmationsTimeRationale
Base2 blocks~4 secondsOptimistic rollup, low reorg risk
Ethereum6 blocks~1.2 minutesStandard for exchanges
Arbitrum1 block~0.3 secondsOptimistic rollup
Polygon20 blocks~40 secondsHigher reorg risk
TRON1 block~3 secondsDPOS consensus

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

Example:

// Webhook: payment.confirmed
{
  "type": "payment.confirmed",
  "data": {
    "payment_order_id": "po_abc123",
    "from_status": "detected",
    "to_status": "confirmed",
    "reason": "confirmations_reached",
    "normalized_event_id": "evt_abc123",
    "amount": "10.00",
    "settlement_asset": "USDC",
    "metadata": {}
  }
}

FINALIZED (Finality Reached)

The transaction has reached finality and cannot be reversed under any circumstances.

Confidence Level: Absolute (100%)

Finality Requirements by Chain:

ChainFinalityTimeMechanism
Base18 blocks~36 secondsL1 finality
Ethereum64 blocks~13 minutesCasper FFG
Arbitrum20 blocks~5 secondsL1 finality
Polygon128 blocks~4.3 minutesCheckpoint finality
TRON19 blocks~1 minuteDPOS finality

Risk: None - payment is guaranteed

When to Use:

  • High-value transactions (> $100)
  • Physical goods shipment
  • Irreversible actions (account upgrades, subscriptions)
  • Recommended for all production fulfillment

Example:

// Webhook: payment.finalized
{
  "type": "payment.finalized",
  "data": {
    "payment_order_id": "po_abc123",
    "from_status": "confirmed",
    "to_status": "finalized",
    "reason": "finality_reached",
    "normalized_event_id": "evt_abc123",
    "amount": "10.00",
    "settlement_asset": "USDC",
    "metadata": {}
  }
}

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:

  1. StableOps detects the reorg or failure
  2. Order status changes to REVERTED
  3. payment.reverted webhook is sent
  4. Address is released back to the pool (becomes AVAILABLE for reuse)

Example:

// Webhook: payment.reverted
{
  "type": "payment.reverted",
  "data": {
    "payment_order_id": "po_abc123",
    "from_status": "confirmed",
    "to_status": "reverted",
    "reason": "blockchain_reorg",
    "normalized_event_id": "evt_abc123",
    "amount": "10.00",
    "settlement_asset": "USDC",
    "metadata": {}
  }
}

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,
      // ...
    })
  }
  
  res.sendStatus(200)
})

Choosing the Right Confirmation Level

Decision Matrix

Transaction ValueFulfillment TypeRecommended StageRationale
< $10Digital goodsCONFIRMEDFast, low risk
$10 - $100Digital goodsCONFIRMEDBalanced speed/security
$100 - $1,000Digital goodsFINALIZEDHigher security needed
> $1,000AnyFINALIZEDMaximum security
AnyPhysical goodsFINALIZEDIrreversible shipping
AnyAccount upgradesFINALIZEDIrreversible changes
AnySubscriptionsFINALIZEDRecurring 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.finalized

Polling 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

ChainReorg DepthFrequencyNotes
Ethereum1-2 blocksDailyUsually harmless
Ethereum> 3 blocksRareRequires investigation
Base1 blockOccasionalL2 reorgs are rare
Polygon1-5 blocksCommonHigher reorg risk
TRON1 blockRareDPOS consensus

How StableOps Handles Reorgs

  1. Continuous Monitoring: StableOps checks blockHash on every confirmation
  2. Reorg Detection: If blockHash changes, a reorg is detected
  3. Status Update: Order status changes to REVERTED
  4. Webhook Notification: payment.reverted webhook is sent
  5. Address Release: Address is released back to the pool (AVAILABLE)

Reorg Protection

StableOps implements multiple layers of reorg protection:

  • Block hash verification: Compare stored blockHash with current chain
  • Receipt validation: Verify transaction receipt still exists
  • Confirmation counting: Only count blocks on the canonical chain
  • Finality tracking: Wait for chain-specific finality guarantees

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

On this page