Disclosure: I work on UsdtPull, a TRON energy rental service. This article was first published on the UsdtPull blog.
To estimate the energy of a USDT transfer before you send it, simulate the call: post the sender, the USDT contract, transfer(address,uint256) and the ABI-encoded arguments to wallet/triggerconstantcontract, then read energy_used. On Nov 2, 2026 the public TronGrid node returned 64,285 for a recipient that holds USDT and 130,285 for one that holds none, exactly what the chain charged for real transfers that morning. The other method, wallet/estimateenergy, answered "this node does not support estimate energy".
From that number you get the energy to rent and the fee_limit. Below: the calls, the results and when the estimate misses.
Two endpoints, one question
wallet/triggerconstantcontract |
wallet/estimateenergy |
|
|---|---|---|
| What it returns |
energy_used (total) and energy_penalty (the dynamic part inside it) |
energy_required |
| On api.trongrid.io, Nov 2, 2026 | worked |
CONTRACT_VALIDATE_ERROR: this node does not support estimate energy |
| Node setting | needs constant calls allowed (vm.supportConstant) |
closed by default; needs vm.estimateEnergy and vm.supportConstant in the node config |
| Writes to chain | no, the unsigned transaction is not broadcast | no |
The estimateenergy reference calls triggerconstantcontract sufficient for most contracts and estimateenergy possibly more precise for a few special ones. For USDT the simulation matched the receipts to the unit, so it is the method to build on; estimateenergy is an option only on a node that has it turned on, such as your own.
Building the request
function_selector is the plain signature, with no spaces. parameter is the arguments in ABI encoding, as one hex string of 32-byte words:
-
word 1, the recipient: the hex address without the
41prefix, padded with zeros on the left to 64 characters; -
word 2, the amount: in the token's smallest units. USDT has 6 decimals, so 10 USDT is 10,000,000, or
989680in hex, padded the same way.
owner_address must be the real sender: the node runs the contract against its balance.
Illustrative example, check the exact format in the TRON documentation:
curl -s https://api.trongrid.io/wallet/triggerconstantcontract \
-H 'content-type: application/json' \
-d '{
"owner_address": "TU4vEruvZwLLkSfV9bNw12EJTPvNr7Pvaa",
"contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"function_selector": "transfer(address,uint256)",
"parameter": "0000000000000000000000003aae419529463d07b5dd02fadb2788c9d24c31570000000000000000000000000000000000000000000000000000000000989680",
"visible": true
}'
In TronWeb the library does the encoding. Illustrative example, check the exact format in the TronWeb docs:
// illustrative example, check the exact format in the TRON documentation
const sim = await tronWeb.transactionBuilder.triggerConstantContract(
'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
'transfer(address,uint256)',
{},
[{ type: 'address', value: recipient }, { type: 'uint256', value: 10_000_000 }],
tronWeb.address.toHex(sender)
);
const ok = sim.transaction?.ret?.[0]?.ret !== 'FAILED';
const energy = sim.energy_used;
What TronGrid returned on Nov 2, 2026
Sender: TU4vEr...7Pvaa, an address that held about 131 million USDT at the time of the calls. Amount: 10 USDT. We checked each recipient with a balanceOf call first.
| Call (UTC) | Recipient | Its USDT before | energy_used |
energy_penalty |
|---|---|---|---|---|
| 05:12:24 | TFKUwi...pQkx1 | 300.000001 | 64,285 | 49,635 |
| 05:12:32 | TQepJA...CjTom | 0 | 130,285 | 100,635 |
| 05:12:38 and 05:12:45 | the same two, via estimateenergy
|
not supported |
Then we read real receipts with gettransactioninfobyid:
| Transaction | Block | energy_usage_total |
energy_penalty_total |
|---|---|---|---|
| c1529f99..., same sender to TFKUwi...pQkx1, 04:52:33 | 87,640,554 | 64,285 | 49,635 |
| b0ee3f24..., same sender to another holder, 05:13:09 | 87,640,966 | 64,285 | 49,635 |
| 4771d00b..., another sender to an address without USDT, 04:52:33 | 87,640,554 | 130,285 | 100,635 |
All six figures match the simulation exactly. The dynamic part is 77% of the energy in both cases: the USDT contract sits at the maximum energy_factor, which is why the penalty is so large (how dynamic energy works).
What a failed simulation looks like
With an address holding no USDT as owner_address, the answer was energy_used 8,624, message "REVERT opcode executed", an empty constant_result and transaction.ret[0].ret FAILED. The top-level result.result was still true: it reports the API request, not the contract. Check ret before you trust the number.
From the estimate to an energy order
Read what the sender already has with getaccountresource: available energy = EnergyLimit - EnergyUsed, a missing field counts as 0. EnergyLimit already includes energy delegated to the address, so a rental that is in place is counted once.
Shortfall = max(0, energy_used - available). That is the volume to order.
Our sender had an EnergyLimit of 2,832,678,142 and EnergyUsed of 959,730,701: about 1.87 billion free, shortfall 0. Its receipts show no energy_fee.
A small payout address is different. Model calculation: the address has 20,000 energy left from its own stake and the next recipient holds no USDT. Shortfall = 130,285 - 20,000 = 110,285.
| Shortfall | Burned at 100 SUN, TRX | Rented at 28 SUN for 1 hour, TRX |
|---|---|---|
| 64,285 (no energy on the sender, recipient holds USDT) | 6.43 | 1.80 |
| 110,285 (the model above) | 11.03 | 3.09 |
| 130,285 (no energy on the sender, recipient without USDT) | 13.03 | 3.65 |
Rental figures use the current rate on Nov 15, 2026. The rate changes, and the exact price is shown in the service before you pay; reference prices are on the pricing page. The order method that takes any volume, and how to sign it, are covered in the energy rental API guide and on the API page.
Setting fee_limit from the estimate
The fee limit guide gives three rules that matter here:
-
fee_limitis in SUN and caps all the energy the caller covers, staked and delegated included, not only the part burned. So base it on the fullenergy_used, never on the shortfall. - The simple form is
energy_used×getEnergyFee. At 100 SUN that is 6,428,500 SUN for 64,285 and 13,028,500 SUN for 130,285. - The upper bound is base energy × (1 + max factor) × price, where base energy =
energy_used-energy_penaltyand the max factor is 3.4. For our two cases: 14,650 × 4.4 = 64,460 and 29,650 × 4.4 = 130,460, so 6,446,000 and 13,046,000 SUN.
fee_limit is a ceiling, not a charge, so a round margin costs nothing. Our own rounding, not a value from the guide: 15 TRX (15,000,000 SUN) covers both USDT cases at 100 SUN, far below the network maximum of 15,000 TRX (getMaxFeeLimit, Nov 2, 2026). Too low a limit stops the transfer with OUT_OF_ENERGY even when the address has energy (the out-of-energy guide).
When the estimate and the receipt disagree
The simulation runs on the state of one block. Three things change the answer before your transaction lands:
- The recipient's balance moves. A holder that withdraws to zero turns 64,285 into 130,285; an empty address that receives USDT from someone else does the opposite. The gap is 66,000, explained in why sending to a new address costs more. Estimate right before signing, not when the payout is queued.
-
The dynamic factor moves. The network updates a contract's
energy_factoronce per maintenance period of 6 hours (update_cycle10,235 for USDT on Nov 2, 2026). USDT was at the cap of 34,000, so under current parameters the factor could only go down. Committee proposals can change the parameters themselves: see how proposals change TRON fees. -
The sender is not the one you simulated. The result depends on
owner_address. A wrong sender, or one without enough USDT, gives a revert, as in our test.
One more for batches: the simulation neither checks nor reserves the sender's energy. It returns the same 64,285 whether the address has 70,000 free units or none, and ten queued transfers will each look fine on their own. Subtract every planned transfer from the available energy before you check the next one.
A pre-send check for a payout service
-
Simulate from the real sender; stop if
retis FAILED. - Read available energy and subtract transfers already planned.
-
Order the shortfall plus a margin, and wait until
EnergyLimitgrows. -
Sign with
fee_limitfrom the full estimate. - Send early in the rental term (how long rented energy lasts).
-
Log the gap between
energy_usage_totaland the estimate.
FAQ
Does triggerconstantcontract cost energy or TRX?
No. The node runs the call without committing it, and the reference says it uses neither bandwidth nor energy of the account.
Why does estimateenergy return "this node does not support estimate energy"?
The method is off by default. On Nov 2, 2026 the public TronGrid node gave this answer to both of our calls; it works only on a node with the setting turned on.
Can I estimate a transfer from an address that has no USDT?
You get a revert and a small energy_used that means nothing. Simulate from the address that will really send, with enough USDT for the amount.
Should fee_limit equal the energy I rent?
No. It caps all energy the transfer uses, so set it from the full estimate. More answers are in the FAQ.
Network figures are from TronGrid and the TRON developer documentation as of Nov 2, 2026; the rental rate is UsdtPull's rate on Nov 15, 2026. Drafted with AI assistance and checked by the author.
Top comments (0)