Withdrawals

How a Tron payout is sourced, funded, signed and confirmed — pooled custodial sourcing, the master-wallet gas top-up, on-chain receipt verification, and every terminal state including the two that must never be refunded.

9 min readUpdated 3 September 2026withdrawals, trc20, trx, queue, refunds

A Tron withdrawal takes the standard Ecosystem route — validate, debit under a row lock, queue — and then diverges sharply at execution. Two things make it different from every other chain: the coins may not be at the user's own address, and the signer must be funded before it can sign.

If you have not read Energy and bandwidth, start there. This page assumes it.

The shared front half

Before anything Tron-specific happens, the withdrawal route does what it does on every chain:

  1. Validate. The destination must be a Tron address beginning with T. Decimal precision is checked against the token's precision. The withdrawal 2FA policy applies if you have enabled it.

  2. Short-circuit internal transfers. If the destination belongs to another user on this platform it is settled internally, with no on-chain transaction. Withdrawing to your own address is rejected.

  3. Estimate — informationally. For a new recipient a 1 TRX account-activation figure is recorded. A network cost is estimated: energy-based for TRC-20, bandwidth-based for native TRX. Neither is charged.

  4. Debit under a row lock. The wallet row is locked FOR UPDATE and amount + the token's platform fee is deducted in one transaction alongside a PENDING transaction row.

  5. Queue it. The transaction ID goes onto an in-memory FIFO which moves the row through PENDINGPROCESSING → a terminal state.

One pre-flight check is Tron-specific and worth knowing, because it decides whether a user is rejected early or debited and refunded later. For a native TRX withdrawal the platform signs only from the user's own address, so a missing key row is fatal and the request is rejected outright with the balance untouched. For a TRC-20 withdrawal the key row is optional at this stage, because execution can source the tokens from elsewhere.

Pooled sourcing

The awkward truth of a custodial exchange is that a user's platform balance and the coins at their own address are not the same thing. Balances arrive from internal transfers, P2P trades and trading profits without any on-chain movement, so a user can legitimately hold 500 USDT on the platform while their own Tron address holds nothing.

Tron handles this by pooling. Before signing, the service reads the signer's actual on-chain balance — for TRC-20 by calling the contract's balanceOf view directly, not by trusting an indexer — and if it is short, looks for an alternative.

The selector ranks custodial wallets by available balance for that currency on that chain, where "available" is the wallet's recorded Tron chain balance minus anything the private ledger says is already spoken for. Only wallets with usable key material are eligible. The service then reads each candidate's live on-chain balance, up to five of them in order, and signs from the first that really holds the amount.

Until Ecosystem 6.5.0 the selector also accepted a wallet's canonical platform balance as evidence, and a wallet row is one per currency across every chain. A USDT holder whose coins sat on Ethereum therefore qualified as a Tron source with nothing at the address; the master wallet paid to fund it with energy, the transfer reverted, the customer was refunded, and the same empty candidate was chosen again on the next attempt.

When an alternative is used, the platform records the draw in ecosystem_private_ledger as an offchainDifference against that wallet and decrements its recorded balance — inside a transaction that locks the row, so concurrent withdrawals sharing a source serialise rather than race. That ledger entry is what stops the same wallet being selected and drained repeatedly.

It is deliberate, it is bounded by the private ledger, and it is the mechanism that lets an exchange settle a balance that never existed on-chain under the user's own key. It also means a Tron address's on-chain balance is not a statement about who owns those coins. Read balances from the platform, not from Tronscan.

Native TRX withdrawals use the same fallback with one difference: a missing key on the user's own wallet cannot be worked around, because the pre-flight check already rejected the request. When a native withdrawal is paid from an alternative address, the ledger draw is clamped at that address's recorded balance rather than refused, the same as the TRC-20 path: refusing inside the ledger transaction after the TRX had already left kept the address selectable.

Funding the signer

Once the signing address is settled, the TRC-20 path sizes and funds it from the master wallet — simulate the transfer for its real energy cost, read the signer's live resources and the current burn prices, compute only the shortfall with 20% headroom, and top the address up to that target.

Topping up to a target rather than sending a fixed amount is what makes a re-queued withdrawal safe: the second attempt re-reads the balance, finds it already funded, and sends nothing.

If the master wallet cannot cover the top-up, the withdrawal is requeued, not failed. The row stays PENDING, the reconciliation watchdog picks it up later, and no refund is issued because nothing was attempted on-chain.

