> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle Gas for TRON Transactions

> Cover TRON gas fees for your users' embedded wallets from a server wallet, so users can send tokens without holding TRX.

On TRON, every transaction costs gas, and users normally pay it in TRX. This recipe shows how your app can cover that gas instead, so users can send tokens like USDT from a wallet that holds no TRX.

Dynamic doesn't sponsor TRON gas the way it does for [Solana](https://www.dynamic.xyz/docs/node/wallets/server-wallets/gas-sponsorship-svm.md) and [EVM](https://www.dynamic.xyz/docs/node/wallets/server-wallets/gas-sponsorship.md). Instead, your backend uses a sponsor wallet to stake TRX and lend the resulting gas to your users' wallets. TRON calls this gas **Energy** and **Bandwidth**, and calls lending it **delegating**.

This recipe uses two wallets:

* A **sponsor wallet**: a [TRON server wallet](https://www.dynamic.xyz/docs/node/tron/overview.md) that your backend controls with the Node SDK. It stakes TRX and lends gas to users.
* A **user wallet**: the user's TRON embedded wallet, which signs TRC-20 transfers in the browser without holding TRX.

## Prerequisites

**Dynamic environment**

* TRON enabled in two places in the Dynamic Console: [Chains & Networks](https://console.dynamic.xyz/dashboard/chains-and-networks), with the same network active as your backend (for example, Nile), and [Embedded Wallets](https://console.dynamic.xyz/dashboard/embedded-wallets/dynamic#dynamic-embedded-wallet).
* A frontend using the JavaScript SDK (`@dynamic-labs-sdk/*`) with the [TRON extension](https://www.dynamic.xyz/docs/javascript/reference/tron/adding-tron-extension.md). The legacy React SDK's `@dynamic-labs/tron` supports only external wallets.

**Sponsor wallet (backend)**

* A TRON server wallet from [Create a TRON wallet](https://www.dynamic.xyz/docs/node/tron/create-wallet.md). Save the full `walletMetadata` that `createWalletAccount` returns, and the wallet's password.
* A Dynamic API token and your environment ID.
* Enough TRX in the sponsor wallet to stake. Step 3 shows how much.

**TRON network**

* A TRON full node, such as [TronGrid](https://www.trongrid.io/) (`https://api.trongrid.io` on mainnet, `https://nile.trongrid.io` on Nile). Use a TronGrid API key in production, because TronGrid rate-limits requests without one.

## How it works

1. The sponsor wallet stakes TRX for Energy, which contract calls such as TRC-20 transfers consume.
2. Your frontend sends the user's address to your backend, which activates the address if it's new and delegates Energy to it.
3. Once the delegation confirms, the user's wallet sends transfers as usual, and TRON spends the delegated Energy instead of TRX. Without delegated Bandwidth, free Bandwidth covers about one transfer a day.
4. When the user no longer needs coverage, the sponsor reclaims the Energy and can unstake the TRX.

## 1. Set up the sponsor client (backend)

Install `@dynamic-labs-wallet/node-tron`. The examples read these environment variables:

* `DYNAMIC_ENVIRONMENT_ID` and `DYNAMIC_AUTH_TOKEN`
* `SPONSOR_WALLET_METADATA` (the saved `walletMetadata` as JSON) and `SPONSOR_WALLET_PASSWORD`
* `TRON_NODE`, and optionally `TRONGRID_API_KEY`

```typescript theme={"system"}
import { DynamicTronWalletClient, type TronTransaction } from '@dynamic-labs-wallet/node-tron';

const client = new DynamicTronWalletClient({
  environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
});
await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);

// For example https://api.trongrid.io (mainnet) or https://nile.trongrid.io (Nile)
const TRON_NODE = process.env.TRON_NODE!;

// Load the walletMetadata saved from createWalletAccount. The by-address lookups
// (getWalletByAddress) omit the backup info that signing needs.
const sponsorWallet = { walletMetadata: JSON.parse(process.env.SPONSOR_WALLET_METADATA!) };
const sponsorAddress = sponsorWallet.walletMetadata.accountAddress;

// In your request handler: the user's address from your frontend. Untrusted input.
const userAddress = req.body.userAddress;
if (typeof userAddress !== 'string' || !/^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(userAddress)) {
  throw new Error('userAddress must be a base58 TRON address');
}
```

> [!WARNING]
>
> Only delegate to wallets of authenticated users, or anyone can drain the sponsor. [Verify the user's Dynamic JWT](https://www.dynamic.xyz/docs/auth/tokens.md#verifying-the-jwt), check that its `environment_id` matches yours, and check that `userAddress` is one of the user's wallets. If the token omits `verified_credentials`, look up the user (the token's `sub`) with [Get a user by ID](https://www.dynamic.xyz/docs/api-reference/users/get-a-user-by-id.md) and your API token.

In production:

* Keep `DYNAMIC_AUTH_TOKEN`, `SPONSOR_WALLET_PASSWORD`, and the wallet metadata in a secrets manager.
* A long-lived `client`'s session expires, and `signTransaction` then fails with HTTP 403. Call `authenticateApiToken` again and retry, or create the client per request.
* Load `@dynamic-labs-wallet/node-tron` as an ES module (the CommonJS build fails wallet creation with `bs58.encode is not a function`). In Next.js, also add `@dynamic-labs-wallet/node` and `@dynamic-labs-wallet/node-tron` to `serverExternalPackages`, because Turbopack can't bundle their native addon.

## 2. Add full-node helpers

The full node builds the sponsor's transactions, and your backend signs and broadcasts them as in [Sign TRON Transactions](https://www.dynamic.xyz/docs/node/tron/sign-transactions.md). The node reports invalid requests as HTTP 200 with an `Error` field, so the helper checks for it before signing.

```typescript theme={"system"}
// TronGrid reads its API key from the TRON-PRO-API-KEY header.
const tronHeaders: Record<string, string> = {
  'Content-Type': 'application/json',
  ...(process.env.TRONGRID_API_KEY ? { 'TRON-PRO-API-KEY': process.env.TRONGRID_API_KEY } : {}),
};

// POST to a full-node HTTP API endpoint and return the parsed JSON response.
const tronPost = async (path: string, body: Record<string, unknown>) => {
  const response = await fetch(`${TRON_NODE}${path}`, {
    method: 'POST',
    headers: tronHeaders,
    body: JSON.stringify(body),
  });
  if (!response.ok) {
    throw new Error(`${path} returned HTTP ${response.status}`);
  }
  return response.json();
};

const signAndBroadcast = async (unsignedTransaction: TronTransaction & { Error?: string }) => {
  if (unsignedTransaction.Error) {
    throw new Error(`Full node rejected the request: ${unsignedTransaction.Error}`);
  }

  const signedTransaction = await client.signTransaction({
    walletMetadata: sponsorWallet.walletMetadata,
    password: process.env.SPONSOR_WALLET_PASSWORD!,
    transaction: unsignedTransaction,
  });

  const response = await fetch(`${TRON_NODE}/wallet/broadcasttransaction`, {
    method: 'POST',
    headers: tronHeaders,
    body: signedTransaction, // already a JSON string
  });

  const result = await response.json();
  if (!result.result) {
    throw new Error(`Broadcast failed: ${JSON.stringify(result)}`);
  }
  return result;
};

// Poll until the transaction is in a block (about 3 seconds). The node returns {} until then.
const waitForConfirmation = async (txid: string) => {
  for (let attempt = 0; attempt < 20; attempt++) {
    const info = await tronPost('/wallet/gettransactioninfobyid', { value: txid });
    if (info.id) {
      return info;
    }
    await new Promise((resolve) => setTimeout(resolve, 3000));
  }
  throw new Error(`Transaction ${txid} was not confirmed`);
};
```

## 3. Stake TRX on the sponsor wallet

Staked TRX earns a share of the network's daily Energy, and spent Energy recovers over 24 hours. Size the stake as the Energy your users need per day divided by the Energy per TRX, which is `TotalEnergyLimit / TotalEnergyWeight` from `/wallet/getaccountresource`. Estimate a transfer's Energy with `/wallet/triggerconstantcontract` (`energy_used`); transfers to an address that doesn't hold the token yet cost more.

> [!NOTE]
>
> Recent mainnet USDT transfers used about 65,000 Energy to an existing holder and 130,000 to a new one.\[^usdt-energy] At 10 Energy per TRX, one 65,000-Energy transfer a day needs 6,500 TRX of stake.

[^usdt-energy]: `energy_usage_total` of 64,285 and 130,285 in the five most recent mainnet USDT (`TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`) transfers checked on October 28, 2026. TRON's dynamic Energy pricing changes these values, so estimate your own.

```typescript theme={"system"}
const stakeTransaction = await tronPost('/wallet/freezebalancev2', {
  owner_address: sponsorAddress,
  frozen_balance: 6_500_000_000, // 6,500 TRX in SUN: use your own sizing
  resource: 'ENERGY',
  visible: true,
});
await signAndBroadcast(stakeTransaction);
```

## 4. Lend gas to the user's wallet

A new embedded wallet doesn't exist on-chain, and receiving TRC-20 tokens doesn't activate it. Delegating to it fails with `Account[…] not exists`, so the sponsor activates it first, for a one-time fee of about 1.1 TRX.[^activation]

[^activation]: The `fee` of 1,100,000 SUN on a Nile `AccountCreateContract` in October 2026 ([`28714d8d…`](https://nile.tronscan.org/#/transaction/28714d8d827cbbf13adaff6a52d41f3a1c369e91a1969f64a0226a1110f854c7)): TRON's 1 TRX account creation fee (`getCreateNewAccountFeeInSystemContract`) plus 0.1 TRX of Bandwidth.

```typescript theme={"system"}
const account = await tronPost('/wallet/getaccount', { address: userAddress, visible: true });

if (!account.address) {
  const createTransaction = await tronPost('/wallet/createaccount', {
    owner_address: sponsorAddress,
    account_address: userAddress,
    visible: true,
  });
  const { txid } = await signAndBroadcast(createTransaction);
  await waitForConfirmation(txid);
}
```

`delegateresource` takes staked TRX in SUN, not Energy, so convert with the Energy per TRX. Stake whose Energy is still recovering can't be delegated, so check `/wallet/getcandelegatedmaxsize` first and stake more or [reclaim](#6-reclaim-unused-gas) when it runs low.

```typescript theme={"system"}
const ENERGY_PER_TRANSFER = 65_000; // estimate yours as described in step 3

const { TotalEnergyLimit, TotalEnergyWeight } = await tronPost('/wallet/getaccountresource', {
  address: sponsorAddress,
  visible: true,
});
const delegateSun = Math.ceil((ENERGY_PER_TRANSFER * TotalEnergyWeight) / TotalEnergyLimit) * 1_000_000;

const { max_size: maxSun = 0 } = await tronPost('/wallet/getcandelegatedmaxsize', {
  owner_address: sponsorAddress,
  type: 1, // ENERGY
  visible: true,
});

if (maxSun < delegateSun) {
  throw new Error('Not enough free stake: reclaim from other users (step 6) or stake more TRX');
}
```

Delegate, and wait for confirmation before responding to the frontend. A transfer sent before the delegation confirms doesn't get the Energy.

```typescript theme={"system"}
const delegateTransaction = await tronPost('/wallet/delegateresource', {
  owner_address: sponsorAddress,
  receiver_address: userAddress,
  balance: delegateSun, // staked TRX in SUN, not Energy units
  resource: 'ENERGY',
  lock: false,
  visible: true,
});
const { txid: delegateTxid } = await signAndBroadcast(delegateTransaction);
await waitForConfirmation(delegateTxid);
```

Repeated delegations to one address add up. Before delegating, call `/wallet/getaccountresource` for the user's address, and skip the delegation when `EnergyLimit` minus `EnergyUsed` already covers `ENERGY_PER_TRANSFER`.

> [!WARNING]
>
> A delegation with `lock: true` can't be reclaimed until its lock period expires.

### Cover more than one transfer a day

Every transaction also uses Bandwidth, which delegated Energy doesn't cover. An activated account gets 600 free Bandwidth a day and a TRC-20 transfer uses about 345, so a user with no TRX can send about one transfer a day.[^bandwidth] Another transfer that day fails with `BANDWITH_ERROR` (TRON's spelling).

[^bandwidth]: Free Bandwidth of 600 a day on mainnet and Nile. The 345 is the `net_usage` of BTT transfers on Nile on October 29, 2026; the exact value depends on the transaction's size.

To cover more than one transfer a day per user, repeat steps 3 and 4 with `resource: 'BANDWIDTH'`: size the stake with `TotalNetLimit / TotalNetWeight` and pass `type: 0` to `getcandelegatedmaxsize`. To reclaim it, sum `frozen_balance_for_bandwidth` in step 6 and undelegate with `resource: 'BANDWIDTH'`.

## 5. Send a transfer without TRX (frontend)

Steps 1 to 4 run in a backend endpoint you define, here `/api/sponsor`. Call it with the user's Dynamic JWT before each transfer. The backend activates the address once and delegates only when the user's remaining Energy is too low:

```typescript theme={"system"}
import { getDefaultClient } from '@dynamic-labs-sdk/client';

const requestSponsorship = async (userAddress: string) => {
  const response = await fetch('/api/sponsor', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${getDefaultClient().token}`,
    },
    body: JSON.stringify({ userAddress }),
  });
  if (!response.ok) {
    throw new Error(`Sponsorship failed: ${await response.text()}`);
  }
};
```

`sendGaslessTransfer` calls `requestSponsorship` first, then sends the transfer with the embedded wallet's TronWeb instance. TronWeb uses the active Dynamic network's RPC, not your `TRON_NODE`, so both must be on the same network. This example sends USDT on mainnet:

```typescript theme={"system"}
import { getTronWeb, isTronWalletAccount } from '@dynamic-labs-sdk/tron';
import { getPrimaryWalletAccount } from '@dynamic-labs-sdk/client';

const USDT_CONTRACT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';

// amount is in the token's base units: USDT has 6 decimals, so 1 USDT is '1000000'
const sendGaslessTransfer = async (recipient: string, amount: string) => {
  const walletAccount = getPrimaryWalletAccount();

  if (!walletAccount || !isTronWalletAccount(walletAccount)) {
    return;
  }

  await requestSponsorship(walletAccount.address);

  const tronWeb = await getTronWeb({ walletAccount });

  const { transaction } = await tronWeb.transactionBuilder.triggerSmartContract(
    USDT_CONTRACT,
    'transfer(address,uint256)',
    { feeLimit: 100_000_000 }, // 100 TRX in SUN: covers the Energy with headroom; a cap, not a charge
    [
      { type: 'address', value: recipient },
      { type: 'uint256', value: amount },
    ],
    walletAccount.address,
  );

  const signedTransaction = await tronWeb.trx.sign(transaction);
  return tronWeb.trx.sendRawTransaction(signedTransaction);
};
```

`feeLimit` is in SUN. It caps the TRX the call can burn, and TRON also uses it to cap the Energy the call can use, including delegated Energy. Set it to at least the Energy the transfer needs times the network's Energy fee (`getEnergyFee` from `/wallet/getchainparameters`), with headroom. The limit isn't a charge: when delegated Energy covers the call, 100 TRX costs nothing. TronWeb rejects `0`, and the network's maximum is `getMaxFeeLimit`. If the delegated Energy runs out and the user holds no TRX, the transfer fails rather than burning TRX.

> [!TIP]
>
> On Nile, test with a token whose sender pays the Energy, such as BTT (`TNuoKL1ni8aoshfFL1ASca1Gou9RXwAzfn`, 18 decimals). Nile's USDT pays its senders' Energy itself, so it succeeds even without delegation. A covered transfer's receipt (`/wallet/gettransactioninfobyid`) has `energy_usage` and no `energy_fee`, `net_fee`, or `origin_energy_usage`.

## 6. Reclaim unused gas

When the user no longer needs coverage, undelegate the full amount lent to them, read from `/wallet/getdelegatedresourcev2`:

```typescript theme={"system"}
const { delegatedResource = [] } = await tronPost('/wallet/getdelegatedresourcev2', {
  fromAddress: sponsorAddress,
  toAddress: userAddress,
  visible: true,
});
const delegatedSun = delegatedResource.reduce(
  (sum: number, resource: { frozen_balance_for_energy?: number }) => sum + (resource.frozen_balance_for_energy ?? 0),
  0,
);

const undelegateTransaction = await tronPost('/wallet/undelegateresource', {
  owner_address: sponsorAddress,
  receiver_address: userAddress,
  balance: delegatedSun,
  resource: 'ENERGY',
  visible: true,
});
await signAndBroadcast(undelegateTransaction);
```

To recover the TRX, call `/wallet/unfreezebalancev2` on undelegated stake, wait out Stake 2.0's unstaking period (14 days on mainnet), then call `/wallet/withdrawexpireunfreeze`.

## Next steps

* [Create a TRON wallet](https://www.dynamic.xyz/docs/node/tron/create-wallet.md)
* [Sign TRON transactions](https://www.dynamic.xyz/docs/node/tron/sign-transactions.md)
* [getTronWeb](https://www.dynamic.xyz/docs/javascript/reference/tron/get-tron-web.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.