Skip to content

Tron

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.

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

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-20Base58 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…',
})

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') — 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() 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.