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 → failedStates 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:
order_events
────────────
id uuid
order_id uuid
type text -- 'created' | 'detected' | 'confirmed' | 'completed' | 'expired'
data jsonb -- { tx_hash, confirmations, amount_received, ... }
created_at timestamptzCurrent 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
| Scenario | Behavior |
|---|---|
| Payment arrives before expiry | Normal flow → confirmed |
| Payment arrives after expiry | Late payment event recorded, manual resolution needed |
| Payment amount < order amount | payment.underpaid webhook fired, order stays pending until expiry |
| Payment amount > order amount | Treated as fully paid, overpaid amount is swept with funds |
| Two payments to same address | Second payment ignored (address is retired after first order) |