TRON USDT transfer history via JSON-RPC: logs, block windows and address formats
How to rebuild an address's USDT-TRC20 history from raw event logs without a third-party indexer: which filter to send, why every request covers at most 100 blocks, how to convert TRON addresses into topics, and what a year of history costs in requests.
A payments team that settles in USDT on TRON eventually needs the answer to one question: which USDT transfers touched this address, and when? Explorers show it, and hosted indexers sell it, but the data itself is in every TRON full node, as event logs. This post rebuilds that history with TRON's Ethereum-compatible JSON-RPC and nothing else, and does the arithmetic on what it costs.
A Chainstack write-up on USDT-TRC20 infrastructure (April 2026, updated July 2026, read 2026-09-22) describes polling blocks by number, one after another, and filtering their internal transactions for the USDT contract; it does not cover reconstructing the historical transfers of one address, the eth_getLogs range limit, or TRON's address formats. Those three are the whole job.
Where TRC-20 transfers live: the Transfer event
USDT on TRON is a TRC-20 token contract. Tether lists its TRON contract as TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t. The TRC-20 interface declares:
event Transfer(address indexed _from, address indexed _to, uint _value);and TRON's documentation says the event fires on transfer and transferFrom, and also on mint (with _from set to zero) and burn (with _to set to zero). TRON's event documentation describes events as data a contract chooses to emit, recorded in the transaction's TransactionInfo. Those are the records eth_getLogs searches.
A log carries up to four topics. For this event:
| Topic | Contents |
|---|---|
topics[0] | The event signature hash: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef |
topics[1] | _from, left-padded to 32 bytes |
topics[2] | _to, left-padded to 32 bytes |
data | _value, the amount in the token's smallest unit |
The signature hash is the one ERC-20 Transfer uses, because Solidity treats uint as an alias for uint256, so the canonical signature is Transfer(address,address,uint256) in both standards.
Filtering by sender and recipient
To get one address's history you send two filters per block window: one with your address in the _from position (outgoing) and one with it in the _to position (incoming). Each filter pins the contract and the signature:
{"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{
"address":"0xa614f803b6fd780986a42c78ec9c7f77e6ded13c",
"fromBlock":"0x51e5e1c",
"toBlock":"0x51e5e7f",
"topics":[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
null,
"0x000000000000000000000000<your-address-as-20-bytes>"
]
}]}null in a topic position is a wildcard. Swap the second and third positions for the outgoing filter. A topic position can also be an array of values, OR-matched, which is how you watch several of your own addresses in one request.
Two contract details from the Triport reference: fromBlock and toBlock are hex quantities, and the address field takes a 0x-prefixed 20-byte value — not a TRON base58 address. Unknown properties in the filter object are rejected.
The 100-block window and how to page it
On Triport, one eth_getLogs request on TRON may span at most 100 blocks, inclusive. Wider requests fail with -32602 (invalid params); when we sent a 256-block range on 2026-09-22 the answer was -32602 with the message "requested block range exceeds the measured upstream limit". An empty array, by contrast, is a successful query that matched nothing.
Paging is mechanical:
const WINDOW = 100; // inclusive range, so toBlock = fromBlock + 99
for (let from = start; from <= end; from += WINDOW) {
const to = Math.min(from + WINDOW - 1, end);
const incoming = await getLogs({ ...usdt, fromBlock: hex(from), toBlock: hex(to), topics: [SIG, null, me] });
const outgoing = await getLogs({ ...usdt, fromBlock: hex(from), toBlock: hex(to), topics: [SIG, me, null] });
await store(from, to, incoming, outgoing); // then persist `to` as your checkpoint
}Windows must be adjacent and non-overlapping: toBlock is inclusive, so the next window starts at to + 1. Persist the last completed to after storing the results, so a restart resumes rather than rescans.
Arithmetic: blocks per day ÷ 100 = calls
TRON produces a block every 3 seconds, according to its developer documentation. That gives an upper bound of 86,400 ÷ 3 = 28,800 blocks per day, and at 100 blocks per request, 288 requests per day of history, per filter. With incoming and outgoing filters, one day of one address's history is at most 576 requests.
On Triport, eth_getLogs on TRON is billed to the tron_read_rpc_heavy category, whose sustained limits are 2 / 5 / 20 / 80 RPS on free / basic / pro / business. These are default, unmeasured values in the tier matrix. Bursts up to 2× sustained are tolerated, but a backfill is sustained load, so plan on the sustained figure.
| Tier | Sustained RPS | One day (576 requests) | One year (210,240 requests) |
|---|---|---|---|
| Free | 2 | 288 s | about 29 h |
| Basic | 5 | about 115 s | about 11.7 h |
| Pro | 20 | about 29 s | about 2.9 h |
| Business | 80 | about 7 s | about 44 min |
The year column is 365 × 576 requests divided by the sustained rate. Two things change the answer:
- More addresses cost less than you think. A topic position accepts an array, so several of your addresses fit in one filter; the request count grows with block windows, not with addresses (until the response itself gets large).
- Fewer blocks than the bound. 28,800 is a ceiling from the 3-second interval; the count of blocks actually produced can be lower. Read the real range from
eth_blockNumberat the start and end of your backfill rather than trusting the ceiling.
eth_getLogs shares the heavy category with the heavy wallet calls, so a backfill competes with your live traffic in that category. Running it below your limit keeps production reads from receiving 429.
Address formats: hex-41, base58 and visible
TRON has two representations of the same 21-byte address. The native hex form starts with 41 followed by 20 bytes; the base58 form starting with T is what users and explorers show. Triport's wallet/getaccount reference pairs them explicitly: hex-41 goes with visible omitted or false, base58 with visible: true, and a mismatched pair "can produce an empty object rather than an explicit error".
For logs you need a third form. eth_getLogs takes the EVM-style 20 bytes, so:
- Decode base58check (
T…) to 21 bytes; the first byte is0x41. - Drop the
41prefix: the remaining 20 bytes are the EVM-style address. - For the contract
addressfield, send them as0x+ 40 hex characters. - For a topic, left-pad the same 20 bytes with zeros to 32 bytes.
Worked through for the USDT contract itself: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t decodes to 41a614f803b6fd780986a42c78ec9c7f77e6ded13c, so the filter address is 0xa614f803b6fd780986a42c78ec9c7f77e6ded13c. Going back, a 20-byte value from topics[1] or topics[2] becomes a user-facing address by prefixing 41 and base58check-encoding it.
The failure mode is quiet. A topic built from the wrong representation matches nothing, and nothing is exactly what an address with no USDT activity also returns. Test your conversion against one transfer you already know about before trusting an empty history. The same trap on the wallet API is covered in why TRON wallet/getaccount returns an empty object.
Native TRX and internal transactions are not in these logs
A Transfer log scan sees TRC-20 movements of one contract, and nothing else:
- Native TRX is not a TRC-20 token and emits no
Transferevent from the USDT contract, so TRX payments to the same address are invisible to this filter. - Other tokens need their own contract address in the filter.
- Anything a contract did without emitting an event is not in the logs at all, because logs are only what contracts choose to emit.
If the job is "every value movement touching this address", USDT logs are one input, not the answer.
When this is the wrong approach
- You need the history in seconds, for many addresses, on demand. At the rates above, a year of history for one address is hours on the lower tiers. An indexed store you maintain (or buy) is the right tool for interactive lookups;
eth_getLogspaging is how you build or verify one. - You need live notifications. Triport publishes no WebSocket log or mempool surface for TRON. For new transfers, poll the latest window on a timer.
- You need TRX or internal-transaction history. Logs will not give it to you, as above.
- You need current balance, not history. Read balance state directly; summing a reconstructed history reproduces it only if your history is complete.
Sources
- Triport,
eth_getLogson TRON: at most 100 blocks per request, inclusive;-32602on a wider range; hex block quantities; 20-byte0xaddresses;tron_read_rpc_heavy2 / 5 / 20 / 80 RPS (default, unmeasured); no TRON WebSocket log surface. Production check 2026-09-22 on the EU and CA origins: a 256-block request returned-32602, and a 100-block request filtered on the USDT contract returned logs. - Triport, TRON limits: category table and the 100-block note.
- Triport,
wallet/getaccounton TRON: hex-41 and base58 withvisible; an empty object on a mismatch. - Triport, Rate limits and tiers: 2× burst.
- TRON Developer Hub, Blocks ("a new block every 3 seconds") — https://developers.tron.network/docs/block, read 2026-09-22.
- TRON Developer Hub, TRC-20 protocol interface (
Transferevent, mint and burn) — https://developers.tron.network/docs/trc20-protocol-interface; Events — https://developers.tron.network/docs/event; both read 2026-09-22. - Tether, supported protocols (TRON contract
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t) — https://tether.to/en/supported-protocols/, read 2026-09-22. The hex-41 form above was computed from it by base58check decoding (checksum verified). - Solidity documentation, types ("
uintandintare aliases foruint256andint256") — https://docs.soliditylang.org/en/latest/types.html, read 2026-09-22. TheTransfersignature hash matches the one in Triport's Ethereum Pub/Sub referencelogsexample. - Chainstack, USDT-TRC20 infrastructure post — https://chainstack.com/tron-rpc-usdt-trc20-infrastructure/, read 2026-09-22.