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
           └───┬───┘
               │  USDT Transfer detected on-chain
               ▼
          ┌─────────┐
          │detected │  ← seen on chain, counting confirmations
          └────┬────┘
               │  MIN_CONFIRMATIONS reached (default: 19)
               ▼
    ┌──────────┬──────────┬──────────┐
    │confirmed │underpaid │ overpaid │  ← webhook fired
    └────┬─────┴────┬─────┴────┬─────┘
         │          │          │
         │  Webhook acknowledged (2xx)
         ▼          ▼          ▼
             ┌──────────────┐
             │callback_sent │  (transient)
             └──────┬───────┘
                    ▼
              ┌──────────┐
              │completed │  ← done
              └──────────┘

  (from pending, if expires_at is reached)
           │
           ▼
       ┌─────────┐
       │ expired │  ← order timed out
       └─────────┘

  (if webhook delivery exhausts all attempts)
    confirmed/underpaid/overpaid → callback_failed

  (if admin cancels a pending order)
    pending → cancelled

State transitions are strictly forward-only (Iron Rule 5). Every transition is appended to the order_events table, so current status is always a projection of an append-only event stream.

State descriptions ​

pending ​

The order has been created and a unique HD-derived payment address has been allocated from the pool. PayWarden is polling the blockchain for any incoming USDT transfer to this address.

The order expires ORDER_TTL seconds after creation (default: 1800 — 30 minutes) if no payment has been detected.

detected ​

A USDT Transfer matching the payment address has been seen on-chain. PayWarden now waits for MIN_CONFIRMATIONS blocks (default: 19).

At this point the payment is visible but not yet final — do not fulfill the order in this state.

confirmed ​

MIN_CONFIRMATIONS has been reached and the received amount equals the expected amount. PayWarden fires the payment.confirmed webhook. This is the state where you should fulfill the order.

underpaid ​

Confirmations reached, but amount_received is less than amount_expected. PayWarden fires the payment.underpaid webhook. Business logic for partial payments is up to your backend.

overpaid ​

Confirmations reached, but amount_received is greater than amount_expected. PayWarden fires the payment.overpaid webhook. The full received amount is swept.

callback_sent ​

Internal transient state used while the webhook is in flight / being recorded. Orders do not dwell here.

completed ​

Your webhook endpoint returned a 2xx response acknowledging a payment.confirmed / payment.underpaid / payment.overpaid event. This is the happy-path terminal state. After completed, fund sweeping is queued.

expired ​

The order's expires_at was reached before any payment was detected. The payment address is returned to the pool. Terminal.

If a customer pays an expired order's address afterwards, the transfer will be seen on-chain but the order status will not change — contact the customer to resolve manually.

callback_failed ​

Webhook delivery exhausted all retry attempts without a 2xx response. The payment was received on-chain — only the notification failed. Terminal from the order's point of view, but the admin dashboard exposes a Retry webhook action and sweep is auto-queued so funds are not blocked by a dead callback endpoint.

cancelled ​

An admin cancelled a still-pending order before any payment was detected. Terminal.

Event sourcing ​

PayWarden never mutates order status without also writing an event. Every state change is recorded as an immutable row in order_events:

order_events
────────────
id           uuid
order_id     uuid
event_type   text   -- order_created | payment_detected | payment_confirmed
                    -- | order_expired | order_cancelled
                    -- | webhook_sent | webhook_failed
                    -- | sweep_queued | sweep_completed | sweep_failed
                    -- | admin_webhook_retry | admin_sweep_retry
payload      jsonb
created_at   timestamptz

Replaying the event stream reconstructs the full audit trail — when each transition happened, what triggered it, and what payload accompanied it.

Webhook events emitted ​

Only three webhook event names are sent to your callback URL:

EventFired when
payment.confirmedConfirmations reached, amount matches
payment.underpaidConfirmations reached, amount < expected
payment.overpaidConfirmations reached, amount > expected

Internal state changes like detection, expiry, and sweeping are recorded in order_events but are not sent as webhooks.

Confirmation count ​

TRON produces blocks roughly every 3 seconds. With the default MIN_CONFIRMATIONS=19:

  • Detection latency: roughly SCAN_INTERVAL (default 3000 ms) after the tx appears in a block
  • Confirmation latency: ~60 seconds after first detection

19 matches TRON's practical finality threshold. For very high-value transactions, consider raising MIN_CONFIRMATIONS.

Expiry and amount mismatch ​

ScenarioBehavior
Payment arrives before expiry, exact amountconfirmed → webhook → completed
Payment arrives before expiry, amount < expectedunderpaid → webhook → completed (or callback_failed if delivery fails)
Payment arrives before expiry, amount > expectedoverpaid → webhook → completed, full received amount swept
No payment before expires_atexpired, address released
Payment arrives after expirySeen on-chain but order stays expired, manual resolution needed
Webhook delivery exhausts retriescallback_failed, sweep auto-queued, admin retry available

Released under the BSL 1.1 License.