Skip to content
New guideAgent Cookbook 12 copy-ready prompts for AI coding agents.See recipes
All blog posts
What a USDT transfer on TRON costs in energy, and why transfers fail

What a USDT transfer on TRON costs in energy, and why transfers fail

By Elias Faltin15min read

Your payout service sends 100,000 USDT transfers a day on TRON. Some cost nothing in TRX, some burn 6.43 TRX, and some burn 13.03 TRX. A few fail on chain and still use resources. The energy bill depends on which account pays, who receives, and how you set fee_limit.

This guide uses receipts from TRON mainnet, read through the Dwellir TRON endpoint on 3 November 2026. It covers what one transfer costs, why the USDT contract costs 4.4 times its base energy, how to estimate before you sign, and how to read failures and confirmations. For the transfer code itself, see the TRC-20 transfer guide.

What a USDT transfer costs on TRON today

We read every USDT contract call in blocks 87,671,872 to 87,671,891 (08:00:27 to 08:01:24 Stockholm time) with wallet/gettransactioninfobyblocknum. The 20 blocks held 1,686 calls to the USDT contract TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t. Of these, 1,673 were transfer calls.

CaseTransfersEnergyBurn cost at 100 sun per energyUSD at $0.3200 per TRX
Recipient already holds USDT1,28264,2856.4285 TRX$2.06
Recipient holds no USDT365130,28513.0285 TRX$4.17
Failed with OUT_OF_ENERGY260 or 3,3340 TRX$0

Every successful transfer used one of the two energy values exactly. Bandwidth ranged from 338 to 481 bytes per transfer, and 1,542 of the 1,647 successful transfers used exactly 345 bytes. When an account has no bandwidth left, it burns 1,000 sun per byte, so 0.345 TRX ($0.11) for a 345-byte transfer.

The chain parameters came from wallet/getchainparameters at 08:02 Stockholm time: getEnergyFee 100 sun, getTransactionFee 1,000 sun per byte and getFreeNetLimit 600 bytes a day. The TRX price is CoinGecko's TRX/USD quote of $0.320007, last updated at 08:05 Stockholm time.

A sender that pays everything in TRX burns 6.7735 TRX ($2.17) to send to a holder and 13.3735 TRX ($4.28) to send to a new recipient. A sender with enough staked or delegated energy and bandwidth burns nothing.

Why USDT costs 4.4 times its base energy

TRON's dynamic energy model raises the energy cost of contracts that use more than a threshold of energy per maintenance cycle. The multiplier ranges from 1x to 1 + DynamicEnergyMaxFactor / 10,000. With getDynamicEnergyMaxFactor at 34,000, the ceiling is 4.4x.

The USDT contract sits at that ceiling. wallet/getcontractinfo returned energy_factor 34,000 for USDT on 3 November 2026. Each receipt shows the penalty in energy_penalty_total:

CaseTotal energyPenaltyBase energy
Recipient holds USDT64,28549,63514,650
Recipient holds no USDT130,285100,63529,650
Stacked bars: a USDT transfer to a holder uses 14,650 base energy plus a 49,635 penalty; a transfer to a wallet with no USDT uses 29,650 plus 100,635.

The two cases differ by 15,000 base energy. That matches the TVM's storage costs: writing a new storage slot costs 20,000 and updating an existing slot costs 5,000 (java-tron EnergyCost). A recipient with no USDT has a balance slot that holds zero, so the transfer sets a zero slot to a nonzero value. With the penalty, those 15,000 become 66,000.

Account activation does not change the cost. In a simulation, a transfer to a freshly generated address with no TRON account cost 130,285 energy, with no extra 25,000. The extra 25,000 applies when a contract call creates a TRON account, and a TRC-20 transfer does not create one.

Read the factor yourself before you plan a budget:

Bash
curl -s -X POST "$TRON_RPC/wallet/getcontractinfo" \
  -H 'content-type: application/json' \
  -d '{"value":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t","visible":true}' \
  | jq '.contract_state.energy_factor'

The factor is recalculated each maintenance cycle and is capped by a governance parameter. A proposal can change getEnergyFee or getDynamicEnergyMaxFactor. Put both in your monitoring, not in a constant.

Three ways to pay for energy

TRON's resource model gives energy no free daily quota. A transfer gets its energy from one of three sources:

