Accept x402 USDT payments on Tron
Introduction
Section titled “Introduction”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.
Install the peer dependency
Section titled “Install the peer dependency”Tron’s library is an optional peer dep, lazy-loaded the first time you name the chain. Install it once:
npm install tronwebThat single dynamic import() is what keeps non-Tron installs lean. See the
PaymentDriver architecture for how auto-mounting works.
The wallet
Section titled “The wallet”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.
Accepting payments
Section titled “Accepting payments”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…' })Tokens
Section titled “Tokens”Tron ships USD₮ only, mirroring TON’s “USD₮ only” decision. Native TRX is also a valid payment asset.
token | Asset | Decimals | Notes |
|---|---|---|---|
'USDT' | USD₮ (TRC-20, Tether-native) | 6 | The default. Contract pre-filled and verified on-chain. |
'native' | TRX | 6 | Supported for completeness; TRX is volatile gas, so prefer USD₮ for stable pricing. |
{ address, decimals } | Any other TRC-20 | n/a | Base58 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…',})Gas is real money, so budget TRX
Section titled “Gas is real money, so budget TRX”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 paymentconsole.log(cost.feeFormatted, cost.feeSymbol) // e.g. '6.5' 'TRX' (cost.feeDecimals === 6)Receiving needs no setup
Section titled “Receiving needs no setup”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.
When a payment can’t go through
Section titled “When a payment can’t go through”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.
Proof binding: digest-bound (Template B)
Section titled “Proof binding: digest-bound (Template B)”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()readstx_not_foundand 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.
Run server-side
Section titled “Run server-side”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.
Swapping on Tron
Section titled “Swapping on Tron”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.