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…' })TON, Gram & the network id
Section titled “TON, Gram & the network id”Three names show up on this chain — here’s the map, because it trips people up:
| What | Value | Stays the same? |
|---|---|---|
| Network (the blockchain) | The Open Network — TON | ✅ Select it with chain: 'ton'. |
| Native coin (the token) | Gram · ticker GRAM | 🔁 Renamed from Toncoin (TON) on 2026-06-15. |
| CAIP-2 network id (on the wire) | tvm:-239 | ✅ The canonical id x402 tooling matches on. |
The Gram rebrand is token-only
Section titled “The Gram rebrand is token-only”On 2026-06-15, a TON community governance vote renamed the native token Toncoin → Gram and
its ticker TON → GRAM. It is a presentation-layer change: balances, addresses, smart
contracts, jettons, and staking are untouched — no migration, swap, or bridge. So in the SDK,
chain: 'ton' and token: 'native' are exactly as before; the only difference is that the native
coin’s symbol now reads GRAM (e.g. a 402’s extra.symbol, and estimateCost’s feeSymbol).
USD₮ and every other jetton are unaffected.
The network id is tvm:-239 (not ton:-239)
Section titled “The network id is tvm:-239 (not ton:-239)”Every x402 payment labels its chain with a CAIP-2
identifier — a universal namespace:reference string that lets any wallet, facilitator, or
discovery index agree on which chain a payment is on. For TON mainnet that is tvm:-239:
tvm— the namespace for the TON Virtual Machine family, per the chain-agnostic registry. (There is notonnamespace — that was a non-canonical id some tools, PipRail included, used early on.)-239— TON mainnet’s network global id, a constant carried in every TON block. (Testnet is-3.)
PipRail emits the canonical tvm:-239 so its TON 402s are matchable by standard x402 clients and
discovery indexes; an inbound challenge that still uses the legacy ton:-239 is accepted and
normalized on parse, so nothing breaks either way. (Unrelated: the SDK-internal proof locator
ton:<jetton-wallet>|<nonce> is a private string, not the network id — it is unchanged.)
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 { key }, where key 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: { key: mnemonic } })// W5 wallet: new PipRailClient({ chain: 'ton', wallet: { key: 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: { key: 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 | Gram (ticker GRAM, formerly 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 GRAM, 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 GRAM (the native coin) 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: { feeFormatted: '0.0…', feeSymbol: 'GRAM', feeDecimals: 9, basis: 'heuristic' } }// cost is the network fee in GRAM (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 GRAM (the native coin) for gas, so a wallet
flush with USD₮ but short on GRAM 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 GRAM for gas console.error('Top up the TON wallet (USD₮ and/or GRAM 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. "add ~0.05 GRAM 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.