Skip to content

TON

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

Three names show up on this chain — here’s the map, because it trips people up:

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

On 2026-06-15, a TON community governance vote renamed the native token Toncoin → Gram and its ticker TONGRAM. 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.

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 no ton namespace — 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.)

The TON libraries are optional peer deps — install them once and the lazy import finds them:

Terminal window
npm install @ton/ton @ton/core @ton/crypto

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.

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 })

Name the symbol; the SDK fills in the jetton master and decimals.

TokenBuilt inNotes
'USDT'YesUSD₮ (Tether-native, dominant on TON). Master + 6 decimals verified on-chain.
'native'YesGram (ticker GRAM, formerly Toncoin/TON), 9 decimals (nanoton).
custom jettonAny 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 })

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₮ payment

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"
}

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.

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.