Addresses and deposits
How Tron deposit addresses are generated, which TronGrid endpoints the platform polls for native TRX and TRC-20 transfers, what gets filtered as dust, and why a Tron deposit credits without waiting for confirmations.
A Tron deposit address is not derived the way an EVM one is, and a Tron deposit is not confirmed the way a Bitcoin one is. Both differences have operational consequences, so this page walks the whole path from address issuance to a credited balance.
How an address is generated
The first time a user opens the deposit page for a Tron-backed currency,
Ecosystem finds or creates their ECO wallet, looks up every active token with
that currency symbol, and asks the Tron service for an address if the wallet's
address map has no usable TRON entry.
The service generates a fresh BIP-39 mnemonic for each address, derives a key
at m/44'/195'/0'/0/0, and converts it to a base58 Tron address. The mnemonic,
public key, private key and derivation path are serialised, encrypted with the
Ecosystem vault key, and written to a wallet_data row.
On EVM chains, every user address is an index under the master wallet's HD
material, which is why deleting a master wallet destroys the ability to spend
from user addresses. Tron does not work that way — each address carries its own
independent seed and the stored index is always 0.
The good news: losing the Tron master wallet row does not orphan user funds. The bad news is unchanged — losing the vault key or passphrase makes every one of those independent seeds permanently unreadable, and there are now many of them rather than one. See the vault.
Two more properties worth internalising:
One address per wallet, and a wallet is per currency. A user's USDT wallet and their TRX wallet each get their own Tron address. They are unrelated keys. This is normal and correct; it is also why a user may show you two different Tron addresses on the same account.
The wallet_data row is keyed by currency. A native wallet's row carries
currency TRX; a TRC-20 wallet's row carries the token's currency, USDT for
instance. The withdrawal path looks the key up by (walletId, chain) alone,
which is unambiguous because a wallet row is already per currency.
Self-repair on read
Every wallet fetch re-checks the address map and regenerates an entry that is
missing, that has no address, or — specific to Tron — that holds an address which
does not start with T. A 0x… value there is a leftover from an old release
that derived Tron addresses through the EVM path, and such an address can never
receive TRC-20 tokens.
That regeneration is a silent, correct self-heal. It is not a symptom of anything and it does not need investigating.
The two detection endpoints
Native TRX and TRC-20 transfers are found through different TronGrid REST
endpoints with different shapes, different filters and different confirmation
semantics. Everything below is a direct REST call carrying the
TRON-PRO-API-KEY header when TRON_API_KEY is set.
Native TRX. The account transactions endpoint, restricted to incoming
transfers and capped at 50 records. Only records whose contract type is
TransferContract are considered, and only those whose decoded recipient is
exactly the watched address. Amounts arrive in sun and are divided by 1,000,000.
TRC-20. The account TRC-20 transfers endpoint, requesting confirmed records only, ordered newest first, capped at 50, and filtered to the specific contract being monitored. Values arrive as raw integer strings and are formatted against the token's decimals using big-integer arithmetic — a plain division loses precision above 2^53, which for a 6-decimal token starts mattering at around nine billion units.
Both are cached in Redis for 30 minutes under a per-address key when read through the general transaction-history path. The deposit monitor bypasses that cache and fetches live.
Dust filtering
Airdropped spam is common on Tron and would otherwise create a transaction row and a notification for every worthless token someone sprays at your users.
| Asset | Ignored below |
|---|---|
| Native TRX | 0.001 TRX |
| TRC-20 | 1 / 10^(decimals − 3) — 0.001 for a 6-decimal token, 0.001 for an 18-decimal one |
Skipped transfers are counted and logged at debug level, so "I sent 0.0001 USDT and nothing happened" has an answer in the log rather than being a mystery.
The three detection paths
The per-session monitor. While a user has the deposit page open, a WebSocket handler starts a polling loop for their address. It fires after a random delay of up to five seconds — so ten users opening the page at once do not produce ten simultaneous TronGrid calls — then polls every 30 seconds.
The loop is resilient in a specific way. A 429 doubles its interval; a 403 triples it; both cap at five minutes; a successful poll resets it to 30 seconds. After ten consecutive errors it stops itself permanently and logs "Max consecutive errors reached". Closing the deposit page tears the loop down properly — an earlier release only flipped a flag the loop never read, and every deposit session leaked an immortal poller that ate the rate limit until every monitor on the install had died.
The background scanner. Addresses seen on a deposit page are registered in Redis for 72 hours and swept by a shared, rate-limited loop that revisits each one roughly every two minutes. Tron's token bucket is 0.5 address-scans per second, chosen to sit under TronGrid's limits. Each pass is one fetch and one process — no polling loop. It exists so a deposit that arrives after the user closes the tab is still found.
Duplicate suppression. Both paths check an in-memory set of recently processed transaction hashes, expiring after 30 minutes, and then check the database for an existing transaction with that hash on that wallet. A hash is marked as in-flight before processing and unmarked if processing throws, so a transient failure retries on the next poll instead of being lost.
From detection to a credited balance
A detected deposit is credited immediately, in the same call that found it.
storeAndBroadcastTransaction, in
backend/src/api/(ext)/ecosystem/utils/redis/deposit.ts, calls
handleEcosystemDeposit synchronously and — when it succeeds — broadcasts the
new balance to the user's session and writes nothing to the shared
pending-transaction store.
That store is the retry path, not the crediting path. A deposit is parked in it
only when the inline credit threw something that is not a 409; a 409 ("already
processed", or a platform-produced output) is a permanent rejection and is
discarded on the spot. The verifyPendingEcoDeposits watchdog then re-runs the
same credit over the parked records every 60 seconds, gives a record 30 attempts,
and dead-letters it to ecosystem:pendingDeposits:dead rather than deleting it
if it never succeeds.
Here Tron differs from the chains around it.
The watchdog applies a confirmation-depth rule to UTXO and EVM chains — three
blocks for Bitcoin, twelve by default elsewhere. Tron, along with Solana, TON and
Monero, is treated as already confirmed: the monitor marks the record
COMPLETED at detection, which is what lets the credit run inline, and if the
record ever does reach the watchdog that status is taken at face value with no
further on-chain check.
For TRC-20 that is well founded — the endpoint is queried with confirmed-only records. For native TRX the account-transactions query does not request confirmed-only, so a native deposit is credited on the strength of TronGrid having returned it. On a chain with three-second blocks and fast irreversibility this is a reasonable trade; it is not the same guarantee as a depth rule, and if your risk model needs one you must add a manual hold.
The credit itself is the shared Ecosystem path: a database transaction that
locks the wallet row, increments the balance, updates the wallet_data record
and writes the user-visible transaction. It is idempotent on the transaction
hash, which is what makes it safe for three detection paths to race.
A deposit that reached the pending store is credited to nobody until the watchdog succeeds, and losing Redis loses that record. The coins are still on-chain and still at the user's address, and the platform will find them again the next time it scans that address — but nothing recovers the pending record itself.
A note on TRC-20 transfer logs
Reading a TRC-20 amount out of a transaction is less obvious than it sounds. One
Tron transaction can carry many Transfer events — batch payouts, router swaps,
fee-on-transfer tokens, two different tokens in one call.
The platform therefore matches the event whose recipient is this wallet's
address and whose emitting contract is the monitored token, and prefers the
per-transfer figure the TronGrid endpoint already filtered. If neither yields a
transfer to this address for this token, nothing is credited and a warning is
logged. Taking the first Transfer in the list would occasionally credit an
amount belonging to somebody else.
Related
- Withdrawals — the other direction, and the expensive one
- Energy and bandwidth — why deposits are free and payouts are not
- Ecosystem: deposit wallets and custody
- Troubleshooting — deposits that never credit