> ## 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.

# Charge a Fee on TRON Transfers

> Charge users a fixed or percentage fee on TRON token transfers, verify it on-chain, and optionally combine it with gas sponsorship.

This recipe shows how your app can charge users a fee on TRON token transfers, such as a flat amount or a percentage of each transfer, and verify on-chain that the fee was paid. A fee is separate from gas sponsorship, which covers the user's TRX gas. You can charge a fee without sponsoring gas, sponsor gas without charging a fee, or combine them, for example to recover what sponsorship costs you.

## Fees and gas sponsorship

| | Gas sponsorship | Fee |
| - | - | - |
| What it does | Your sponsor wallet pays the network for the user's gas | The user pays your app, in the token they send |
| What you need | A sponsor wallet with staked TRX | A fee wallet and a backend that verifies the fee |
| Who pays for gas | You | The user, unless you also sponsor gas |
| Where it's covered | [Handle gas for TRON transactions](https://www.dynamic.xyz/docs/recipes/integrations/tron-gas.md) | This page |

## How to collect a fee on TRON

A TRON transaction makes one contract call, so a wallet can't add the fee to the same transaction the way a batched EVM call can. You can collect it in one of two ways:

* **A second transfer that your backend verifies.** The user sends the fee to your fee wallet, then the transfer, and your backend checks both on-chain. This page covers this approach.
* **A contract that moves both amounts in one transaction.** The user approves the contract once, and each transfer through it pays the fee atomically. This page doesn't cover this approach.

Collecting the fee before the transfer ensures that a user who abandons the flow has already paid it.

## Prerequisites

