TON Transaction History: lt, Hashes and Archive Depth
How TON transaction history works: logical time (lt), hashes and the last transaction pointer. Reading history via MCP and why depth needs an archive node.
It's 3:47 AM and a message lands: a user paid for an order in TON, but the bot never credited it. You open the explorer — the money is there, the incoming transfer is right where it should be. So the blockchain is fine; the problem is how your service reads transaction history. And that's when you find out that history in TON works nothing like what you're used to from Ethereum.
Payment monitoring on TON looks deceptively simple: poll the balance, react to changes. In practice it falls apart fast. Two payments of the same amount within one minute — was that one payment or two? A user paid in USDT and the GRAM balance didn't move at all. You need a six-month statement, but the liteserver only returns recent transactions and stays silent about the rest.
If you're wiring an AI agent to TON or building a backend that reconciles payments, let's get down to business: what TON transaction history actually is, why you need the lt + hash pair, why depth requires an archive node, and how to read all of this from an agent via MCP.
What transaction history is in TON and why an agent needs it
In TON, every account — a wallet, a contract, a jetton wallet — has its own chain of transactions. Each transaction is the result of processing an inbound message: receiving GRAM, transferring a jetton, calling a contract method. For an agent (Claude, Cursor, any MCP client) that accepts payments or reconciles settlements, history is the only reliable source of truth: the balance tells you "how much right now", while history tells you "what exactly happened, when, from whom and for how much".
The practical, day-to-day tasks an agent needs history for:
- confirming a payment arrived ("check whether a transfer hit this address in the last hour");
- building a wallet statement;
- tracking down a specific transaction by its identifier;
- reconciling incoming jettons (say, USDT) against an expected amount.
The catch is that "give me transactions #100–120" doesn't work in TON. There's no global, contiguous block numbering you could use to "fetch transaction number N". Ordering works differently — through logical time.
Logical time (lt): why TON has no ordinary block numbering
In Ethereum everything is straightforward: there's block #19,000,000 with transactions in order inside it. One global counter, monotonically increasing, everyone looks at the same number.
TON is a multi-threaded system: a masterchain, workchains, shards that split and merge under load. There's simply no single "block number" you could use to linearly order every event in the network. Instead, TON uses logical time (lt) — a monotonically increasing counter the network uses to order events, messages and transactions. It guarantees one thing: if event A influenced event B, then lt(A) < lt(B).
The practical consequence: a transaction's position is defined not by a block number but by the pair (lt, hash). lt handles ordering (who came first), hash handles unambiguous identification of a specific transaction. On their own they're useless for a point lookup: different objects can have nearby lt values, and a hash alone doesn't tell the liteserver where to start reading.
Transaction hash and the lt + hash pair: the account's last transaction pointer
A transaction hash is its cryptographic fingerprint; on TON it comes in base64 or hex. lt says "when, in order", hash says "which one exactly", because a single lt slice can theoretically be ambiguous, and without the hash the liteserver won't hand the transaction over. Burn this into memory: (lt, hash) is mandatory as a pair. Just lt or just hash is not enough.
Where do you get the starting point? The account state stores a pointer to the very latest transaction — last_transaction_id, which is exactly that lt + hash pair (the last_trans_lt / last_trans_hash fields). It's the head of the list, and it's where the backwards walk through history begins.
In the TONNode MCP toolkit this is what get_account_state does — it returns the account's status, flags and the pointer to its last transaction.
How get_transactions pages through history: lt + hash and the prev_trans chain
Now the key detail that makes history pageable at all. Every transaction, besides its own lt and hash, carries two fields: prev_trans_lt and prev_trans_hash — a reference to the previous transaction of the same account. In other words, transactions form a singly linked list running backwards in time; the head is the last_transaction_id from the account state.
get_transactions takes:
account— the account address;- the starting
ltandhash— the point to read from; count— the batch size.
It returns a batch of transactions, walking backwards from the given point. The pagination logic:
- Take
last_trans_lt/last_trans_hashfromget_account_state— that's the head. - Call
get_transactions(account, lt, hash, count)— get a batch. - From the last transaction in the batch, take
prev_trans_ltandprev_trans_hash. - Call
get_transactionsagain with that pair — that's the next page. - Repeat until
prev_trans_lthits 0 (the beginning of the account's life) or until you've gone deep enough.
That's how pagination works in TON: not "page 2", but "continue from this (lt, hash) pair".
Fresh blocks vs deep history: why you need an archive node
This is where most people trip up. A regular liteserver serves fresh blocks and the account's recent history: it keeps a limited state window — some number of the latest blocks — and as the network grows, older data gets evicted from it. Walk the prev_trans chain deep enough and at some point the liteserver simply stops returning transactions: they're physically no longer inside its window. You'll see a fresh payment just fine — a transaction from six months ago, not so much.
That's not a bug, it's the design: keeping every account's full path since genesis is expensive. Deep history lives on an archive node — a node that never prunes old blocks and holds the network's complete state for its entire history.
The practical takeaway:
- payment detection, a fresh statement, "did anything arrive in the last hour" — a regular liteserver is enough;
- a full wallet audit from day one — you need an archive node.
The public liteservers from TON's global config are a pain point of their own. They're shared and rate-limited: under load they routinely answer not ready or hit ADNL timeouts, and they don't have deep history either. If not ready is exactly what you're getting, that's a symptom of an overloaded shared liteserver — full breakdown in why a liteserver answers not ready and how to fix it.
Being honest about TONNode: the archive node is on the roadmap and currently syncing — I'm not promising you archive depth as a shipped feature. What you get today is fresh and recent history through a managed endpoint, without the public-liteserver lottery. For the full archive back to genesis, wait for a separate announcement.
How to read TON history from an AI agent via MCP
TONNode is a hosted MCP server for TON with exactly 16 tools, and get_transactions is part of the read set. MCP (Model Context Protocol) is the standard that lets an agent call tools by itself, without you hand-crafting ADNL requests. No SDK to install, no liteserver to run, no TL-B to parse — the agent hits the tool directly.
Free local setup
The full read set, public config, the @tonnode/mcp package — open source, MIT:
{
"mcpServers": {
"ton": {
"command": "npx",
"args": ["-y", "@tonnode/mcp"]
}
}
}
The package speaks TON's native ADNL protocol, no HTTP layers in between.
Hosted endpoint with your own key
For guaranteed throughput, use the hosted endpoint with your own key:
{
"mcpServers": {
"ton": {
"type": "http",
"url": "https://mcp.tonnode.io/mcp",
"headers": { "Authorization": "Bearer tn_live_…" }
}
}
}
For reading history you'll want:
get_account_state— the starting point of the walk (last_trans_lt/last_trans_hash), account status and flags;get_transactions— the transactions themselves, page by page, keyed by the(lt, hash)pair;parse_address— offline address conversion betweenEQ/UQ/raw;get_jetton_infoandget_jetton_balance— for jetton history.
In practice: payment detection and paging through history
Let's put together the typical scenario — "did the payment arrive, and for how much".
The agent prompt
Using the
tontool, callget_account_stateforEQC…myshop, thenget_transactionsfrom itslast_trans_lt/last_trans_hash, count 20. Find an incoming transfer of 5 GRAM within the last 10 minutes. Normalize sender addresses to EQ viaparse_addressbefore comparing. If you need deeper history, takeprev_trans_lt/prev_trans_hashfrom the last transaction and repeatget_transactions.
The agent will fetch the head of the chain on its own, page through a batch and reconcile the amounts.
Pagination pseudocode
The same walk in pseudocode (the calls are MCP tools the agent invokes):
state = get_account_state(account)
lt = state.last_trans_lt
hash = state.last_trans_hash
while lt != 0:
batch = get_transactions(account, lt, hash, count=20)
for tx in batch:
if matches_expected_payment(tx): # incoming message with the right amount/comment
return tx
last = batch[-1]
lt = last.prev_trans_lt
hash = last.prev_trans_hash # without the hash you can't request the next page
Details that save hours of debugging
- Addresses in transaction fields come in raw form (
0:abcd…). Before comparing a sender or recipient against the familiarEQ…address, normalize both to the same format viaparse_address— otherwise the string comparison will fail even though it's the same address. More on the formats in EQ, UQ and raw in TON addresses. - USDT payments are invisible on the GRAM wallet's chain. Jettons live on a separate jetton wallet whose address is computed on-chain (
get_jetton_balancedoes that for you and returns the current balance). For jetton history, walk the jetton wallet's transactions, not the main wallet's. - Convert raw amounts through decimals. Jetton amounts come in minimal units: to turn
5000000into a human-readable 5 USDT, takedecimalsfromget_jetton_info— for USDT it's 6, for most jettons it's 9. Mix them up and you're off by a factor of 1000. Full walkthrough in how to detect an incoming USDT payment on TON. - Dedupe by
(lt, hash), not by amount. Two identical payments differ only by their lt+hash pair — that pair is your idempotency key.
If, beyond history, the agent also needs to read a contract's current state, that's what run_get_method is for — calling get methods without an SDK is covered in reading TON without an SDK via get methods.
TL;DR
- TON has no block numbers like Ethereum — a transaction's position is defined by the pair
(lt, hash): lt for ordering, hash for identification, and they're mandatory together. - An account's transactions form a linked list via
prev_trans_lt/prev_trans_hash; you page backwards, starting from thelast_transaction_idinget_account_state. - A regular liteserver serves fresh and recent history; full depth is the job of an archive node.
- For jettons, don't forget decimals from
get_jetton_info(USDT = 6) and raw addresses that need to go throughparse_address.
Done wrestling with public liteservers that answer not ready? get_transactions works out of the box on the free Hobby plan — 60 requests/min, forever, no card, key issued right after sign-in.
Wire up TON history reads in a minute → tonnode.io/dashboard?plan=hobby
And for everything else an agent can do on TON beyond reading history — all 16 tools are on the TONNode tools page.
Give your agent TON access
16 MCP tools: reads, non-custodial swaps, cross-chain and wallets. Free tier — 60 req/min, no card required.