Setting Up Payment Acceptance in TON
TON is not Ethereum with a different RPC. The main feature that breaks intuition: transactions are asynchronous and have a tree structure. When a user sends TON to your address, that's one transaction. When sending Jetton (USDT on TON, for example) — that's a chain of three messages: transfer → internal message → notification. Monitoring must account for this.
Two Scenarios: Native TON and Jetton (USDT/USDC)
Accepting Native TON
The simplest case. Generate a unique address or use one address with comment (memo) for payment identification — TON natively supports text comments in transactions.
Monitoring via TON Center API or TonAPI:
import { TonClient } from '@ton/ton';
import { Address } from '@ton/core';
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
apiKey: process.env.TONCENTER_API_KEY,
});
async function checkIncomingTransactions(
address: string,
lastLt: string // last known logical time
) {
const addr = Address.parse(address);
const transactions = await client.getTransactions(addr, {
limit: 20,
lt: lastLt,
archival: false,
});
for (const tx of transactions) {
// Only incoming, not bounce
if (tx.inMessage && tx.inMessage.info.type === 'internal') {
const info = tx.inMessage.info;
const value = info.value.coins; // in nanoTON
const comment = tx.inMessage.body; // text comment
// Match comment with our payment ID
console.log(`Received: ${value} nanoTON, comment: ${comment}`);
}
}
}
Important: check bounce flag and bounced flag. Bounced transaction means recipient returned funds — don't count as payment.
Accepting Jetton (USDT, USDC, NOT, etc.)
Jetton Transfer works differently: user sends message to their JettonWallet contract, which sends internal message to recipient's JettonWallet, which sends transfer_notification to recipient address.
To receive Jetton transfer notifications, specify forward_ton_amount and forward_payload in parameters:
transfer_notification#7362d09c
query_id: uint64
amount: coins // Jetton quantity
sender: MsgAddress // sender address
forward_payload: ^Cell // our custom payload (payment ID)
To receive Jetton on server, monitor not the main address but our JettonWallet address:
// Get our JettonWallet address for USDT
async function getJettonWalletAddress(
ownerAddress: string,
jettonMasterAddress: string
): Promise<string> {
const master = client.open(
JettonMaster.create(Address.parse(jettonMasterAddress))
);
const walletAddr = await master.getWalletAddress(
Address.parse(ownerAddress)
);
return walletAddr.toString();
}
// USDT on TON mainnet
const USDT_MASTER = 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs';
Unique Addresses vs Comment-Based Identification
Approach 1: Comment/Memo Identification
One address for all payments, user specifies unique comment (payment ID). Simple to implement, but requires mandatory UX — explain to user the necessity of comment. User error = lost payment (need manual reconciliation).
Approach 2: Unique Address Per Payment
Generate HD wallet addresses (TON supports standard BIP39 mnemonics + non-standard derivation). Each order — separate address. No comments, no user errors, simple monitoring.
import { mnemonicToPrivateKey } from '@ton/crypto';
import { WalletContractV4 } from '@ton/ton';
async function derivePaymentAddress(
masterMnemonic: string[],
orderIndex: number
): Promise<string> {
// TON is not standard BIP44, use different subwalletIds
const keyPair = await mnemonicToPrivateKey(masterMnemonic);
const wallet = WalletContractV4.create({
publicKey: keyPair.publicKey,
workchain: 0,
walletId: 698983191 + orderIndex, // unique subwalletId
});
return wallet.address.toString({ bounceable: false });
}
Downside: need to periodically consolidate funds from child addresses to main (consolidation sweep).
Polling vs Webhook
TON Center API supports webhook subscriptions on transactions by address — more convenient than polling. TonAPI (tonapi.io) — richer API with WebSocket streaming.
For self-hosted: TON node (C++ or Go implementation) with custom indexer. But much more complex — node occupies ~2TB and syncs for several days.
For most projects: TonAPI with WebSocket + polling as fallback with cursor by lt (logical time).
Confirmation
TON has no concept of "number of confirmations" like Bitcoin. Transaction is either included in block and final, or not. TON uses BFT-like consensus — finality achieved within seconds of block inclusion. Practically: sufficient to wait 1 block (~5 seconds) and verify transaction is not bounced.
Testing
TON Testnet is a separate network. Get Testnet TON via bot @testgiver_ton_bot. API endpoint: https://testnet.toncenter.com/api/v2/jsonRPC. For development also useful Sandbox from Blueprint — local TVM emulator without network calls.







