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.