DEV Community

UsdtPull
UsdtPull

Posted on Originally published at usdtpull.com AI-assisted

Estimate TRON Energy Before a USDT Transfer with triggerconstantcontract

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 41 prefix, 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 989680 in 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
  }'
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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_limit is in SUN and caps all the energy the caller covers, staked and delegated included, not only the part burned. So base it on the full energy_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_penalty and 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:

  1. 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.
  2. The dynamic factor moves. The network updates a contract's energy_factor once per maintenance period of 6 hours (update_cycle 10,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.
  3. 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

  1. Simulate from the real sender; stop if ret is FAILED.
  2. Read available energy and subtract transfers already planned.
  3. Order the shortfall plus a margin, and wait until EnergyLimit grows.
  4. Sign with fee_limit from the full estimate.
  5. Send early in the rental term (how long rented energy lasts).
  6. Log the gap between energy_usage_total and 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)