SourceAPIWhat it costs you
Stake TRXwallet/freezebalancev2Locked TRX. Energy refills over 24 hours.
Receive delegated energywallet/delegateresourceAnother account stakes and delegates to you. Energy rental markets sell this.
Burn TRXnone, automatic100 sun per energy at today's getEnergyFee

Staking yields a share of the network's daily energy. wallet/getaccountresource reported TotalEnergyLimit 180,000,000,000 and TotalEnergyWeight 18,001,575,237 staked TRX on 3 November 2026. That is 9.999 energy per staked TRX per day. One transfer a day to a holder needs 6,429 staked TRX. One transfer a day to a new recipient needs 13,030.

Most volume in the sample avoided the burn. Of 1,647 successful transfers, 1,376 (83.5%) used only staked or delegated energy. 213 (12.9%) burned TRX for all their energy, and 58 (3.5%) burned for part of it.

Bandwidth is a separate budget. The 600 free bytes a day cover one 345-byte transfer. The network does not combine payment methods for one transaction, so a second transfer that day burns TRX for its full bandwidth. In the sample, 794 of 1,647 successful transfers (48.2%) burned 0.345 TRX or more for bandwidth.

Who pays when a contract shares the cost

A contract can pay part of its callers' energy. consume_user_resource_percent sets the caller's share, and origin_energy_limit caps what the deployer pays per call. USDT reports a caller share of 30 and an origin_energy_limit of 10,000,000.

The deployer still paid almost nothing. In 20 of 1,647 transfers, origin_energy_usage was 1. In the other 1,627 it was absent. TRON charges the caller for any energy the deployer cannot cover. Plan as if your account pays all of it.

Estimate before you sign

Two endpoints run a transfer without broadcasting it. wallet/triggerconstantcontract executes the call and returns energy_used. wallet/estimateenergy returns energy_required.

On 3 November 2026 we ran both for a 1 USDT transfer from a funded account:

Recipienttriggerconstantcontract energy_usedestimateenergy energy_requiredReceipts on chain
Holds USDT64,28572,32164,285
Holds no USDT130,285138,428130,285

triggerconstantcontract matched the receipts exactly. estimateenergy added 8,036 and 8,143 energy.

The revert trap

A simulation that reverts still returns result.result: true. We simulated the same transfer from an address with no USDT:

JSON
{
  "result": { "result": true, "message": "REVERT opcode executed" },
  "energy_used": 8624,
  "transaction": { "ret": [{ "ret": "FAILED" }] }
}

With visible: true, message is plain text. Without it, message is hex. A check that reads only result.result and energy_used accepts this call. It then signs a transfer that will revert on chain and sets a fee_limit sized for 8,624 energy, 7.5 times below a real transfer. wallet/estimateenergy handles the same case better: it returned CONTRACT_EXE_ERROR with the same message.

Do not use the decoded return value either. Both successful USDT simulations returned a constant_result of 0. Treat the transfer as safe to sign only when message is empty and transaction.ret is not FAILED.

This script simulates a transfer, applies those checks, and proposes a fee_limit:

JavaScript
// node simulate-usdt-transfer.mjs <from> <to> <amount in USDT base units>
const RPC = process.env.TRON_RPC; // https://api-tron-mainnet.n.dwellir.com/<key>
const USDT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

// Base58 address to a 32-byte ABI word: drop the 0x41 prefix and the checksum.
function abiAddress(base58) {
  let n = 0n;
  for (const c of base58) n = n * 58n + BigInt(ALPHABET.indexOf(c));
  return n.toString(16).padStart(50, '0').slice(2, 42).padStart(64, '0');
}

