Skip to content

Fund Sweeping

Fund sweeping is the process of moving received USDT from individual per-order payment addresses to your cold wallet. PayWarden automates this entirely.

Why sweeping is needed

Each order uses a unique HD-derived payment address. After the payment webhook is delivered, USDT sits at that derived address. Sweeping consolidates funds to your COLD_WALLET_ADDRESS so you can:

  • Access your revenue in one place
  • Avoid managing hundreds of individual addresses
  • Retire the derived index from the active pool

The hot wallet (configured via HOT_WALLET_KEY) is used only to sign sweeps and to top up TRX for gas on the derived address. Swept funds never land in the hot wallet — they go directly to COLD_WALLET_ADDRESS.

How it works

Webhook delivered (status → completed)


[BullMQ delayed job created]  ← SWEEP_DELAY_MS (default 30000 ms)


Gas Manager: activate derived account if needed


Gas Manager: check hot wallet TRX balance
      │   (warn via notifier if below HOT_WALLET_ALERT_TRX)

Gas Manager: top up TRX on derived address if needed
      │   (sends SWEEP_GAS_TOPUP_TRX from hot wallet)

Fund Sweeper: build USDT TRC-20 transfer
      │   derived address → COLD_WALLET_ADDRESS

Sign with per-index private key
      │   (privateKey buffer zeroed immediately after signing)

Broadcast and wait for confirmation (up to 5 min)


Mark address swept (sweepTxHash, lastSweptAt)
Release address back to pool
Emit sweep_completed event

Sweep is triggered after a successful webhook delivery (markWebhookCompleted) and also auto-queued when a webhook permanently fails (markWebhookFailed) — sweep is a chain operation, orthogonal to webhook delivery, so funds are never blocked by a dead callback endpoint.

If amountReceived is below SWEEP_MIN_USDT, the sweep is skipped and the address is released back to the pool.

Gas management

TRC-20 transfers on TRON require TRX to pay for bandwidth/energy. Newly derived payment addresses receive USDT only — they have no TRX and may not even be activated.

Before each sweep, the Gas Manager:

  1. Activates the derived account if it has never been seen on-chain (sends 0.1 TRX from the hot wallet).
  2. Checks the hot wallet's own TRX balance and emits a low-balance warning through the notifier when it drops below HOT_WALLET_ALERT_TRX.
  3. Tops up the derived address with SWEEP_GAS_TOPUP_TRX TRX if its balance is insufficient, then waits for confirmation.
  4. Proceeds with the USDT sweep transaction.

WARNING

Your hot wallet must always carry enough TRX to cover activation + gas top-ups for every in-flight sweep. When the alert fires, top up the hot wallet promptly — sweeps will start failing once it runs dry.

Configuration

Sweep is disabled unless both HOT_WALLET_KEY and COLD_WALLET_ADDRESS are set. When disabled, addresses are released immediately back to the pool instead of being swept.

dotenv
# Required to enable sweeping
HOT_WALLET_KEY=<64 hex chars>         # private key used ONLY for signing sweeps & gas top-ups
COLD_WALLET_ADDRESS=TXxx...           # destination for all swept USDT

# Optional tuning (defaults shown)
SWEEP_MODE=burn                       # burn | delegate
SWEEP_DELAY_MS=30000                  # delay before sweep job runs (milliseconds)
SWEEP_MIN_USDT=0.5                    # skip sweep if amount_received is below this
SWEEP_GAS_TOPUP_TRX=20                # TRX sent to derived address for gas
HOT_WALLET_ALERT_TRX=50               # warn when hot wallet TRX drops below this

TIP

SWEEP_DELAY_MS is in milliseconds, not minutes. The default 30000 means each sweep is delayed 30 seconds after the triggering webhook event.

Sweep modes

SWEEP_MODE accepts two values:

burn (default) — Top up the derived address with TRX from the hot wallet, then broadcast the USDT transfer. Simple, works for any address. The top-up TRX is spent as gas on every sweep.

delegate — Reserved for energy-delegation based gas. The current 1.0 implementation primarily exercises the burn path; delegate is recognized by the config schema and recorded in event payloads.

Monitoring sweeps

Order events written by the sweeper:

  • sweep_queued — job added to the BullMQ queue (payload: delay_ms, mode)
  • sweep_completed — sweep tx confirmed on-chain (payload: sweep_tx_hash, amount, to, mode)
  • sweep_failed — all retries exhausted (payload: reason, attempts_made)

The admin dashboard surfaces queue state (waiting / active / failed), the hot wallet TRX balance, and per-order sweep history.

Failed sweeps

Each sweep job retries up to 3 times with exponential backoff (30 s base) before being marked failed. On permanent failure:

  • A sweep_failed event is appended to the order's event log.
  • The notifier emits a sweep_failed alert (Telegram / email if configured).
  • The derived address stays allocated so the funds can be recovered manually via the admin dashboard's Retry sweep action.

Admins can also force a retry through retrySweep(orderId), which requires the order to be in a sweep-eligible status and sweep to be enabled.

Security

Private keys for derived addresses exist in memory only during the signing step, and are wiped immediately:

typescript
const privKeyBuffer = await wallet.getPrivateKeyForSigning(order.derivationIdx);
try {
  const privKeyHex = privKeyBuffer.toString('hex');
  const signed = await tronWeb.trx.sign(transaction, privKeyHex);
  await tronWeb.trx.sendRawTransaction(signed);
} finally {
  privKeyBuffer.fill(0); // zeroed regardless of success/failure
}

The HD seed is decrypted from vault/seed.enc at startup using VAULT_KEY and held in memory for the lifetime of the process. The HOT_WALLET_KEY is a separate, standalone private key used only for gas top-ups and activation — it never touches the HD seed.

Released under the BSL 1.1 License.