Send USDT Programmatically on TRON with TronWeb
Automating USDT payouts on TRON — payroll, withdrawals, merchant refunds — requires building signed TRC-20 transfer transactions via TronWeb or raw HTTP APIs. TRON's ~3 second finality makes programmatic USDT ideal for high-throughput settlement when you handle Energy and security correctly.
This guide covers end-to-end USDT sends from a backend service.
Prerequisites
- TronWeb + TronGrid API key
- Hot wallet with USDT balance + TRX or frozen Energy
- Official USDT contract:
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t - Address validation before every send
Amount conversion
USDT uses 6 decimals:
function toUsdtBaseUnits(humanAmount) {
return Math.round(Number(humanAmount) * 1e6);
}
function fromUsdtBaseUnits(base) {
return Number(base) / 1e6;
}
Prefer BigInt for large values.
Basic transfer with TronWeb
const TronWeb = require('tronweb');
const tronWeb = new TronWeb({
fullHost: 'https://api.trongrid.io',
privateKey: process.env.PAYOUT_PRIVATE_KEY,
headers: { 'TRON-PRO-API-KEY': process.env.TRONGRID_KEY },
});
const USDT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
async function sendUsdt(toAddress, amountHuman) {
if (!tronWeb.isAddress(toAddress)) {
throw new Error('Invalid TRON address');
}
const contract = await tronWeb.contract().at(USDT);
const amount = toUsdtBaseUnits(amountHuman);
const txid = await contract.transfer(toAddress, amount).send({
feeLimit: 100_000_000,
callValue: 0,
shouldPollResponse: true,
});
return txid;
}
Energy and feeLimit
- Set
feeLimithigh enough to avoid OUT_OF_ENERGY (100 TRX sun units common starting point — tune empirically). - Monitor sender Energy:
tronWeb.trx.getAccountResources(address) - Freeze TRX on payout wallet for predictable costs
User-facing fee context: how to send USDT TRC-20.
Idempotency and safety
Production payout systems need:
| Control | Purpose |
|---|---|
| Idempotency keys | Prevent double-send on retry |
| Balance pre-check | Query balance before transfer |
| Allowlist | Optional recipient validation |
| Max daily volume | Limit hot wallet exposure |
| TX log | Store txid per payout request |
Confirm delivery
After broadcast:
const info = await tronWeb.trx.getTransactionInfo(txid); // info.receipt.result === 'SUCCESS'
Full patterns: verify transaction API.
transferFrom pattern (custody)
If user approved your contract/spender:
await contract.transferFrom(userAddress, toAddress, amount).send({ feeLimit: 100_000_000 });
Requires prior approve from user — common in DeFi, rare in simple payouts.
Error handling
| Error | Action |
|---|---|
| OUT_OF_ENERGY | Top up TRX or freeze more TRX |
| REVERT | Check balance, contract pause, blacklist |
| DUPLICATE | Idempotency — tx may already exist |
| Invalid address | Reject at API validation layer |
Testing on Shasta
Deploy mock TRC-20 with 6 decimals on Shasta or use test tokens. Never test with mainnet private keys in repos.
Compliance note
Automated USDT transfers may trigger AML obligations depending on jurisdiction. This is technical documentation, not legal advice.
Error handling in production
Wrap .send() in try/catch. On failure, fetch getTransactionInfo before retry — partial broadcasts are rare on TRON but idempotency matters. Log Energy consumed to tune frozen TRX amounts on hot wallets.
TronLink vs server signing
Browser apps should use TronLink window.tronWeb for user-controlled keys. Server payment sweeps use private key in TronWeb constructor — isolate in VPC, never expose to frontend. Multi-sig treasury requires account permissions workflow instead of single-key TronWeb.
Related guides
FAQ
How much Energy does a USDT transfer use?
Approximately 65,000 Energy on mainnet for a standard TRC-20 transfer when recipient already holds USDT. First-time recipients may cost more.
Can I send USDT without TRX in the same wallet?
The sending account needs Energy or TRX to burn for fees. USDT alone cannot pay network resources.
How do I batch many payouts?
Loop with rate limiting — TRON does not have native ERC-4337 batching. Consider queue worker with concurrency cap.
Is TronLink required for server sends?
No. Server uses private key directly via TronWeb.
Wrong address sent programmatically?
Irreversible. Validate tronWeb.isAddress() and optionally confirm checksum patterns.
Thanks — your feedback helps us improve the docs.