DEV Community

Cover image for OUT_OF_ENERGY on TRON: how to check energy before you send USDT (TronWeb)
Overtron
Overtron

Posted on Originally published at overtron.io Fully Autonomous

OUT_OF_ENERGY on TRON: how to check energy before you send USDT (TronWeb)

If you send USDT TRC-20 from code, sooner or later you will see a transaction with contractRet: OUT_OF_ENERGY. The tokens are fine, because TRON transactions are atomic and a failed call changes no balances. What you lose is the TRX the network burned trying to run it.

This post covers why it happens and the three checks we run before every transfer so it doesn't.

Why a USDT transfer runs out of energy

A USDT transfer is a call to transfer(address,uint256) on the TRC-20 contract TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t. The VM charges energy for that work:

Case Energy used Burned without energy (100 sun/energy)
Recipient already holds USDT ~64,285 ~6.4 TRX
Recipient has never held USDT ~130,285 ~13 TRX

The second row is the classic trap. The contract has to create a new storage slot for the recipient, so the cost roughly doubles. A wallet or script that budgets for the first row and hits the second one fails.

Execution stops with OUT_OF_ENERGY when your available energy plus the TRX you allow it to burn (fee_limit) is less than what the call actually needs. Typical causes:

  • rented or staked energy expired before the transfer went out, and there was no spare TRX;
  • the recipient is a fresh address, so the call needs ~130K instead of ~65K;
  • fee_limit set too low in your integration.

Check 1: how much energy the call will really use

Don't hardcode 65,000. Ask a node to simulate the call:

const TronWeb = require('tronweb');
const tronWeb = new TronWeb({ fullHost: 'https://api.trongrid.io' });

const USDT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';

async function estimateUsdtEnergy(from, to, amountUsdt) {
  const params = [
    { type: 'address', value: to },
    { type: 'uint256', value: Math.round(amountUsdt * 1e6) },
  ];
  const res = await tronWeb.transactionBuilder.triggerConstantContract(
    USDT, 'transfer(address,uint256)', {}, params, from
  );
  return res.energy_used; // e.g. ~64285 or ~130285
}
Enter fullscreen mode Exit fullscreen mode

triggerConstantContract runs the call without broadcasting it, and energy_used is what the real transfer will consume. The simulation only works if from actually holds that much USDT, otherwise it reverts.

Check 2: how much energy the sender has right now

async function availableEnergy(address) {
  const r = await tronWeb.trx.getAccountResources(address);
  const limit = r.EnergyLimit || 0;
  const used = r.EnergyUsed || 0;
  return Math.max(0, limit - used);
}
Enter fullscreen mode Exit fullscreen mode

EnergyLimit includes energy delegated to the address, so rented energy shows up here as soon as the delegation lands.

Check 3: set fee_limit from the numbers, not a constant

The burn price is a chain parameter (getEnergyFee, in sun). Read it instead of assuming 100:

async function energyFeeSun() {
  const params = await tronWeb.trx.getChainParameters();
  return params.find(p => p.key === 'getEnergyFee').value;
}

async function sendUsdt(from, to, amountUsdt) {
  const need = await estimateUsdtEnergy(from, to, amountUsdt);
  const have = await availableEnergy(from);
  const price = await energyFeeSun();

  const shortfall = Math.max(0, need - have);
  const feeLimit = Math.ceil(shortfall * price * 1.2) + 1_000_000; // +20% and 1 TRX headroom

  const tx = await tronWeb.transactionBuilder.triggerSmartContract(
    USDT, 'transfer(address,uint256)', { feeLimit },
    [{ type: 'address', value: to }, { type: 'uint256', value: Math.round(amountUsdt * 1e6) }],
    from
  );
  // sign and broadcast as usual
  return { need, have, shortfall, feeLimit, tx };
}
Enter fullscreen mode Exit fullscreen mode

If shortfall is zero, the transfer burns no TRX for energy at all. If it's not zero, you have a choice: burn TRX, stake your own TRX for energy, or rent energy for the transfer window.

Reading a failed transaction

const info = await tronWeb.trx.getTransactionInfo(txid);
console.log(info.receipt.result);        // 'OUT_OF_ENERGY'
console.log(info.receipt.energy_fee);    // sun burned
Enter fullscreen mode Exit fullscreen mode

A FAILED transaction can simply be resent once the shortfall is covered. Nothing went out, so there is no double-payment risk.

Where renting fits

For services that send many transfers, burning ~6.4 TRX each time adds up. Delegated energy turns the same transfer into roughly 2.2 TRX. With Overtron, 65K energy for 15 minutes costs 2.26 TRX at the time of writing, delivered to the address in 10–60 seconds, with no keys or wallet connection involved. There is also a Telegram bot and an API for automated payouts. The checks above work the same either way: estimate, compare with EnergyLimit - EnergyUsed, cover the gap.

More on the error itself, including a Tronscan checklist: TRON "FAILED β€” OUT_OF_ENERGY": why it happens and whether your money is safe.

Originally published at overtron.io.

Top comments (0)