TON
Introduction
Section titled “Introduction”TON (The Open Network, the Telegram blockchain) is a non-EVM family. Name it — chain: 'ton' —
and the driver auto-mounts on first use, so a pure-EVM or Solana install never downloads the
TON libraries. The protocol layer is unchanged; only the wallet shape and one RPC caveat differ.
import { requirePayment } from '@piprail/sdk'
requirePayment({ chain: 'ton', token: 'USDT', amount: '0.10', payTo: 'EQ…' })Install the peer dependency
Section titled “Install the peer dependency”The TON libraries are optional peer deps — install them once and the lazy import finds them:
npm install @ton/ton @ton/core @ton/cryptoThe wallet
Section titled “The wallet”A TON wallet is a 24-word { mnemonic } (a string[] or one space-separated string) or a ready
{ keyPair }. The wallet contract defaults to v4; pass version: 'v5r1' for a W5 wallet — it
must match the version your funded address was created with.
import { PipRailClient } from '@piprail/sdk'
const mnemonic = process.env.TON_MNEMONIC // 24 words, space-separated or a string[]
const client = new PipRailClient({ chain: 'ton', wallet: { mnemonic } })// W5 wallet: new PipRailClient({ chain: 'ton', wallet: { mnemonic, version: 'v5r1' } })The shape is checked synchronously at bind time, so passing an EVM or Solana wallet fails fast
with a WrongFamilyError. See Wallets by family.
You need a free RPC API key
Section titled “You need a free RPC API key”TON is the only chain with a one-time setup step. The default keyless toncenter endpoint is
rate-limited (~1 req/s) and will stall confirm() / verify(), which poll and read archival
history. Use a keyed, archival-capable endpoint and put the key in the URL:
const rpcUrl = `https://toncenter.com/api/v2/jsonRPC?api_key=${process.env.TONCENTER_KEY}`const payTo = 'EQ…' // your bounceable TON address (EQ… or UQ…)
requirePayment({ chain: 'ton', token: 'USDT', amount: '0.10', payTo, rpcUrl })new PipRailClient({ chain: 'ton', wallet: { mnemonic }, rpcUrl })Tokens
Section titled “Tokens”Name the symbol; the SDK fills in the jetton master and decimals.
| Token | Built in | Notes |
|---|---|---|
'USDT' | Yes | USD₮ (Tether-native, dominant on TON). Master + 6 decimals verified on-chain. |
'native' | Yes | Toncoin (TON), 9 decimals (nanoton). |
| custom jetton | — | Any other jetton via { master, decimals } (e.g. USDe). |
// A custom jetton is { master, decimals }:requirePayment({ chain: 'ton', token: { master: 'EQ…', decimals: 6 }, amount: '0.10', payTo })Receive prerequisite — none
Section titled “Receive prerequisite — none”The merchant needs no setup. The payer’s attached gas (~0.05 TON, leftover refunded)
auto-deploys the merchant’s jetton wallet on first receipt, so there’s no trustline or opt-in to
register — planPayment() won’t raise RECIPIENT_NOT_READY for TON. The payer, however,
needs Toncoin for gas even when paying USD₮ — budget it with
estimateCost(), which reports the fee in the native coin.
const { quote, cost } = await client.estimateCost('https://api.example.com/report')// → { quote: { amountFormatted: '0.10', symbol: 'USDT', … }, cost: { amountFormatted: '0.0…', symbol: 'TON', basis: 'estimated' } }// cost is the network fee in TON (the native gas coin), separate from the USD₮ paymentWhen the payer can’t cover gas
Section titled “When the payer can’t cover gas”The headline TON caveat is that even a USD₮ payment burns Toncoin for gas, so a wallet flush with
USD₮ but short on TON still can’t settle. planPayment() reports
that as a blocker without throwing; fetch() throws a typed
InsufficientFundsError (.code === 'INSUFFICIENT_FUNDS') so you can
catch it and top up the right coin:
import { InsufficientFundsError } from '@piprail/sdk'
try { const res = await client.fetch('https://api.example.com/report') console.log(await res.text())} catch (err) { if (err instanceof InsufficientFundsError) { // fund the payer — USD₮ for the payment, and TON for gas console.error('Top up the TON wallet (USD₮ and/or Toncoin gas):', err.message) } else { throw err }}To branch before spending instead of catching, plan first:
const plan = await client.planPayment('https://api.example.com/report')if (!plan) { await client.fetch('https://api.example.com/report') // not payment-gated} else if (plan.payable) { await client.fetch('https://api.example.com/report')} else { console.log(plan.fundingHint) // e.g. "needs ~0.05 TON for gas"}Proof binding — Template A (memo-bound)
Section titled “Proof binding — Template A (memo-bound)”TON uses Template A: the challenge nonce rides in the jetton transfer
comment, and verify() matches it on the merchant’s own jetton wallet — so a look-alike
jetton can’t satisfy the gate, and the proof is cryptographically bound to the challenge that
issued it.
Server-side only
Section titled “Server-side only”TON’s libraries don’t ship a clean browser ESM build yet, so run the TON path server-side — the identical one line, on Node, Bun, Deno, or Workers. The lazy import means a pure-EVM page never downloads them. See Chains & tokens for the full cross-chain caveat list.