Skip to content

Order Lifecycle

Every payment order goes through a defined sequence of states. Understanding this lifecycle helps you build reliable integrations.

State machine

         POST /payments


           ┌───────┐
           │pending│  ← waiting for customer payment
           └───┬───┘
               │  Transfer detected on-chain

          ┌─────────┐
          │detected │  ← seen on chain, counting confirmations
          └────┬────┘
               │  Confirmations reached (default: 19)

         ┌──────────┐
         │confirmed │  ← payment confirmed, webhook fired
         └────┬─────┘
              │  Webhook acknowledged (2xx response)

        ┌──────────┐
        │completed │  ← done ✓
        └──────────┘

  (from pending, if timer expires)


       ┌─────────┐
       │ expired │  ← order timed out
       └─────────┘

  (if webhook fails 10 times)
    confirmed → failed

States only move forward — there is no rollback. This guarantees your event log is append-only and auditable.

State descriptions

pending

The order has been created and a unique payment address has been assigned. PayWarden is polling the blockchain for any incoming USDT transfer to this address.

The order will expire after ORDER_EXPIRY_MINUTES (default: 60 minutes) if no payment is received.

detected

A USDT Transfer event has been seen on-chain matching the payment address. PayWarden is now waiting for the required number of block confirmations (CONFIRMATIONS_REQUIRED, default: 19).

At this point, the payment is visible on-chain but not yet final. Do not fulfill orders in this state.

confirmed

The required number of confirmations has been reached. The payment is final. PayWarden fires the payment.confirmed webhook to your backend.

This is the state where you should fulfill the order (ship goods, activate subscription, etc.).

completed

Your webhook endpoint returned a 2xx response, acknowledging receipt of the payment.confirmed event.

expired

The order timer ran out before any payment was received. The payment address is returned to the pool for future use.

If a customer pays to an expired order's address, PayWarden will still detect the transfer and create a late-payment event, but the order status stays expired. Contact the customer to resolve manually.

failed

PayWarden attempted to deliver the payment.confirmed webhook 10 times over ~17 minutes, but your endpoint never returned a 2xx response. The payment was received — only the notification failed.

Check your webhook endpoint logs and use the admin dashboard to manually retry failed webhooks.

Event sourcing

PayWarden never updates order status directly. Every state change is recorded as an immutable event in the order_events table:

sql
order_events
────────────
id          uuid
order_id    uuid
type        text  -- 'created' | 'detected' | 'confirmed' | 'completed' | 'expired'
data        jsonb -- { tx_hash, confirmations, amount_received, ... }
created_at  timestamptz

Current status is derived by replaying events. This gives you a complete audit trail — you can see exactly when each state transition happened and why.

Confirmation count

TRON produces blocks approximately every 3 seconds. With the default 19 confirmations:

  • Detection: ~3–15 seconds after payment broadcast
  • Confirmation: ~60 seconds after first detection

The 19-confirmation threshold matches TRON's finality standard and is the minimum recommended for USDT transfers of any size.

For very high-value transactions, consider increasing CONFIRMATIONS_REQUIRED to 27 or higher.

Expiry and overpayment handling

ScenarioBehavior
Payment arrives before expiryNormal flow → confirmed
Payment arrives after expiryLate payment event recorded, manual resolution needed
Payment amount < order amountpayment.underpaid webhook fired, order stays pending until expiry
Payment amount > order amountTreated as fully paid, overpaid amount is swept with funds
Two payments to same addressSecond payment ignored (address is retired after first order)

Released under the BSL 1.1 License.