Tron
Introduction
Section titled “Introduction”Tron is the single largest stablecoin-payment rail on earth — it holds 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 { privateKey } — 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: { privateKey: 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 | — | Base58 T… contract — pass it yourself. |
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 — budget TRX
Section titled “Gas is real money — 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') — its
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 — the 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.