Native TRX withdrawals skip this entirely. The address being drained is the one paying the bandwidth.

Signing, broadcasting and confirming

The transfer is built with triggerSmartContract calling transfer(address,uint256) under a 100 TRX feeLimit, then signed.

Before it is broadcast, the platform writes the signed transaction's hash to the row as a pending hash. Tron transaction IDs are deterministic before broadcast — signing only appends a signature — so persisting the hash first means a crash or an HTTP timeout during broadcast leaves a recoverable row instead of one that looks like a clean failure and gets wrongly refunded.

Native TRX does the same since Tron 6.1.5. It used to sign and broadcast without recording the hash, and any exception from the broadcast call was read as "never sent", so a node that accepted the transaction and then timed out on the reply produced a FAILED row, a refund, and coins on-chain. A broadcast exception now probes the node for the pending hash for about a minute, past the transaction's own expiry: found means the withdrawal continues, definitely absent means FAILED, and an unreachable node means TIMEOUT, which is never refunded by the queue. The node's own refusal codes are read too: a signature or validation error is a clean failure, while a busy node is treated as unknown.

TronWeb returning result: true means the node accepted the transaction, nothing more. A TRC-20 transfer can be mined and still revert or run out of energy, burning resources without moving a single token.

Marking that COMPLETED would write a transaction hash, and the refund path refuses to refund a row that carries one — a permanent, silent customer loss. The platform therefore polls the on-chain receipt for up to 60 seconds and only accepts SUCCESS.

Every terminal state

State Meaning Refunded What you do
COMPLETED Receipt confirmed SUCCESS on-chain No Nothing
FAILED Reverted, out of energy, or failed before broadcast Yes Read the description; usually resources or a bad recipient
TIMEOUT Broadcast but no receipt inside 60 seconds No Look the hash up on Tronscan and resolve manually
PENDING (requeued) Master wallet short of TRX, or a top-up not yet confirmed No Fund the master wallet; the watchdog drains the backlog

Two guards protect the money, and both are worth understanding because they explain behaviour that otherwise looks wrong.

A row that already carries a transaction hash is never relabelled FAILED. The failure write is conditional on the hash being null. If it is suppressed, the log says so explicitly. The coins left the platform; refunding would mint funds.

TIMEOUT is not a failure. The transfer may well be on-chain. It is routed to manual review rather than refunded, and the reconciliation watchdog will resolve it if the receipt appears.

What to do when a payout stalls

TRC-20 withdrawals sit in PENDING and never move

Almost always an empty master wallet. Search the backend log for WITHDRAWAL_REQUEUED — the message names the master address, the balance it has and the amount it needs. Fund it; the backlog drains without intervention.

Confirm the shape by checking whether native TRX withdrawals are completing at the same time. If they are, it is gas, not Tron.

A withdrawal failed with OUT_OF_ENERGY

The transfer was mined and reverted, so nothing moved and the user was refunded. The energy simulation under-estimated, or the signer lost resources between funding and broadcast.

If it recurs on one token, that token's real cost exceeds the simulation — check whether the recipients are addresses that have never held it, which costs more energy. There is no per-token energy override; the practical response is to ensure the master wallet is comfortably funded so the 20% headroom is not the only margin.

"Private key not found for the wallet"

An old wallet that carries a Tron address but whose key row was never written — a state an earlier release could produce because the address was recorded before the key.

For TRC-20 this is survivable: the platform logs a warning and sources from an alternative custodial wallet. It only becomes fatal when no funded alternative exists, and then the queue refunds the user with no tokens moved. For native TRX it is fatal at validation, before any debit.

Wallet fetches self-heal a missing address, but not a missing key on an address that already looks valid. Affected wallets are a data-repair job, not a configuration change.

The withdrawal reports TIMEOUT

Broadcast succeeded, the receipt did not appear within 60 seconds. Take the hash from the transaction row and look it up on Tronscan.

If it succeeded, the coins are gone and the user's debit was correct — resolve the row as completed. If it reverted, the user must be refunded manually. Do not assume either; the state exists precisely because the platform could not tell.

Estimated fee looks absurd in the logs

The energy simulation executes the real transfer against real state, so it reverts when the signing address does not itself hold the tokens — which is the normal case under pooled sourcing. The service catches that, logs it, and falls back to 65,000 energy.

A REVERT opcode executed warning from the estimator is expected and harmless. It never becomes a user-facing charge.