* A backend that verifies the user's Dynamic JWT and checks that the user's address is one of the user's wallets, as in the [TRON gas recipe's step 1](https://www.dynamic.xyz/docs/recipes/integrations/tron-gas.md#1-set-up-the-sponsor-client-backend).
* The full-node helpers `tronPost` and `waitForConfirmation` from the [TRON gas recipe's step 2](https://www.dynamic.xyz/docs/recipes/integrations/tron-gas.md#2-add-full-node-helpers), and `tronweb` to encode and decode addresses.
* A fee wallet: a TRON address you control that receives fees. It should already hold the token, because a transfer to an address that doesn't hold it costs more Energy.
* To combine the fee with gas sponsorship, the backend and frontend from the [TRON gas recipe](https://www.dynamic.xyz/docs/recipes/integrations/tron-gas.md).

## Set the fee amount

Amounts are in the token's base units; USDT has 6 decimals, so 1 USDT is `'1000000'`. A flat fee is a constant, and a percentage fee is a share of the transfer amount, for example in basis points:

```typescript theme={"system"}
const FEE_BPS = 50n; // 0.5%

const percentageFee = (amount: string) => ((BigInt(amount) * FEE_BPS) / 10_000n).toString();
```

To price the fee from the gas your sponsor wallet covers, use `priceGasFee` in [Combine the fee with gas sponsorship](#combine-the-fee-with-gas-sponsorship).

## Verify the fee (backend)

`verifyTransfer` decodes a transaction and checks its sender, token, recipient, amount, and result. Verify both the transfer and the fee before you provide the service the fee pays for, and record each transaction ID so that it can't be reused.

```typescript theme={"system"}
import { TronWeb } from 'tronweb';

const TOKEN_CONTRACT = process.env.TOKEN_CONTRACT!; // the token users send, for example mainnet USDT
const FEE_WALLET = process.env.FEE_WALLET!;

type ExpectedTransfer = { owner: string; to: string; minAmount: string };

const verifyTransfer = async (txid: string, expected: ExpectedTransfer) => {
  const [tx, info] = await Promise.all([
    tronPost('/wallet/gettransactionbyid', { value: txid, visible: true }),
    waitForConfirmation(txid),
  ]);

  const contract = tx.raw_data?.contract?.[0];
  const value = contract?.parameter?.value;
  const data: string = value?.data ?? '';
  const checks = {
    type: contract?.type === 'TriggerSmartContract',
    owner: value?.owner_address === expected.owner,
    token: value?.contract_address === TOKEN_CONTRACT,
    selector: data.startsWith('a9059cbb'),
    recipient: data.length >= 136 && TronWeb.address.fromHex('41' + data.slice(32, 72)) === expected.to,
    amount: data.length >= 136 && BigInt('0x' + data.slice(72, 136)) >= BigInt(expected.minAmount),
  };
  const failed = Object.entries(checks).filter(([, ok]) => !ok).map(([name]) => name);
  return {
    failed,
    succeeded: info.receipt?.result === 'SUCCESS',
    energyUsed: info.receipt?.energy_usage ?? 0,
    confirmedAt: info.blockTimeStamp as number,
  };
};

// Use a database in production, so every server instance sees the same transaction IDs.
const usedTxids = new Set<string>();
```

Without gas sponsorship, the user's wallet pays its own TRX gas for both transfers, and your backend checks them like this:

```typescript theme={"system"}
export const verifyFeePaid = async (
  userAddress: string,
  transfer: { txid: string; recipient: string; amount: string },
  fee: { txid: string; amount: string },
) => {
  if (usedTxids.has(transfer.txid) || usedTxids.has(fee.txid)) {
    throw new Error('A new transaction ID is required');
  }
  const checked = [
    await verifyTransfer(transfer.txid, { owner: userAddress, to: transfer.recipient, minAmount: transfer.amount }),
    await verifyTransfer(fee.txid, { owner: userAddress, to: FEE_WALLET, minAmount: fee.amount }),
  ];
  if (checked.some((result) => result.failed.length > 0 || !result.succeeded)) {
    throw new Error('The transfer or the fee failed verification');
  }
  usedTxids.add(transfer.txid);
  usedTxids.add(fee.txid);
};
```

Compute `fee.amount` on your backend from your own fee rule, not from a value the frontend sends.

## Combine the fee with gas sponsorship

When you also sponsor gas, sponsorship gives your backend something to withhold: it refuses to sponsor a wallet again until its fee arrives. In this flow, the user sends the fee first, and your backend verifies it before the user sends the transfer.[^fee-validation]

[^fee-validation]: Tested with gas sponsorship on Nile on November 4 and 5, 2026, with BTT and a fixed 0.1 BTT fee, in this order: fee, then transfer. The test ran eight cases: the normal flow, a skipped fee, stopping after the fee, cancelling before sending, a failed transfer after the fee, expiry, a failed fee, and a failed decoy fee. In the normal flow, the fee was [`1524d96a…`](https://nile.tronscan.org/#/transaction/1524d96a2cc25216f465dbe7c2f5a9db3db801e530c9b0f05dec4a2bb5228a37) and the transfer was [`89e08c43…`](https://nile.tronscan.org/#/transaction/89e08c435eecddf97077f090ef128583db64067966a6443a3e8d7285fa6094e8); each used 13,276 Energy. A run with a Dynamic embedded wallet sent the fee [`fd433876…`](https://nile.tronscan.org/#/transaction/fd433876b915ba35b328e3cee7a76370e0f2099873f281686cb2a684398816b5) and then the transfer [`2019f83a…`](https://nile.tronscan.org/#/transaction/2019f83a14882d1e39df76c8477f2e635e202eeb8600fd13ffd3fa2e13b5ade7) six seconds later. Fees without sponsorship and percentage fees weren't tested separately.

1. Your frontend asks for a quote and shows the fee to the user.
2. When the user confirms, your backend delegates Energy for both transfers and tops up TRX for any Bandwidth that free Bandwidth doesn't cover.
3. The user's wallet signs and sends the fee, and your backend verifies it on-chain.
4. The user's wallet signs and sends the transfer, and your backend verifies it and reclaims the Energy. If the user cancels or abandons the flow before paying the fee, it reclaims the Energy without a fee. After the fee, it reclaims the Energy and keeps the fee.

### 1. Price the fee and lend the gas (backend)

These functions run in the same backend as the TRON gas recipe and use its `tronPost`, `signAndBroadcast`, `waitForConfirmation`, and `sponsorAddress` from [steps 1 and 2](https://www.dynamic.xyz/docs/recipes/integrations/tron-gas.md#1-set-up-the-sponsor-client-backend). Activate the user's address as in [step 4](https://www.dynamic.xyz/docs/recipes/integrations/tron-gas.md#4-lend-gas-to-the-users-wallet) before sponsoring it. They also use `verifyTransfer`, `usedTxids`, `TOKEN_CONTRACT`, and `FEE_WALLET` from [Verify the fee](#verify-the-fee-backend), and read these environment variables:

* `FIXED_FEE_BASE_UNITS` (optional): a flat fee in the token's base units, instead of pricing from the TRX the gas would burn
* `FEE_MULTIPLIER` and `PENDING_FEE_TTL_MS` (optional)

`priceGasFee` estimates both transfers and converts the TRX they would burn into the token, so replace `getTrxUsdtPrice` with your own price source. The fee transfer's Energy is estimated with 1 base unit; the amount doesn't change it.

```typescript theme={"system"}
const FEE_MULTIPLIER = Number(process.env.FEE_MULTIPLIER ?? '0.9'); // share of the TRX burn you charge
const FIXED_FEE_BASE_UNITS = process.env.FIXED_FEE_BASE_UNITS;
const PENDING_FEE_TTL_MS = Number(process.env.PENDING_FEE_TTL_MS ?? 10 * 60 * 1000);
const BANDWIDTH_PER_TRANSFER = 400; // a TRC-20 transfer uses about 345, plus headroom

const encodeTransfer = (to: string, amount: string) =>
  TronWeb.address.toHex(to).replace(/^41/, '').padStart(64, '0') +
  BigInt(amount).toString(16).padStart(64, '0');

const estimateEnergy = async (owner: string, to: string, amount: string): Promise<number> => {
  const response = await tronPost('/wallet/triggerconstantcontract', {
    owner_address: owner,
    contract_address: TOKEN_CONTRACT,
    function_selector: 'transfer(address,uint256)',
    parameter: encodeTransfer(to, amount),
    visible: true,
  });
  if (!response.result?.result) {
    throw new Error(`Energy estimate failed: ${JSON.stringify(response.result)}`);
  }
  return response.energy_used;
};

// Replace with your own price source. Returns USDT per 1 TRX, for example 0.25.
const getTrxUsdtPrice = async (): Promise<number> => {
  throw new Error('Implement getTrxUsdtPrice');
};

const getChainParameter = async (key: string): Promise<number> => {
  const { chainParameter } = await tronPost('/wallet/getchainparameters', {});
  const entry = chainParameter.find((p: { key: string }) => p.key === key);
  if (!entry) throw new Error(`Chain parameter ${key} not found`);
  return entry.value;
};

const priceGasFee = async (userAddress: string, recipient: string, amount: string) => {
  const energyMain = await estimateEnergy(userAddress, recipient, amount);
  const energyFeeLeg = await estimateEnergy(userAddress, FEE_WALLET, '1');
  const energy = Math.ceil((energyMain + energyFeeLeg) * 1.1);
  const bandwidth = 2 * BANDWIDTH_PER_TRANSFER;

  if (FIXED_FEE_BASE_UNITS) {
    return { energy, bandwidth, feeAmount: FIXED_FEE_BASE_UNITS };
  }

  const burnSun =
    energy * (await getChainParameter('getEnergyFee')) + bandwidth * (await getChainParameter('getTransactionFee'));
  // SUN × (USDT per TRX) = USDT base units, because both have 6 decimals
  const feeAmount = Math.ceil(burnSun * FEE_MULTIPLIER * (await getTrxUsdtPrice())).toString();
  return { energy, bandwidth, feeAmount };
};
```

The backend delegates Energy as in the recipe's step 4, sized for both transfers. For Bandwidth, it counts free and delegated Bandwidth in whole transfers, because a transfer can't split its Bandwidth between them, and sends the user the TRX for the rest.

```typescript theme={"system"}
const wholeTransfers = (bandwidth: number) =>
  Math.floor(Math.max(bandwidth, 0) / BANDWIDTH_PER_TRANSFER) * BANDWIDTH_PER_TRANSFER;

const delegateEnergy = async (energyNeeded: number, userAddress: string) => {
  const user = await tronPost('/wallet/getaccountresource', { address: userAddress, visible: true });
  const deficit = energyNeeded - ((user.EnergyLimit ?? 0) - (user.EnergyUsed ?? 0));
  if (deficit <= 0) return;

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

  const { max_size: maxSun = 0 } = await tronPost('/wallet/getcandelegatedmaxsize', {
    owner_address: sponsorAddress,
    type: 1, // ENERGY
    visible: true,
  });
  if (maxSun < sun) {
    throw new Error('Not enough free stake: reclaim from other users or stake more TRX');
  }

  const delegateTransaction = await tronPost('/wallet/delegateresource', {
    owner_address: sponsorAddress,
    receiver_address: userAddress,
    balance: sun,
    resource: 'ENERGY',
    lock: false,
    visible: true,
  });
  const { txid } = await signAndBroadcast(delegateTransaction);
  await waitForConfirmation(txid);
};

const topUpBandwidth = async (bandwidthNeeded: number, userAddress: string) => {
  const user = await tronPost('/wallet/getaccountresource', { address: userAddress, visible: true });
  const covered =
    wholeTransfers((user.freeNetLimit ?? 0) - (user.freeNetUsed ?? 0)) +
    wholeTransfers((user.NetLimit ?? 0) - (user.NetUsed ?? 0));
  const deficit = bandwidthNeeded - covered;
  if (deficit <= 0) return;

  const { balance = 0 } = await tronPost('/wallet/getaccount', { address: userAddress, visible: true });
  const sun = deficit * (await getChainParameter('getTransactionFee')) - balance;
  if (sun <= 0) return;

  const transferTransaction = await tronPost('/wallet/createtransaction', {
    owner_address: sponsorAddress,
    to_address: userAddress,
    amount: sun,
    visible: true,
  });
  const { txid } = await signAndBroadcast(transferTransaction);
  await waitForConfirmation(txid);
};

// The recipe's step 6, waiting for confirmation so the stake is free again before the next delegation.
const reclaimGas = async (userAddress: string) => {
  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,
  );
  if (delegatedSun === 0) return;

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

const getEnergyUsed = async (userAddress: string): Promise<number> => {
  const user = await tronPost('/wallet/getaccountresource', { address: userAddress, visible: true });
  return user.EnergyUsed ?? 0;
};
```

### 2. Verify the fee, then the transfer (backend)

The backend records each sponsored wallet's pending fee and the wallet's `EnergyUsed` at sponsorship, which shows later whether the user spent the delegated Energy. `payFee` verifies the fee before the user sends the transfer, and `settleFee` refuses a transfer until the fee is paid.

```typescript theme={"system"}
type PendingFee = {
  feeAmount: string;
  recipient: string;
  amount: string;
  energyUsedAtSponsor: number;
  gasUsed: boolean;
  createdAt: number;
  feeTxid?: string; // set once the fee is verified on-chain
};
// Use a database in production. Claim a wallet's entry with one atomic write, such as an
// insert under a unique key on userAddress, so concurrent requests can't both pass the check.
const pendingFees = new Map<string, PendingFee>();

export const getGasQuote = async (userAddress: string, recipient: string, amount: string) => {
  const { feeAmount } = await priceGasFee(userAddress, recipient, amount);
  return { feeAmount, feeRecipient: FEE_WALLET };
};

export const sponsorWithFee = async (userAddress: string, recipient: string, amount: string, maxFeeAmount: string) => {
  if (pendingFees.has(userAddress)) {
    throw new Error('A previous gas fee is unsettled');
  }
  const pending: PendingFee = {
    feeAmount: maxFeeAmount,
    recipient,
    amount,
    energyUsedAtSponsor: 0,
    gasUsed: false,
    createdAt: Date.now(),
  };
  pendingFees.set(userAddress, pending);

  try {
    const { energy, bandwidth, feeAmount } = await priceGasFee(userAddress, recipient, amount);
    if (BigInt(feeAmount) > BigInt(maxFeeAmount)) {
      throw new Error('The gas fee is higher than the quote. Request a new quote.');
    }
    pending.feeAmount = feeAmount;
    pending.energyUsedAtSponsor = await getEnergyUsed(userAddress);

    await delegateEnergy(energy, userAddress);
    await topUpBandwidth(bandwidth, userAddress);
    return { feeAmount, feeRecipient: FEE_WALLET };
  } catch (error) {
    await reclaimGas(userAddress);
    pendingFees.delete(userAddress);
    throw error;
  }
};

// A failed transfer clears the fee only if it is the only sponsored Energy the user spent.
const sponsoredGasPaidForAnother = async (userAddress: string, pending: PendingFee, energyUsed: number) =>
  (await getEnergyUsed(userAddress)) - pending.energyUsedAtSponsor > energyUsed;

export const payFee = async (userAddress: string, feeTxid: string) => {
  const pending = pendingFees.get(userAddress);
  if (!pending) throw new Error('No unsettled fee');
  if (pending.feeTxid) throw new Error('The fee is already paid');
  if (usedTxids.has(feeTxid)) throw new Error('A new fee transaction ID is required');

  const fee = await verifyTransfer(feeTxid, { owner: userAddress, to: FEE_WALLET, minAmount: pending.feeAmount });
  if (fee.confirmedAt < pending.createdAt) fee.failed.push('sentAfterSponsorship');
  if (fee.failed.length > 0) {
    throw new Error(`Fee transaction failed checks: ${fee.failed.join(', ')}`);
  }
  if (!fee.succeeded) {
    if (await sponsoredGasPaidForAnother(userAddress, pending, fee.energyUsed)) {
      throw new Error('The sponsored gas paid for another transaction. Pay the fee instead.');
    }
    await reclaimGas(userAddress);
    pendingFees.delete(userAddress);
    return { status: 'fee_transfer_failed' as const };
  }

  usedTxids.add(feeTxid);
  pending.feeTxid = feeTxid;
  return { status: 'fee_paid' as const };
};

// The fee is paid, so the transfer's result no longer decides whether it's owed.
export const settleFee = async (userAddress: string, mainTxid: string) => {
  const pending = pendingFees.get(userAddress);
  if (!pending) throw new Error('No unsettled fee');
  if (!pending.feeTxid) throw new Error('Pay the fee first');
  if (usedTxids.has(mainTxid)) throw new Error('A new main transaction ID is required');

  const main = await verifyTransfer(mainTxid, { owner: userAddress, to: pending.recipient, minAmount: pending.amount });
  if (main.confirmedAt < pending.createdAt) main.failed.push('sentAfterSponsorship');
  if (main.failed.length > 0) {
    throw new Error(`Main transaction failed checks: ${main.failed.join(', ')}`);
  }

  usedTxids.add(mainTxid);
  await reclaimGas(userAddress);
  pendingFees.delete(userAddress);
  return { status: main.succeeded ? ('paid' as const) : ('main_transfer_failed' as const) };
};
```

Expose `getGasQuote`, `sponsorWithFee`, `payFee`, and `settleFee` as endpoints, here `/api/gas-quote`, `/api/sponsor`, `/api/sponsor/fee`, and `/api/sponsor/settle`, and give each one the JWT and wallet checks from the recipe's [step 1](https://www.dynamic.xyz/docs/recipes/integrations/tron-gas.md#1-set-up-the-sponsor-client-backend).

### 3. Cancel and expire sponsorships (backend)

If the user cancels before sending, or stops after paying the fee, reclaim the Energy and clear the pending fee. Expose `cancelSponsorship` as `/api/sponsor/cancel`. Run `expireStaleSponsorships` on a schedule, such as every minute, to reclaim Energy from abandoned flows. A wallet that spent sponsored Energy without paying the fee keeps its unsettled fee until the user pays it.

```typescript theme={"system"}
// Reclaiming moves the Energy the user spent back to the sponsor, so record the answer first.
const sponsoredGasWasUsed = async (userAddress: string, pending: PendingFee) => {
  if (!pending.gasUsed) {
    pending.gasUsed = (await getEnergyUsed(userAddress)) > pending.energyUsedAtSponsor;
  }
  return pending.gasUsed;
};

export const cancelSponsorship = async (userAddress: string) => {
  const pending = pendingFees.get(userAddress);
  if (!pending) return;
  // After the fee, cancelling only ends the sponsorship.
  if (!pending.feeTxid && (await sponsoredGasWasUsed(userAddress, pending))) {
    throw new Error('The sponsored gas was already used. Pay the fee instead.');
  }
  await reclaimGas(userAddress);
  pendingFees.delete(userAddress);
};

export const expireStaleSponsorships = async () => {
  for (const [userAddress, pending] of pendingFees) {
    if (Date.now() - pending.createdAt < PENDING_FEE_TTL_MS) continue;
    if (pending.feeTxid) {
      await reclaimGas(userAddress);
      pendingFees.delete(userAddress);
      continue;
    }
    if (pending.gasUsed) continue;
    const used = await sponsoredGasWasUsed(userAddress, pending);
    await reclaimGas(userAddress);
    if (!used) pendingFees.delete(userAddress);
  }
};
```

In production, keep these points in mind:

* Store `pendingFees` and `usedTxids` in a database that every server instance and the scheduled job share. On a serverless host, run `expireStaleSponsorships` from a scheduled endpoint, because an in-memory `Map` and a timer work only in one long-lived process.
* Set `PENDING_FEE_TTL_MS` longer than the slowest user takes to approve both transfers. An expired sponsorship loses its Energy, and a transfer sent afterwards can fail.
* If your backend also reclaims from other wallets when the sponsor runs low, skip wallets with a pending fee. Otherwise, it can take back the Energy between the user's two transfers.
* The TRX from `topUpBandwidth` can't be reclaimed. A user who cancels keeps it, so rate-limit sponsorships per user.

### 4. Send the fee, then the transfer (frontend)

Show the quoted fee on your confirm screen before calling `sendGaslessTransferWithFee`, because the user signs two transfers: the fee and the one they asked for. The function sends the fee first and waits for your backend to verify it, which takes a few seconds, before it asks the user to sign the transfer. If either transfer fails to send, it cancels the sponsorship. These functions reuse `USDT_CONTRACT` from the recipe's [step 5](https://www.dynamic.xyz/docs/recipes/integrations/tron-gas.md#5-send-a-transfer-without-trx-frontend).

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

type TronWebInstance = Awaited<ReturnType<typeof getTronWeb>>;

const postJson = async (path: string, body: Record<string, unknown>) => {
  const response = await fetch(path, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${getDefaultClient().token}`,
    },
    body: JSON.stringify(body),
  });
  if (!response.ok) {
    throw new Error(`${path} failed: ${await response.text()}`);
  }
  return response.json();
};

const sendUsdt = async (tronWeb: TronWebInstance, from: string, to: string, amount: string) => {
  const { transaction } = await tronWeb.transactionBuilder.triggerSmartContract(
    USDT_CONTRACT,
    'transfer(address,uint256)',
    { feeLimit: 100_000_000 },
    [
      { type: 'address', value: to },
      { type: 'uint256', value: amount },
    ],
    from,
  );
  const signedTransaction = await tronWeb.trx.sign(transaction);
  const result = await tronWeb.trx.sendRawTransaction(signedTransaction);
  if (!result.result) {
    throw new Error(`Broadcast failed: ${JSON.stringify(result)}`);
  }
  return result.txid as string;
};

export const getGasQuote = async (recipient: string, amount: string) => {
  const walletAccount = getPrimaryWalletAccount();
  if (!walletAccount || !isTronWalletAccount(walletAccount)) {
    throw new Error('No TRON wallet');
  }
  return postJson('/api/gas-quote', { userAddress: walletAccount.address, recipient, amount }) as Promise<{
    feeAmount: string;
    feeRecipient: string;
  }>;
};

export const sendGaslessTransferWithFee = async (recipient: string, amount: string, quotedFeeAmount: string) => {
  const walletAccount = getPrimaryWalletAccount();
  if (!walletAccount || !isTronWalletAccount(walletAccount)) {
    return;
  }
  const userAddress = walletAccount.address;

  const { feeAmount, feeRecipient } = await postJson('/api/sponsor', {
    userAddress,
    recipient,
    amount,
    maxFeeAmount: quotedFeeAmount,
  });

  const tronWeb = await getTronWeb({ walletAccount });

  let feeTxid: string;
  try {
    feeTxid = await sendUsdt(tronWeb, userAddress, feeRecipient, feeAmount);
  } catch (error) {
    await postJson('/api/sponsor/cancel', { userAddress });
    throw error;
  }
  const paid = await postJson('/api/sponsor/fee', { userAddress, feeTxid });
  if (paid.status !== 'fee_paid') return paid;

  let mainTxid: string;
  try {
    mainTxid = await sendUsdt(tronWeb, userAddress, recipient, amount);
  } catch (error) {
    // The fee is paid, so cancelling only ends the sponsorship.
    await postJson('/api/sponsor/cancel', { userAddress });
    throw error;
  }
  return postJson('/api/sponsor/settle', { userAddress, mainTxid });
};
```

A user who sends the transfer without the fee, outside this function, keeps an unsettled fee and can't be sponsored again. To let them pay it, send the fee transfer, call `/api/sponsor/fee`, and then call `/api/sponsor/settle` with the original `mainTxid`.

## Next steps

* [Handle gas for TRON transactions](https://www.dynamic.xyz/docs/recipes/integrations/tron-gas.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.