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 eventSweep 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:
- Activates the derived account if it has never been seen on-chain (sends 0.1 TRX from the hot wallet).
- 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. - Tops up the derived address with
SWEEP_GAS_TOPUP_TRXTRX if its balance is insufficient, then waits for confirmation. - 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.
# 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 thisTIP
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_failedevent is appended to the order's event log. - The notifier emits a
sweep_failedalert (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:
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.