async function post(path, body) {
  const res = await fetch(`${RPC}/${path}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  });
  return res.json();
}

async function simulateTransfer(from, to, amount) {
  const r = await post('wallet/triggerconstantcontract', {
    owner_address: from,
    contract_address: USDT,
    function_selector: 'transfer(address,uint256)',
    parameter: abiAddress(to) + BigInt(amount).toString(16).padStart(64, '0'),
    visible: true,
  });
  // visible: true returns message as text; visible: false returns it as hex.
  const raw = r.result?.message ?? '';
  const message = /^([0-9a-f]{2})+$/i.test(raw) ? Buffer.from(raw, 'hex').toString() : raw;
  const ret = r.transaction?.ret?.[0]?.ret;
  // result.result is true for a reverted call too. Check message and ret.
  if (r.result?.result !== true || message || ret === 'FAILED') {
    return { ok: false, reason: message || r.result?.code || 'unknown', energy: r.energy_used };
  }
  return { ok: true, energy: r.energy_used, penalty: r.energy_penalty };
}

const { chainParameter } = await post('wallet/getchainparameters', {});
const param = (key) => chainParameter.find((p) => p.key === key).value;
const energyFee = param('getEnergyFee'); // sun per energy
const maxFeeLimit = param('getMaxFeeLimit');

const [from, to, amount] = process.argv.slice(2);
const sim = await simulateTransfer(from, to, amount);
if (!sim.ok) {
  console.log({ ...sim, action: 'do not sign' });
} else {
  // Size for a recipient with no USDT (130,285 energy on 3 Nov 2026), even when
  // today's recipient holds some: its balance can reach zero before execution.
  const worstCase = Math.max(sim.energy, 130_285);
  const feeLimit = Math.min(Math.ceil(worstCase * energyFee * 1.2), maxFeeLimit);
  console.log({ ...sim, burnTrx: (sim.energy * energyFee) / 1e6, feeLimitTrx: feeLimit / 1e6 });
}

Output on mainnet at 08:10 Stockholm time on 3 November 2026, for a recipient with USDT, a recipient with none, and a sender with none:

Text
{ ok: true, energy: 64285, penalty: 49635, burnTrx: 6.4285, feeLimitTrx: 15.6342 }
{ ok: true, energy: 130285, penalty: 100635, burnTrx: 13.0285, feeLimitTrx: 15.6342 }
{ ok: false, reason: 'REVERT opcode executed', energy: 8624, action: 'do not sign' }

Size fee_limit for the recipient you cannot see

fee_limit caps the total energy a contract call may use. TRON's documentation states that the cap covers energy from your staked or delegated quota as well as the TRX burn. A fee_limit of 6.446 TRX allows 64,460 energy at 100 sun per energy, even for an account with millions of staked energy.

The worst case today is 130,285 energy, or 13.0285 TRX. getMaxFeeLimit caps fee_limit at 15,000 TRX.

Senders in the sample used a wide range. Across 1,673 transfers, fee_limit ran from 6.4285 TRX to 1,000 TRX, with a median of 50 TRX. 103 transfers (6.2%) set it below 13.0285 TRX, and 60 of those used exactly 6.446 TRX. That value equals the 14,650 base energy times 4.4. It covers a transfer to a holder and fails every transfer to a new recipient.

Three fee_limit settings for a USDT transfer: 6.446 TRX fails new recipients, 15 to 20 TRX covers both cases, and 100 TRX from an empty sender fails at the first opcode.

A low fee_limit fails in the expensive way. The call runs until it hits the cap, reverts with OUT_OF_ENERGY, and the network still charges the energy used up to that limit. With a 6.446 TRX fee_limit, a transfer to a new recipient can consume up to 64,460 energy and move no USDT.

A high fee_limit costs nothing extra on a successful USDT transfer, because the network charges the energy actually used. It only raises the most a failed or changed call can consume. For USDT, a fee_limit of 15 to 20 TRX covers both cases at today's parameters with headroom. Recompute it from getEnergyFee and the contract's energy_factor when either changes.

Read OUT_OF_ENERGY receipts

26 of the 1,686 sampled USDT calls (1.5%) failed with OUT_OF_ENERGY. Each failed call used 345 bytes of bandwidth. A failed receipt from wallet/gettransactioninfobyid looks like this:

JSON
{
  "id": "287367ce4c581edebefedba072255838bbb5980bf9a1ecb452c1fa1570dffca0",
  "blockNumber": 87671873,
  "receipt": { "net_usage": 345, "result": "OUT_OF_ENERGY" },
  "result": "FAILED",
  "resMessage": "4e6f7420656e6f75676820656e6572677920666f7220275055534831..."
}

resMessage is hex. It decodes to:

Text
Not enough energy for 'PUSH1' operation executing: curInvokeEnergyLimit[0], curOpEnergy[3], penaltyEnergy[10], usedEnergy[0]

curInvokeEnergyLimit[0] means the call had no energy budget at all. 25 of the 26 failures showed it. All 26 set a fee_limit of 100 TRX, so the cap was not the cause. We checked five of the senders after the run. Each held less than 0.0001 TRX, had no staked or delegated energy, and held no USDT.

Use these rules in your transfer worker:

  • Treat a receipt as successful only when receipt.result is SUCCESS. A top-level result of FAILED means the transfer did not move funds.
  • Decode resMessage and log it with the transaction ID. curInvokeEnergyLimit[0] points to an empty sender. A limit equal to fee_limit / getEnergyFee means the call hit your fee_limit.
  • Do not rebroadcast the same signed transaction. It is already in a block under its transaction ID. Fix the cause, then build, sign and broadcast a new transaction.
  • Record the burned TRX of failed attempts. A failure that hits the cap still costs energy.

Confirm with solidified blocks

TRON serves two views of the chain. wallet/ reads the latest blocks. walletsolidity/ reads only solidified blocks, which TRON's transaction documentation says need confirmation from 19 distinct super representatives.

We compared wallet/getnowblock with walletsolidity/getnowblock five times, one minute apart, from 08:05 to 08:09 Stockholm time. The solidified head trailed the head by 17 or 18 blocks each time, or 51 to 54 seconds.

We then tracked one USDT transfer in block 87,671,972:

Time after the blockwallet/gettransactioninfobyidwalletsolidity/gettransactioninfobyid
4.9 secondsReceipt{}
14.5 to 53.0 secondsReceipt{}
62.6 secondsReceiptReceipt
Timeline of one USDT transfer: the wallet/ receipt appears at 4.9 seconds, walletsolidity/ returns {} until 53.0 seconds and the solidified receipt at 62.6 seconds.

walletsolidity/ returns {} until the block solidifies. Do not treat {} as "not found" or as a failed transfer. Treat it as "not solidified yet". Credit a deposit or release a withdrawal after the solidified receipt shows SUCCESS. For the background on why a fast block is not a final block, see what blockchain finality means and what a chain reorg is.

Count the RPC calls per transfer

The safe flow above adds requests to every transfer. This is the count for one transfer with the timings measured above:

StepMethodCalls
Check sender energy and bandwidthwallet/getaccountresource1
Simulatewallet/triggerconstantcontract1
Build the transaction with fee_limitwallet/triggersmartcontract1
Broadcastwallet/broadcasttransaction1
Poll for the receipt every 3 secondswallet/gettransactioninfobyid2
Poll for the solidified receipt every 10 secondswalletsolidity/gettransactioninfobyid7
Total13

Waiting 55 seconds after inclusion before the first solidified poll cuts the last step to one or two calls, and the total to 7 or 8. Cache wallet/getchainparameters and the contract's energy_factor once per maintenance cycle instead of once per transfer.

At 100,000 transfers a day, that is 700,000 to 1,300,000 requests a day. Dwellir bills one RPC response as one API credit, whatever the method. See Dwellir pricing for the plans and the rate limits that apply to a polling worker.

A runbook checklist for USDT transfers on TRON

  • Read getEnergyFee, getMaxFeeLimit and the USDT energy_factor once per maintenance cycle and alert on a change.
  • Budget 64,285 energy for a recipient with USDT and 130,285 for one without, at today's parameters.
  • Simulate with wallet/triggerconstantcontract. Reject the transfer when message is set or transaction.ret is FAILED.
  • Ignore the decoded return value of a USDT transfer simulation.
  • Set fee_limit for the 130,285 case. 15 to 20 TRX covers it today.
  • Keep enough TRX for bandwidth. After the first transfer of the day, each transfer burns 0.345 TRX unless you stake or receive delegated bandwidth.
  • Treat receipt.result other than SUCCESS as failed, and decode resMessage.
  • Never rebroadcast a failed signed transaction. Build a new one.
  • Treat {} from walletsolidity/ as "not solidified yet".
  • Credit or release funds only after the solidified receipt shows SUCCESS.

Dwellir serves the TRON HTTP API on wallet/ and walletsolidity/ paths, plus a JSON-RPC interface. The TRON documentation lists every method used here. The TRON network page has the endpoint URLs.

Start free.1 response = 1 credit.

100,000 responses a day at 20 per second, on 150+ networks. Upgrade when you need more.