TON Payment Acceptance Setup

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
TON Payment Acceptance Setup
Simple
~2-3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

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.