To detect USDT deposits on Tron, read each solidified block with /walletsolidity/gettransactioninfobyblocknum, keep the logs emitted by the USDT contract with the Transfer topic, convert the recipient back to a base58 address and match it against your deposit addresses. One request per block returns every log in that block, so the scanner’s cost does not grow with the number of addresses you watch.
That is the whole design. The rest of this guide is the detail that makes it correct: Tron’s two address formats, why solidified blocks remove the need for a confirmation counter, the receipts you must skip, a tested scanner and what a USDT withdrawal costs in energy.
For the endpoint itself, TronWeb setup and the gRPC surface, see the Tron network page.
Tron basics a deposit system depends on
| Block time | 3-second slots |
| Block producers | 27 Super Representatives |
| Irreversible after | 19 distinct producers, so 18 blocks after the block itself |
| USDT contract | TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t |
| USDT decimals | 6 |
| Fees | bandwidth and energy, staked or paid by burning TRX |
The numbers in the first three rows come from Tron’s protocol documentation on its DPoS consensus. We checked the fourth and fifth on mainnet on 13 October 2026: decimals() on the USDT contract returns 6, and its symbol() returns USDT.
Two address formats
Tron addresses have two forms, and a deposit scanner moves between them constantly:
- Base58, starting with
T, such asTR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t. This is what users paste and what you store. - Hex, 21 bytes starting with
41. The USDT contract is41a614f803b6fd780986a42c78ec9c7f77e6ded13c.
Event logs use a third shape. The address of a log is the 20-byte hex without the 41 prefix and without 0x, and the addresses inside the topics are left-padded to 32 bytes. To match a log against your base58 deposit list, take the last 40 hex characters of the topic, put 41 in front and convert to base58. TronWeb does the conversion with tronWeb.address.fromHex().
Solidified blocks: the confirmation rule is built in
A new Tron block is not final when it is produced. Tron’s documentation defines an irreversible block, also called a solidified block: one acknowledged by at least 70% of the 27 Super Representatives, which is 19. In practice a block is solidified once 18 more blocks from different producers have been built on top of it.
The API exposes this directly. Every /wallet/... route reads the latest state, and its /walletsolidity/... twin reads only solidified state. When we compared /wallet/getnowblock and /walletsolidity/getnowblock on mainnet, the solidified head trailed the latest block by 18 to 20 blocks, which is about one minute.
So the rule for a deposit system is simple: credit deposits from /walletsolidity routes and a reorg can never undo a credit. A block that is not solidified yet simply returns no data there, so keep the cursor at or below the solidified head. Use the /wallet routes only to show “deposit detected” before it is final.
Scan a block in one request
gettransactioninfobyblocknum returns the execution result of every transaction in a block: receipt, fee and logs. That is exactly what a TRC-20 deposit scanner needs, and on the /walletsolidity path it only answers for solidified blocks.
import { TronWeb } from "tronweb";
const BASE = "https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/tron";
const tronWeb = new TronWeb({ fullHost: BASE });
const USDT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
const USDT_HEX = tronWeb.address.toHex(USDT).slice(2).toLowerCase(); // 20 bytes, no 41 prefix
const TRANSFER_TOPIC =
"ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
async function post(path, body = {}) {
const res = await fetch(`${BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
return res.json();
}
// Topics are 32-byte hex without 0x. The last 20 bytes are the address;
// the base58 form needs the 41 prefix back.
function topicToBase58(topic) {
return tronWeb.address.fromHex("41" + topic.slice(-40));
}
export async function solidifiedHead() {
const block = await post("/walletsolidity/getnowblock");
return block.block_header.raw_data.number;
}
export async function scanBlock(num, depositAddresses) {
const infos = await post("/walletsolidity/gettransactioninfobyblocknum", { num });
const deposits = [];
for (const tx of infos) {
if (tx.receipt?.result !== "SUCCESS") continue; // reverted or out of energy
for (const [logIndex, log] of (tx.log ?? []).entries()) {
if (log.address !== USDT_HEX || log.topics[0] !== TRANSFER_TOPIC) continue;
const to = topicToBase58(log.topics[2]);
if (!depositAddresses.has(to)) continue;
deposits.push({
txId: tx.id,
logIndex,
block: num,
from: topicToBase58(log.topics[1]),
to,
amount: BigInt("0x" + log.data), // USDT base units: divide by 10^6 to display
});
}
}
return deposits;
}
The loop around it is a cursor: start at the last block you finished, call scanBlock for every block up to solidifiedHead() and save the cursor after each block. With 3-second blocks, a scanner that keeps up makes about 20 requests a minute, whether you watch ten addresses or a million.
We ran scanBlock against solidified mainnet blocks on 13 October 2026 and again in an independent re-check. It returned real USDT deposits with the correct sender, recipient and amount. Block 87084270 held 247 transactions, 52 of them USDT transfers. Block 87085119 held 30. USDT is a large share of Tron’s traffic.
Three checks that prevent wrong credits
1. Skip failed transactions. A transaction can be included in a block and still fail. In the blocks we sampled, failed contract calls carried OUT_OF_ENERGY or REVERT, and plain TRX transfers carried no result field at all. Only a contract call with SUCCESS moved USDT. The receipt?.result !== "SUCCESS" line in the scanner is what keeps a failed transfer from being credited.
2. Match the contract address, never the symbol. Anyone can deploy a TRC-20 token called “USDT” and send it to your deposit addresses. The scanner compares log.address with the real USDT contract, so a lookalike token never matches. Never decide by calling symbol().
3. Deduplicate on transaction ID plus log index. One transaction can carry several transfers, for example a batch payout, and a retried block returns the same logs again. Make (txId, logIndex) a unique key in the table you credit from, and a retry becomes harmless.
Alternative: eth_getLogs on Tron’s JSON-RPC
Tron also exposes an Ethereum-compatible JSON-RPC interface. On BoltRPC it lives at https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/tron/jsonrpc, with chain ID 728126428 (0x2b6653dc). It supports eth_getLogs, which lets you reuse an existing EVM log pipeline:
curl https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/tron/jsonrpc \
-X POST \
-H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getLogs","params":[{"address":"0xa614f803b6fd780986a42c78ec9c7f77e6ded13c","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],"fromBlock":"finalized","toBlock":"finalized"}],"id":1}'
Here the addresses come back in EVM shape with 0x, and the contract address is the 20-byte hex without 41. The finalized block tag works on this interface, and Tron’s JSON-RPC documentation defines it as the latest solidified block. On TronGrid, though, it ran one to two blocks ahead of the /walletsolidity head in every comparison we made, probably because the two answers came from different nodes. If you credit from this route, keep a small margin behind finalized. If your team already runs an eth_getLogs pipeline for EVM chains, this route keeps one code path. If you build for Tron from scratch, the /walletsolidity scanner above is simpler and gives you receipts in the same response.
React to new blocks without polling
Tron’s full-node API is request and response (HTTP, plus unary gRPC), with no push subscriptions. BoltRPC adds a newHeads stream over WebSocket at wss://endpoints.boltrpc.io/ws/YOUR_API_KEY/tron: one message per block. Use it as a trigger, not as the data source. When a message arrives, run the scanner up to the new solidified head. If the socket drops, the cursor still knows where you stopped, so nothing is skipped. The trade-offs are the same as on EVM chains, covered in WebSocket vs polling.
Withdrawals: energy and fee_limit
Sending USDT is a smart contract call, and on Tron that costs energy as well as bandwidth. Two things to know before you size a hot wallet:
The recipient changes the cost. On 13 October 2026, estimating a 1 USDT transfer with /wallet/triggerconstantcontract returned 64,285 energy to an address that already held USDT and 130,285 energy to an address that had never held it. Writing a new balance slot costs more than updating one. The same figures showed up in real receipts in the block we scanned.
Most of that is a popularity surcharge. Tron’s dynamic energy model raises the energy a heavily used contract consumes. On 13 October 2026 USDT’s energy_factor (from /wallet/getcontractinfo) was 34000, a multiplier of 3.4, and receipts showed it: of the 64,285 energy for a transfer, 49,635 was energy_penalty_total, so the base cost was 14,650. The factor moves with usage, so estimate before every withdrawal instead of hard-coding a number. The estimate only reflects a real transfer when the owner address holds enough USDT; from an unfunded address the call reverts and the figure is meaningless:
const est = await post("/wallet/triggerconstantcontract", {
owner_address: "TYourHotWallet",
contract_address: USDT,
function_selector: "transfer(address,uint256)",
parameter:
tronWeb.address.toHex("TRecipient").slice(2).padStart(64, "0") +
(1_000_000).toString(16).padStart(64, "0"), // 1 USDT
visible: true,
});
console.log(est.energy_used);
Energy comes from TRX staked for energy, or the network burns TRX from the sender to cover what is missing. Set fee_limit on every contract call. Tron’s documentation is explicit that it caps what an unexpected failure can burn, and energy consumed by a failed execution is not refunded. Our explainer on handling RPC errors covers retries for the broadcast step.
Public endpoints and rate limits
Deposit scanning is constant, sustained read traffic: around 20 block reads a minute just to keep up, plus balance checks, estimates and backfills after any downtime. Public endpoints are not built for that. While testing this guide, TronGrid’s public API without an API key answered HTTP 429 twice: once during a short series of quick calls and once when we sent a burst of concurrent requests, where most were refused until it recovered about 20 seconds later. The limits are not published per request, so you only find them in production. A scanner that hits a 429 stops crediting deposits until it recovers, and your users see payments that look stuck. Read RPC rate limits explained for how to budget request rates, or use a dedicated endpoint where the ceiling is your plan rather than a shared key.
FAQ
How do I detect USDT TRC-20 deposits on Tron?
Read each solidified block with /walletsolidity/gettransactioninfobyblocknum, keep logs whose address is the USDT contract and whose first topic is the Transfer signature, convert the recipient topic to a base58 address and match it against your deposit addresses. Skip receipts whose result is not SUCCESS.
How many confirmations does a Tron deposit need?
Tron marks a block irreversible (solidified) once 19 of the 27 Super Representatives have acknowledged it, which is 18 blocks after it was produced, about one minute. Read through the /walletsolidity routes and every block you see is already final, so no separate confirmation counter is needed.
What is the difference between /wallet and /walletsolidity on Tron?
/wallet routes read the latest state, including blocks that are not yet irreversible. /walletsolidity routes read only solidified blocks. Use /walletsolidity to credit deposits and /wallet to show pending activity or to broadcast transactions.
What is the USDT contract address on Tron?
USDT on Tron mainnet is TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t, or 41a614f803b6fd780986a42c78ec9c7f77e6ded13c in hex. It has 6 decimals. Always match deposits on this address, never on the token symbol.
How much energy does a USDT transfer on Tron use?
On 13 October 2026 we measured 64,285 energy for a transfer to an address that already held USDT and 130,285 for an address that had never held it. Most of it is the dynamic energy surcharge on USDT (a factor of 3.4 that day), which changes with usage, so estimate each withdrawal with triggerconstantcontract from a funded address before sending it.
Can I use eth_getLogs on Tron?
Yes. Tron’s Ethereum-compatible JSON-RPC supports eth_getLogs and the finalized block tag. On BoltRPC it is served at /tron/jsonrpc with chain ID 728126428. Addresses come back in 20-byte EVM hex without the 41 prefix. Tron documents finalized as the latest solidified block, but on TronGrid it ran one to two blocks ahead of /walletsolidity, so keep a small margin if you credit from it.
Build your Tron deposit pipeline
BoltRPC serves the Tron full-node HTTP API, the /walletsolidity routes, the JSON-RPC interface and native gRPC on one key, with a WebSocket block stream for triggers. Get the endpoint on the Tron network page and check request-unit costs on pricing. Building on more than one stablecoin rail? The Plasma RPC guide applies the same deposit pattern to USDT0 on an EVM chain.
