Skip to content

Accept x402 USDT payments on Tron

Tron is the single largest stablecoin-payment rail on earth, holding roughly 45% of all USDT in circulation. You name chain: 'tron'; the driver auto-mounts on first use, so a pure-EVM install never downloads the Tron library. Tron settles in USD₮ (TRC-20) by default, with native TRX available too.

Tron’s library is an optional peer dep, lazy-loaded the first time you name the chain. Install it once:

Terminal window
npm install tronweb

That single dynamic import() is what keeps non-Tron installs lean. See the PaymentDriver architecture for how auto-mounting works.

A Tron wallet is { key }, a 32-byte hex key (Tron uses secp256k1, the same key format as EVM). The chain: 'tron' selector is what routes to the Tron driver.

import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({
wallet: { key: process.env.AGENT_KEY }, // 32-byte hex, with or without 0x
chain: 'tron',
})

payTo is a Base58 T… address. Passing an 0x… address throws WrongFamilyError. The full matrix of wallet shapes lives in Wallets by family.

On the server, name the chain and token. The canonical USD₮ contract is pre-filled, so you never paste an address:

import { requirePayment } from '@piprail/sdk'
requirePayment({ chain: 'tron', token: 'USDT', amount: '0.10', payTo: 'T…' })

Tron ships USD₮ only, mirroring TON’s “USD₮ only” decision. Native TRX is also a valid payment asset.

tokenAssetDecimalsNotes
'USDT'USD₮ (TRC-20, Tether-native)6The default. Contract pre-filled and verified on-chain.
'native'TRX6Supported for completeness; TRX is volatile gas, so prefer USD₮ for stable pricing.
{ address, decimals }Any other TRC-20n/aBase58 T… contract, passed by you.

A custom TRC-20 is just its Base58 contract plus decimals:

requirePayment({
chain: 'tron',
token: { address: 'T…', decimals: 6 },
amount: '0.10',
payTo: 'T…',
})

Unlike most chains, Tron gas is a meaningful cost. A USD₮ transfer burns Energy (~30k unstaked ≈ several TRX), so the payer must hold TRX as well as USD₮. Use estimateCost() to budget the total, payment plus TRX gas, before any funds move. Tron is where this matters most.

const url = 'https://api.example.com/report'
const { quote, cost } = await client.estimateCost(url)
// → { quote: PipRailQuote, cost: CostEstimate }
// cost is the network fee in TRX (the native gas coin), separate from the USD₮ payment:
console.log(quote.amountFormatted, quote.symbol) // '0.10' 'USDT', the payment
console.log(cost.feeFormatted, cost.feeSymbol) // e.g. '6.5' 'TRX' (cost.feeDecimals === 6)

Unlike NEAR (storage_deposit), Stellar/XRPL (trustlines), or Algorand (ASA opt-in), a Tron recipient needs no account setup to receive USD₮: any valid T… address can be paid. So planPayment() won’t raise a RECIPIENT_NOT_READY blocker for Tron on the USD₮ path. The only readiness requirement is on the payer side: enough TRX for Energy/bandwidth.

See planPayment() for the full readiness check.

On Tron the headline failure is a gas shortfall: you hold USD₮ but not enough TRX to cover Energy. Both an empty token balance and an empty TRX balance converge on one typed InsufficientFundsError (.code === 'INSUFFICIENT_FUNDS'), whose message names which one is short and echoes the raw chain code. Catch it on fetch:

import { InsufficientFundsError, PipRailError } from '@piprail/sdk'
const url = 'https://api.example.com/report'
try {
const res = await client.fetch(url)
console.log(res.status) // → 200 once the proof verifies
} catch (err) {
if (err instanceof InsufficientFundsError) {
// On Tron this is usually "no TRX for Energy", not "no USD₮".
console.error('Fund the payer (USD₮ and/or TRX):', err.message)
} else if (err instanceof PipRailError) {
console.error(`Payment failed [${err.code}]:`, err.message)
} else {
throw err
}
}

To distinguish a token shortfall from a gas shortfall before spending, call planPayment(): it reports them as separate INSUFFICIENT_TOKEN and INSUFFICIENT_GAS blockers (with a fundingHint) rather than throwing.

Tron verification is digest-bound (Template B): the proof is the transaction id, and verify() confirms the transfer by reading the tx, checking the recipient + amount, applying a recency window, and recording the txid in a single-use proof set. A USD₮ payment is a TRC-20 Transfer event; a native TRX payment is a plain TransferContract. Same path, just reading the contract instead of the event log.

Two Tron specifics worth knowing:

  • Verification reads the solidity node. The merchant verifies the confirmed transfer on Tron’s solidity (finality) node, not the full node. The proof only counts once it’s solidified.
  • Finality is slow-ish. A tx solidifies after ~19 blocks (~57s); until then verify() reads tx_not_found and is retried.

Because the proof is single-use, multi-instance deployments should plug in a persistent isUsed/markUsed store and keep maxTimeoutSeconds tight. See Replay protection and Proof binding for the mechanics.

Tron’s tronweb library doesn’t ship a clean browser ESM build, so run the Tron path server-side: the identical one line, on Node/Bun/Deno/Workers. The public TronGrid RPC (https://api.trongrid.io) is rate-limited; pass your own rpcUrl in production.

Holding the wrong token? quoteSwap() prices a same-chain swap read-only and swap() is the only call that moves anything. It is opt-in and never automatic: paying never swaps and planning never swaps.

Tron has no protocol-level swap, so a named venue routes it: SunSwap V2. It is keyless, and PipRail sets no integrator or platform fee on it.

⭐ This is one of the routes that is exact-output: the invoice amount goes in as the output and the input is capped on chain, rather than being sized from a probe.

This route ships without a mainnet proof, and says so. The route is real, and every part of it that can be checked without spending has been: both directions quote live through the SDK, and the approve and the router call both execute cleanly in constant-call simulations against the real contracts. It has NOT been broadcast because Tron charges about 230,629 ENERGY per swap, which without staked energy is roughly 23 TRX (about $7.79) regardless of trade size, plus about 10 TRX more the first time a TRC-20 is approved.

const quote = await client.quoteSwap({ from: 'native', to: 'USDT', wantAmount: '0.50' })
if (quote) await client.swap(quote) // null means no route, never "no funds"

Full guide: Swapping tokens. Every route, indexed by chain as well as by venue, is at piprail.com/swaps.