MCP tool reference
The TONNode MCP server (@tonnode/mcp) exposes 16 tools — no more, no fewer. This page documents every one of them: arguments, types, defaults, a real call and a real response.
Everything here was read off the running server (tools/list handshake plus live tools/call responses against TON mainnet). Sample responses are genuine captures; the numbers in them were true at the moment of capture and will have moved since.
TONNode ships two things: this MCP server and liteserver node access (see the liteserver documentation). There is no REST API and no mempool stream — if you see either mentioned anywhere, it is not shipped.
Connecting
The paid product is the hosted endpoint https://mcp.tonnode.io/mcp. The key travels in a header — nothing to install. The exact config differs per client.
Cursor (.cursor/mcp.json) and Claude Code (.mcp.json) speak to a remote MCP server directly:
{
"mcpServers": {
"ton": {
"url": "https://mcp.tonnode.io/mcp",
"headers": { "Authorization": "Bearer <your key>" }
}
}
}Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json) — its config file launches local processes and has no field for a remote URL, so the remote server is bridged with mcp-remote. The header is passed as a single argument with no space after the colon:
{
"mcpServers": {
"ton": {
"command": "npx",
"args": [
"-y", "mcp-remote", "https://mcp.tonnode.io/mcp",
"--header", "Authorization:Bearer <your key>"
]
}
}
}VS Code (.vscode/mcp.json) — VS Code uses servers, not mcpServers; a copied mcpServers block is silently ignored:
{
"servers": {
"ton": {
"type": "http",
"url": "https://mcp.tonnode.io/mcp",
"headers": { "Authorization": "Bearer <your key>" }
}
}
}Codex (~/.codex/config.toml) reads the key from an environment variable:
[mcp_servers.ton]
url = "https://mcp.tonnode.io/mcp"
bearer_token_env_var = "TONNODE_KEY"Running it locally. npx -y @tonnode/mcp starts the same 16 tools over stdio. That mode has no authentication and takes no TONNode key: it is a local process that talks to the liteservers listed in the public TON global config. Point it at other liteservers with TON_LITESERVERS, or at testnet with --testnet (the swap tools refuse to run there).
Get a key: connect a wallet at tonnode.io/dashboard, top up the balance (GRAM or USDT on-chain, or xRocket inside Telegram), pick a plan. A plan is a rate limit, not a request quota: Hobby $0 / 60 rpm, Pro $29/mo / 300 rpm, Scale $199/mo / 1200 rpm. There is no monthly request cap to run out of.
Source: github.com/tonnode · package: @tonnode/mcp 0.9.1
A programmatic client
Every example on this page is a name + arguments pair. This helper runs any of them; it is used verbatim in the snippets below.
// npm install @modelcontextprotocol/sdk
// TONNODE_KEY=tn_live_… node tonnode.mjs
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("https://mcp.tonnode.io/mcp"),
{ requestInit: { headers: { Authorization: `Bearer ${process.env.TONNODE_KEY}` } } }
);
const client = new Client({ name: "tonnode-docs", version: "1.0.0" });
await client.connect(transport);
/** Call a tool and return the parsed JSON result. Throws on tool errors. */
export async function call(name, args = {}) {
const res = await client.callTool({ name, arguments: args });
if (res.isError) throw new Error(res.content[0].text);
return res.structuredContent ?? JSON.parse(res.content[0].text);
}Every tool returns the same envelope: a single text content block holding pretty-printed JSON, plus the identical object as structuredContent. Errors come back as isError: true with {"error": "<message>"} in the text block.
Without an SDK
The transport is plain Streamable HTTP. initialize, keep the mcp-session-id, then call:
KEY=tn_live_your_key
# 1. initialize — the session id comes back in a response header
curl -sD headers.txt -X POST https://mcp.tonnode.io/mcp \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-06-18","capabilities":{},
"clientInfo":{"name":"curl","version":"1.0"}}}'
SID=$(awk -F': ' '/^mcp-session-id/{print $2}' headers.txt | tr -d '\r')
# 2. tell the server the handshake is done
curl -s -X POST https://mcp.tonnode.io/mcp \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" -H "mcp-session-id: $SID" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
# 3. call a tool
curl -s -X POST https://mcp.tonnode.io/mcp \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" -H "mcp-session-id: $SID" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"get_masterchain_info","arguments":{}}}'Both Accept values are mandatory — the transport rejects a POST without them. Responses arrive as a one-event SSE frame (event: message + data: {…}). JSON-RPC batches are rejected with 400.
Conventions
These hold across every tool.
| Convention | Detail |
|---|---|
| Addresses | Accepted in friendly (EQ… bounceable, UQ… non-bounceable) or raw (0:…, -1:…) form, interchangeably. Returned in friendly bounceable form, except wallet echoes from the build tools, which are non-bounceable. |
| Amounts | Always raw indivisible units, as decimal strings. GRAM has 9 decimals (1 GRAM = 1000000000), USDT on TON has 6. Read any other jetton's decimals with get_jetton_info. |
at_seqno |
Chain reads are anchored to a masterchain block and report which one, so several reads can be reconciled against the same height. |
bigint safety |
Every large number is a string, never a JSON number. |
| Native coin naming | The coin was renamed from Toncoin to GRAM; the network is still TON. Fields are *_gram / *_nano. |
| Swaps are mainnet-only | The swap and cross-chain tools refuse to run on testnet — the RFQ backend has no public testnet endpoint. |
| Nothing is signed | No tool ever holds a key, signs, or broadcasts. The build_* tools return unsigned TonConnect payloads for the wallet owner to approve. |
Tool annotations
The server declares MCP annotations so orchestrators can decide what to auto-approve. Do not auto-run anything in the bottom two rows.
| Annotation set | Tools |
|---|---|
readOnly, idempotent, openWorld |
get_masterchain_info, get_balance, get_account_state, get_transactions, run_get_method, get_jetton_balance, get_jetton_info |
readOnly, idempotent, closed world |
parse_address |
readOnly, not idempotent |
get_swap_quote, get_crosschain_quote, track_crosschain_swap |
not readOnly — emits an armed payload or secret material |
build_swap_tx, build_crosschain_swap_tx, build_crosschain_refund, generate_wallet |
not readOnly, destructive — irreversible |
disclose_crosschain_secret |
Tool index
Chain reads
| Tool | Answers |
|---|---|
get_masterchain_info |
Is the network (and my endpoint) alive? What is the current height? |
get_balance |
How much GRAM does this address hold? |
get_account_state |
Is this contract deployed, active, frozen? |
get_transactions |
Did the payment arrive? What did this address just do? |
run_get_method |
What does this contract's get-method return? |
Jettons
| Tool | Answers |
|---|---|
get_jetton_balance |
How much USDT (or any TEP-74 token) does this wallet hold? |
get_jetton_info |
What are this token's decimals, supply, admin? |
Utilities
| Tool | Answers |
|---|---|
parse_address |
Are these two addresses the same account? What is the raw form? |
Swaps on TON
| Tool | Answers |
|---|---|
get_swap_quote |
What do I get for 5 GRAM right now? |
build_swap_tx |
Turn that quote into something my wallet can sign. |
Cross-chain (ordered flow — see the lifecycle)
| Tool | Step |
|---|---|
get_crosschain_quote |
1 — price the TON → EVM trade |
build_crosschain_swap_tx |
2 — unsigned escrow transfer + the HTLC secret |
track_crosschain_swap |
3 — poll both chains |
disclose_crosschain_secret |
4 — settle |
build_crosschain_refund |
fallback — reclaim a stalled escrow |
Wallet
| Tool | Answers |
|---|---|
generate_wallet |
Mint a fresh wallet for an agent. Returns secrets. |
Chain reads
These run over TON's native ADNL liteserver protocol — no HTTP indexer in the middle.
get_masterchain_info
The latest masterchain block: workchain, shard, seqno and block hashes.
When to use. Health-checking the network or your own endpoint, and getting the current block height. The masterchain produces a block roughly every 3 seconds — if seqno does not grow between two calls a few seconds apart, the liteserver answering you is lagging.
Parameters. None.
Example call
await call("get_masterchain_info");Example response
{
"workchain": -1,
"shard": "-9223372036854775808",
"seqno": 82174631,
"rootHash": "ZoR3D4k9QcLQ2YEw9YdKnnG7cKCba48XAB75VnSDNr8=",
"fileHash": "g4ZSWMffk+PC+0p4zkKqchd/d2hD+XCD8PykSkufJps="
}| Field | Type | Meaning |
|---|---|---|
workchain |
number | Always -1 for the masterchain |
shard |
string | Shard id as a signed 64-bit decimal string |
seqno |
number | Masterchain block height |
rootHash / fileHash |
string | Block hashes, base64 |
get_balance
Native GRAM balance of an address.
When to use. The question is about the native coin only. For tokens use get_jetton_balance; for deployment status and the last-transaction cursor use get_account_state.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
address |
string | yes | — | TON address, friendly (EQ…/UQ…) or raw (0:…/-1:…) |
Example call
await call("get_balance", {
address: "-1:3333333333333333333333333333333333333333333333333333333333333333"
});Example response
{
"address": "Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF",
"balance_gram": "1329968526.250365411",
"balance_nano": "1329968526250365411",
"at_seqno": 82174631
}| Field | Type | Meaning |
|---|---|---|
address |
string | Input normalized to friendly bounceable form |
balance_gram |
string | Human-readable decimal, e.g. "12.5" |
balance_nano |
string | Raw nanocoins (1 GRAM = 1e9) |
at_seqno |
number | Masterchain block the reading is anchored to |
A never-funded address returns 0 — that is a valid answer, not an error.
get_account_state
Full account state: status, balance, last-transaction pointer, and whether code and data are deployed.
When to use. Checking whether a contract or wallet is deployed, diagnosing an address that does not respond, and before run_get_method — a get-method needs status: "active".
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
address |
string | yes | — | TON address, friendly or raw |
Example call
await call("get_account_state", {
address: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" // USDT master
});Example response
{
"address": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
"status": "active",
"balance_gram": "1067.260130657",
"last_transaction": {
"lt": "92706854000020",
"hash": "71a1f753f02267e5a6e1db2c3b3bc7771adf053f8c51bded5cb2e2276fd42b8d"
},
"has_code": true,
"has_data": true,
"at_seqno": 82174632
}| Field | Type | Meaning |
|---|---|---|
status |
string | active, frozen or uninit |
balance_gram |
string | Native balance, human-readable |
last_transaction |
object | null | { lt, hash } — lt decimal string, hash 64-char hex. null for an account that never transacted |
has_code / has_data |
boolean | Contract code/data present (always false unless status is active) |
at_seqno |
number | Anchor block |
Reading it. status: "uninit" with a non-zero balance means funds arrived but the wallet contract has not been deployed yet — normal for a freshly funded wallet. last_transaction is exactly the cursor get_transactions starts from.
get_transactions
Recent transactions of an address, newest first.
When to use. Verifying that a payment arrived, listing wallet activity, tracing what an address just did.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
address |
string | yes | — | TON address, friendly or raw |
limit |
integer | no | 10 |
How many transactions to return, 1–30 |
Example call
await call("get_transactions", {
address: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
limit: 2
});Example response
{
"address": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
"transactions": [
{
"hash": "71a1f753f02267e5a6e1db2c3b3bc7771adf053f8c51bded5cb2e2276fd42b8d",
"lt": "92706854000020",
"unix_time": 1785083044,
"in_value_gram": "0.012591002",
"in_from": "EQBOmxbUlIYOTJ4I8UODLUHqmIVYZm5KU8NU1I_kh0OyFOAH",
"out_messages": 1,
"total_fees_gram": "0.000449864"
},
{
"hash": "3ff362d5e82968e1e032f66b17c0d8ce94a06ab2813cd98fc346c1dfacb9e264",
"lt": "92682482000007",
"unix_time": 1785073058,
"in_value_gram": "0.012591002",
"in_from": "EQDTGD-F77I51teW6XiaDcVaVU2zcnzotx_-ntCeghp5zt6x",
"out_messages": 1,
"total_fees_gram": "0.000449399"
}
]
}| Field | Type | Meaning |
|---|---|---|
hash |
string | Transaction hash, hex |
lt |
string | Logical time |
unix_time |
number | Block time, unix seconds |
in_value_gram |
string | null | GRAM attached to the incoming message; null when the transaction has no internal inbound message (outgoing-only, or an external-in) |
in_from |
string | null | Sender of the incoming message |
out_messages |
number | Count of outgoing messages |
total_fees_gram |
string | Total fees charged |
parse_error |
boolean | Present (with hash) instead of the other fields when a transaction cannot be decoded |
Limits worth knowing.
- No pagination. Each call reads backwards from the account's newest transaction, so at most the 30 most recent are reachable. There is no cursor argument.
- An address that was never active returns
"transactions": []— not an error. - History depth. A liteserver that has pruned the requested range answers
lt not in db. The tool rewrites that into an explicit message telling you to pointTON_LITESERVERSat an archive node. Deep history needs an archive-capable endpoint — the liteservers in the public global config are non-archive.
run_get_method
Execute a read-only get-method on any contract. No gas, no state change.
When to use. Reading typed on-chain data a dedicated tool does not cover: seqno, get_sale_data, get_collection_data, get_nft_data, and anything custom a contract exposes.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
address |
string | yes | — | Contract address. Must be active — check with get_account_state if unsure |
method |
string | yes | — | Method name, e.g. "seqno" or "get_jetton_data" |
args |
string[] | no | [] |
Integer arguments as decimal strings, e.g. ["0"] |
Only integer arguments are supported. Methods that take an address or slice argument have dedicated tools — use get_jetton_balance instead of calling get_wallet_address by hand.
Example call
await call("run_get_method", {
address: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
method: "get_jetton_data"
});Example response
{
"address": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
"method": "get_jetton_data",
"exit_code": 0,
"stack": [
{ "type": "int", "value": "1429976002510000" },
{ "type": "int", "value": "-1" },
{ "type": "slice", "boc_base64": "te6cckEBAQEAJAAAQ4AMiB/HjSggcHLHKKLniWIo834XNprhIcsO73tLA4XzMxC89RGs" },
{ "type": "cell", "boc_base64": "te6cckEBBwEAfQABAwDAAQIBIAIFAUO/+HLr21FNnJfCg7fwrlF5Ap4rYRnDlGJxnk9G7Y90E+ZAAwECAAQAPmh0dHBzOi8vdGV0aGVyLnRvL3VzZHQtdG9uLmpzb24BQ7/3QH6XjwGkBxFBGxrLdzqWvdk/qDu1yoQ1ATyMSzrJH0AGAAQANmxT2v0=" },
{ "type": "cell", "boc_base64": "te6cckEBAQEAIwAIQgKPRS16Tf10BmtoI2UXclntBXNENb52tf1L1divK3w9aCBrv3Y=" }
]
}exit_code. 0 and 1 both mean success (TVM uses either for a normal return). 11 almost always means the contract has no such method. Anything else is a contract-specific error.
Stack item shapes.
type |
Extra fields |
|---|---|
int |
value — decimal string |
cell, slice, builder |
boc_base64 — the cell serialized as a base64 BoC |
tuple |
items — array of stack items, recursively |
null, nan |
none |
Decode a returned cell client-side with @ton/core:
// npm install @ton/core
import { Cell } from "@ton/core";
const res = await call("run_get_method", {
address: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
method: "get_jetton_data"
});
// item 2 is the admin address slice
const admin = Cell.fromBase64(res.stack[2].boc_base64).beginParse().loadAddress();
console.log(admin.toString()); // EQBkQP48aUEDg5Y5RRc8SxFHm_C5tNcJDlh3e9pYHC-ZmG2MJettons
get_jetton_balance
Token balance of an owner — USDT and every other TEP-74 jetton.
How it works. Calls get_wallet_address(owner) on the jetton master to derive the owner's jetton-wallet contract, then reads get_wallet_data on it. Two on-chain reads, one tool call.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
owner |
string | yes | — | Holder's TON address, friendly or raw |
jetton_master |
string | yes | — | The token's master contract address |
Example call
await call("get_jetton_balance", {
owner: "-1:3333333333333333333333333333333333333333333333333333333333333333",
jetton_master: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"
});Example response
{
"owner": "Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF",
"jetton_master": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
"jetton_wallet": "EQBkvsMdV2-HqsiLwaaJi-VkNVC69z2j7hYZ6thdkFMVXU_T",
"balance": "0",
"deployed": false,
"at_seqno": 82174515
}| Field | Type | Meaning |
|---|---|---|
jetton_wallet |
string | The derived per-owner jetton wallet address |
balance |
string | Raw indivisible units — divide by 10^decimals |
deployed |
boolean | false means the owner never held this token, so the balance is genuinely 0 |
at_seqno |
number | Anchor block |
Converting to a human amount. balance is raw. USDT uses 6 decimals, most other jettons 9 — read the real figure with get_jetton_info:
const info = await call("get_jetton_info", { jetton_master: USDT });
const bal = await call("get_jetton_balance", { owner: WALLET, jetton_master: USDT });
const human = Number(bal.balance) / 10 ** (info.decimals ?? 9);If jetton_master is not actually a jetton master, the tool fails loudly: get_wallet_address failed with exit code … — is EQ… really a jetton master?
get_jetton_info
Metadata of a jetton master: name, symbol, decimals, supply, mintable flag, admin.
When to use. Before any amount conversion. Swap quotes and balances are in raw units; you need decimals to turn them into numbers a human reads. Call it before get_swap_quote whenever you do not already know a token's decimals.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
jetton_master |
string | yes | — | Token master contract address |
Example call
await call("get_jetton_info", {
jetton_master: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"
});Example response (mainnet USDT — note the semi-chained metadata)
{
"jetton_master": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
"name": null,
"symbol": null,
"decimals": 6,
"description": null,
"total_supply": "1429976002510000",
"mintable": true,
"admin": "EQBkQP48aUEDg5Y5RRc8SxFHm_C5tNcJDlh3e9pYHC-ZmG2M",
"metadata_type": "onchain+uri",
"metadata_uri": "https://tether.to/usdt-ton.json",
"at_seqno": 82174515
}| Field | Type | Meaning |
|---|---|---|
name, symbol, description |
string | null | On-chain (TEP-64) attributes; null when the token keeps them off-chain |
decimals |
number | null | Decimal places; null when stored off-chain |
total_supply |
string | Raw units |
mintable |
boolean | Whether the master can still mint |
admin |
string | null | Admin address; null for a renounced/addr_none admin |
metadata_type |
string | onchain, offchain, onchain+uri, or unknown |
metadata_uri |
string | null | Where off-chain metadata lives — the tool does not fetch it |
at_seqno |
number | Anchor block |
Off-chain metadata. When metadata_type is offchain, name/symbol/decimals live in a JSON file at metadata_uri and come back null here. onchain+uri (the USDT case above) means the numeric fields are on-chain but the display fields are in that JSON. Fetch the URI yourself if you need the name.
Utilities
parse_address
Parse, validate and convert a TON address between all its forms. Purely local — no network access, no rate-limit cost beyond the request itself.
When to use. Normalizing user input, comparing two addresses that look different, or converting to the raw form that indexers expect.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
address |
string | yes | — | Any form: friendly (EQ…/UQ…, URL-safe or not) or raw (0:…/-1:…) |
Example call
await call("parse_address", {
address: "-1:3333333333333333333333333333333333333333333333333333333333333333"
});Example response
{
"raw": "-1:3333333333333333333333333333333333333333333333333333333333333333",
"friendly_bounceable": "Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF",
"friendly_non_bounceable": "Uf8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMxYA",
"workchain": -1,
"input_format": "raw",
"input_flags": null
}| Field | Type | Meaning |
|---|---|---|
raw |
string | workchain:hex form |
friendly_bounceable |
string | EQ… (basechain) / Ef… (masterchain) |
friendly_non_bounceable |
string | UQ… / Uf… |
workchain |
number | 0 = basechain, -1 = masterchain |
input_format |
string | friendly or raw — what you passed in |
input_flags |
object | null | { bounceable, test_only } of the input; null when the input was raw |
Why it matters. EQ… and UQ… encode the same account. Bounceable (EQ…) is conventional for contracts; non-bounceable (UQ…) for user wallets. Two addresses are the same account if and only if their raw forms match — never compare friendly strings.
Swaps on TON
get_swap_quote and build_swap_tx are backed by Omniston, STON.fi's RFQ protocol aggregating STON.fi and DeDust liquidity. Mainnet only.
The flow is two calls and is strictly non-custodial:
get_swap_quote→ a firm quote with an id.build_swap_tx→ unsigned TonConnect messages. You sign and send them.
Quotes expire in roughly a minute. Build promptly or re-quote.
get_swap_quote
A firm quote to exchange GRAM or any jetton for another asset on TON.
When to use. Answering "what would I get for X right now", or as step 1 of an actual swap. For a swap into another blockchain use get_crosschain_quote instead.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
from_asset |
string | yes | — | "GRAM" for the native coin, or a jetton master address |
to_asset |
string | yes | — | Same encoding. Must differ from from_asset |
amount_units |
string | yes | — | Raw indivisible units of the exact side, e.g. "1000000000" = 1 GRAM |
exact |
"input" | "output" |
no | "input" |
input = spend exactly this much; output = target receiving this much after fees |
slippage_bps |
integer | no | 500 |
Max price slippage in basis points, 1–500 (500 = 5%) |
"TON", "toncoin" and "native" are accepted as aliases for "GRAM".
Example call
const quote = await call("get_swap_quote", {
from_asset: "GRAM",
to_asset: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", // USDT
amount_units: "5000000000", // 5 GRAM
slippage_bps: 100 // 1%
});Example response
{
"quote_id": "9c010713aeaa68dac2bf8577b6b32560f7f214defb71bc69702efd12305b0f86",
"resolver": "Omniston",
"input_asset": "GRAM",
"output_asset": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
"input_units": "5000000000",
"output_units": "7573330",
"min_output_units": "7497597",
"recommended_min_output_units": "7497597",
"recommended_slippage_bps": 100,
"price_impact_bps": 0,
"protocol_fee_units": "0",
"integrator_fee_units": "0",
"gas_budget_nano": "100000000",
"estimated_settlement_seconds": 1,
"route": [
{
"from": "GRAM",
"to": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
"protocols": ["DeDust"]
}
]
}| Field | Type | Meaning |
|---|---|---|
quote_id |
string | 64-char hex. Pass to build_swap_tx promptly |
resolver |
string | Which resolver produced the quote |
input_units / output_units |
string | Raw units in and expected out |
min_output_units |
string | The on-chain floor the swap will be built with — the only guaranteed minimum |
recommended_min_output_units |
string | Omniston's own suggested floor (can be looser than yours) |
recommended_slippage_bps |
number | The slippage that floor corresponds to |
price_impact_bps |
number | null | Price impact in basis points |
protocol_fee_units |
string | Protocol fee, in output units |
integrator_fee_units |
string | Operator revenue share, already deducted from output_units ("0" when not configured) |
gas_budget_nano |
string | null | Extra GRAM the wallet must hold for gas, in nano |
estimated_settlement_seconds |
number | null | Expected settlement time |
route |
array | Hops: { from, to, protocols[] } — e.g. StonFiV2, DeDust |
Reading it. output_units is an expectation; min_output_units is a promise. If the pool moves past the floor, the swap refunds instead of executing. Tightening slippage_bps narrows the floor — the 5% cap is deliberate, anything looser only feeds MEV. With exact: "output" the floor is still min_output_units, so raise amount_units or tighten slippage when an exact minimum must clear.
build_swap_tx
Turn a quote into the unsigned transaction for the wallet owner to sign.
This output moves real funds once signed. Treat it as an armed payment: show it to the wallet owner, never auto-approve it.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
quote_id |
string | yes | — | 64-char hex id from get_swap_quote |
wallet |
string | yes | — | Address that will sign and send; also receives the output and any gas excess |
use_recommended_slippage |
boolean | no | false |
Build with recommended_min_output_units instead of the floor from your quote request — it can be looser than what you asked for |
Example call
const tx = await call("build_swap_tx", {
quote_id: quote.quote_id,
wallet: "UQ…your wallet…"
});Response shape (payload BoCs elided — they are long base64 strings unique to each build)
{
"quote_id": "9c010713aeaa68dac2bf8577b6b32560f7f214defb71bc69702efd12305b0f86",
"wallet": "UQ…",
"message_count": 1,
"total_attached_nano": "…",
"tonconnect": {
"validUntil": 1785083344,
"network": "-239",
"messages": [
{ "address": "EQ…", "amount": "…", "payload": "te6cck…" }
]
}
}| Field | Type | Meaning |
|---|---|---|
message_count |
number | How many messages the swap needs |
total_attached_nano |
string | Sum of all messages[].amount — the total GRAM leaving the wallet |
tonconnect.validUntil |
number | Unix seconds; the payload is built with a 5-minute deadline |
tonconnect.network |
string | "-239" = TON mainnet, so a compliant wallet refuses to sign on the wrong network |
tonconnect.messages[].address |
string | Destination, friendly form |
tonconnect.messages[].amount |
string | Nanocoins attached to this message |
tonconnect.messages[].payload |
string | Message body, base64 BoC |
tonconnect.messages[].stateInit |
string? | Present only when a jetton wallet must be deployed as part of the swap |
tonconnect is exactly the shape TonConnect expects — hand it straight over:
// npm install @tonconnect/ui-react
await tonConnectUI.sendTransaction(tx.tonconnect);Funding requirements. The wallet must hold the input amount plus gas_budget_nano in GRAM. For a GRAM-input swap the input is included in the attached value. Omniston emulates the transfer while building, so an underfunded wallet fails before anything is signed:
{"error":"Emulation error: wrong output amount. Expected: 7497597, actual: 0 — {\"error_info\":{\"reason\":\"QUOTE_VALIDATION_FAILED\",\"domain\":\"omni.ston.fi\",\"metadata\":{\"expected\":\"7497597\",\"actual\":\"0\",\"type\":\"EMULATION_WRONG_OUTPUT_AMOUNT\"}}}"}An expired quote also fails here — re-quote and rebuild.
Cross-chain swaps: an ordered flow
The five *_crosschain_* tools are not independent calls. They are one atomic-swap lifecycle, and calling them out of order loses money. TON is always the source chain: the trade is funded and signed by a TON wallet. Destinations: Ethereum, Arbitrum, Base, BNB, Polygon, Avalanche.
flowchart TD
A["1. get_crosschain_quote<br/>firm price + htlc_hashing_function"] --> B["2. build_crosschain_swap_tx<br/>unsigned escrow tx + htlc_secret"]
B --> C["3. wallet owner signs and sends<br/>escrow position exists on TON"]
C --> D["4. track_crosschain_swap<br/>poll every ~10s"]
D -->|"dst_phase = ready_for_private_completion"| E["5. disclose_crosschain_secret<br/>both sides settle"]
D -->|"cancellation_mode = onchain"| F["fallback: build_crosschain_refund<br/>reclaim escrow"]
E --> G["status = fully_filled"]Three rules that decide whether this ends well:
- Keep
htlc_secretuntil the trade completes. The server does not store it — it exists only in thebuild_crosschain_swap_txresponse. Without it the trade cannot settle (funds stay refundable after the timeout, but the swap is lost). - Every build generates a NEW secret bound to that specific payload. If you rebuild for the same quote, discard every earlier payload and secret. Signing an older payload after a rebuild locks funds under a hash whose secret you are no longer tracking.
- Disclose promptly, never late. Once revealed, the preimage is public and the resolver can claim the TON side. Revealing close to the destination's rollback window risks losing the input without receiving the output — that is the classic late-reveal loss. The tool refuses inside a 60-second safety margin and tells you to refund instead.
The server never signs, never holds funds, and cannot redirect anything: a disclosed secret can only complete the trade as quoted, and an unfilled escrow is always reclaimable by the owner wallet.
get_crosschain_quote
Step 1. Firm quote for paying on TON and receiving on an EVM chain, settled through Omniston's atomic HTLC escrow. Mainnet only.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
from_asset |
string | yes | — | Asset to pay on TON: "GRAM" or a jetton master address |
to_chain |
enum | yes | — | ethereum, arbitrum, base, bnb, polygon, avalanche |
to_asset |
string | yes | — | "native" (ETH/BNB/POL/AVAX) or a 0x… ERC-20 contract address on to_chain |
amount_units |
string | yes | — | Raw indivisible units of the exact side |
exact |
"input" | "output" |
no | "input" |
input = spend exactly this much of from_asset; output = target receiving this much of to_asset |
An ERC-20 address in mixed case must carry a correct EIP-55 checksum — that is the typo guard. All-lowercase or all-uppercase input is normalized for you.
Example call
const xq = await call("get_crosschain_quote", {
from_asset: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", // USDT on TON
to_chain: "ethereum",
to_asset: "0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT on Ethereum
amount_units: "100000000" // 100 USDT (6 decimals)
});Example response
{
"quote_id": "db858eb36b19b0c256d7d8929763abe29c97075693d33616972835595ac81895",
"resolver": "Arbiter",
"input_asset": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
"output_asset": "ethereum:0xdAC17F958D2ee523a2206206994597C13D831ec7",
"input_units": "100000000",
"output_units": "99914503",
"protocol_fee_units": "9992",
"integrator_fee_units": "0",
"gas_budget_nano": "400000000",
"security_deposit_asset": "EQBnGWMCf3-FZZq1W4IWcWiGAc3PHuZ0_H-7sad2oY00o83S",
"security_deposit_units": "500000000",
"htlc_hashing_function": "keccak256",
"estimated_settlement_seconds": 39
}| Field | Type | Meaning |
|---|---|---|
quote_id |
string | 64-char hex; expires in about a minute |
output_asset |
string | Destination asset rendered as chain:address (or chain:native) |
protocol_fee_units |
string | Protocol fee in output units |
integrator_fee_units |
string | Operator revenue share ("0" when not configured) |
gas_budget_nano |
string | null | GRAM needed for gas on the TON side |
security_deposit_asset / security_deposit_units |
string | null | Extra value temporarily locked in the escrow, returned on completion |
htlc_hashing_function |
string | null | keccak256 or sha256 — copy verbatim into the next call |
estimated_settlement_seconds |
number | null | Expected end-to-end settlement time |
Note what is absent compared with an on-TON quote: there is no slippage_bps argument and no min_output_units. Cross-chain settlement is an escrowed order at the quoted terms, not an AMM route.
If htlc_hashing_function comes back null, stop — do not build.
Thin pairs simply do not get quoted:
{"error":"no resolver quoted this pair/amount — pool may lack liquidity, or the amount is too small to cover gas"}build_crosschain_swap_tx
Step 2. Build the unsigned TON escrow transfer and generate the HTLC secret that later completes it.
The response contains both an armed payment and a payment authorization. Anyone who sees
htlc_secrettogether withquote_idcan trigger disclosure. Keep this response out of logs, transcripts and shared context.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
quote_id |
string | yes | — | 64-char hex id from get_crosschain_quote |
wallet |
string | yes | — | TON address that signs the escrow, owns the position and receives refunds/gas excess |
to_chain |
enum | yes | — | Destination chain — must match the quote |
destination_address |
string | yes | — | 0x… wallet on the destination chain that receives the output |
hashing_function |
"keccak256" | "sha256" |
yes | — | Copy htlc_hashing_function from the quote verbatim. A mismatch builds an escrow that can never settle |
Example call
const build = await call("build_crosschain_swap_tx", {
quote_id: xq.quote_id,
wallet: "UQ…your TON wallet…",
to_chain: "ethereum",
destination_address: "0x…your EVM wallet…",
hashing_function: xq.htlc_hashing_function
});
// Persist this before signing anything.
saveSecret(build.quote_id, build.htlc_secret);
await tonConnectUI.sendTransaction(build.tonconnect);Response shape (secret and BoCs elided)
{
"quote_id": "db858eb36b19b0c256d7d8929763abe29c97075693d33616972835595ac81895",
"wallet": "UQ…",
"destination_address": "0x…",
"htlc_secret": "<64-char hex — 32 random bytes>",
"htlc_secret_hash": "<64-char hex — hash of the secret>",
"message_count": 1,
"total_attached_nano": "…",
"tonconnect": {
"validUntil": 1785083344,
"network": "-239",
"messages": [
{ "address": "EQ…", "amount": "…", "payload": "te6cck…" }
]
}
}| Field | Type | Meaning |
|---|---|---|
destination_address |
string | Your EVM address, returned in EIP-55 checksummed form |
htlc_secret |
string | The preimage. Not stored server-side. Store it yourself |
htlc_secret_hash |
string | hashing_function(secret) — what gets locked on-chain |
tonconnect |
object | Same shape as build_swap_tx; sign and send it |
track_crosschain_swap
Step 3. A snapshot of the escrow order: lifecycle phases and HTLC time windows on both chains. Poll it about every 10 seconds after sending the escrow transaction.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
quote_id |
string | yes | — | The quote the escrow was built from |
wallet |
string | yes | — | The TON wallet that sent the escrow transaction |
Example call
const st = await call("track_crosschain_swap", {
quote_id: xq.quote_id,
wallet: "UQ…your TON wallet…"
});Response when the escrow has not landed yet (exact, not illustrative)
{
"found": false,
"status": null,
"remaining_input_units": null,
"escrowed_input_units": null,
"cancellation_mode": null,
"estimated_finish_timestamp": null,
"maximum_finish_timestamp": null,
"executions": []
}Fields when found is true
| Field | Type | Meaning |
|---|---|---|
status |
string | Trade status, lowercased without the TRADE_STATUS_ prefix — fully_filled once settled |
remaining_input_units |
string | Input not yet filled |
escrowed_input_units |
string | Input currently locked in escrow |
cancellation_mode |
string | Lowercased without the ORDER_CANCELLATION_MODE_ prefix. onchain means the trader can reclaim funds with build_crosschain_refund |
estimated_finish_timestamp |
number | null | Unix seconds |
maximum_finish_timestamp |
number | null | Unix seconds |
executions[] |
array | One entry per fill — a large order can be filled by several resolvers |
Each execution
| Field | Type | Meaning |
|---|---|---|
index |
number | Execution index — pass it to disclose_crosschain_secret |
resolver |
string | Who is filling this slice |
input_units / output_units |
string | Raw units for this slice |
src_phase |
string | null | TON-side phase, lowercased without the EXECUTION_PHASE_ prefix |
dst_phase |
string | null | Destination-side phase, same normalization |
input_position / output_position |
string | null | Position addresses as chain:address |
input_position_timestamps |
object | null | { private_completion_at, public_completion_at, private_rollback_at, public_rollback_at }, unix seconds |
output_position_timestamps |
object | null | Same four fields for the destination position |
The decision this tool exists to support. When an execution's dst_phase reaches ready_for_private_completion (or ready_for_public_completion), the resolver has locked the output on the destination chain — disclose promptly. Do not disclose when now is anywhere near that execution's output_position_timestamps.private_rollback_at: past it the resolver can roll the destination back and still claim your TON with the now-public preimage. Refund instead.
A poll loop:
async function waitForReady(quoteId, wallet, timeoutMs = 10 * 60_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const st = await call("track_crosschain_swap", { quote_id: quoteId, wallet });
const ready = st.executions.find((e) =>
e.dst_phase === "ready_for_private_completion" ||
e.dst_phase === "ready_for_public_completion"
);
if (ready) return ready;
if (st.cancellation_mode === "onchain") return null; // refund path
await new Promise((r) => setTimeout(r, 10_000));
}
return null;
}found: false means the escrow transaction has not landed yet (or was never sent). A quote_id the backend has never seen errors instead:
{"error":"Object not found: quote with id: 0000000000000000000000000000000000000000000000000000000000000000 — {\"error_info\":{\"reason\":\"NOT_FOUND\",\"domain\":\"omni.ston.fi\",\"metadata\":{\"object_id\":\"…\",\"object_type\":\"quote\"}}}"}This returns a snapshot, not a stream. Poll again for updates.
disclose_crosschain_secret
Step 4 — the final, trade-committing call. Revealing the preimage lets the resolver claim the TON side. It cannot be taken back.
This is the only tool annotated destructive. Never auto-approve it.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
quote_id |
string | yes | — | The quote the escrow was built from |
wallet |
string | yes | — | The TON wallet that created the escrow position (used to locate and verify the order) |
secret |
string | yes | — | The 64-char hex htlc_secret from build_crosschain_swap_tx |
execution_index |
integer | no | 0 |
Execution index from track_crosschain_swap; 0 for single-fill trades |
Guards run before anything is revealed. Prose warnings do not protect funds, so the tool re-verifies live state and refuses unless all of these hold:
- The order exists on-chain (it probes for up to 6 seconds).
- The chosen execution's destination position is at
ready_for_private_completionorready_for_public_completion. - The secret matches that execution's on-chain hash (checked against both keccak256 and sha256).
now + 60sis still before the destination position'sprivate_rollback_at.
Each failure returns an explicit error ending in secret NOT disclosed:
destination position is not ready (dst_phase=…) — disclosing now would let the resolver claim your TON without delivering; keep polling track_crosschain_swap; secret NOT disclosed
secret does not match this execution's on-chain hash — wrong secret or wrong quote_id pair; secret NOT disclosed
too close to the destination rollback window — a late reveal risks losing the input; do NOT disclose, reclaim the escrow with build_crosschain_refund instead; secret NOT disclosed
order not found — the escrow transaction has not landed (or quote_id/wallet is wrong); secret NOT disclosedExample call
const ready = await waitForReady(xq.quote_id, WALLET);
if (ready) {
await call("disclose_crosschain_secret", {
quote_id: xq.quote_id,
wallet: WALLET,
secret: loadSecret(xq.quote_id),
execution_index: ready.index
});
}Response
{
"disclosed": true,
"quote_id": "db858eb36b19b0c256d7d8929763abe29c97075693d33616972835595ac81895",
"execution_index": 0,
"verified_dst_phase": "ready_for_private_completion"
}| Field | Type | Meaning |
|---|---|---|
disclosed |
boolean | true only when the reveal actually went through |
verified_dst_phase |
string | The destination phase the tool confirmed before revealing |
The secret can only complete the trade as quoted. It cannot redirect funds anywhere else.
build_crosschain_refund
Fallback. Build the unsigned transaction that cancels the escrow position and returns the funds to the owner wallet.
When to use. The trade did not settle and track_crosschain_swap reports cancellation_mode: "onchain". Before that point the position cannot be cancelled by the trader — building earlier will fail.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
quote_id |
string | yes | — | The quote the escrow was built from |
wallet |
string | yes | — | The TON wallet that created the escrow position — only it can cancel |
Example call
const refund = await call("build_crosschain_refund", {
quote_id: xq.quote_id,
wallet: "UQ…your TON wallet…"
});
await tonConnectUI.sendTransaction(refund.tonconnect);Response shape
{
"quote_id": "db858eb36b19b0c256d7d8929763abe29c97075693d33616972835595ac81895",
"wallet": "UQ…",
"message_count": 1,
"total_attached_nano": "…",
"tonconnect": {
"validUntil": 1785083344,
"network": "-239",
"messages": [
{ "address": "EQ…", "amount": "…", "payload": "te6cck…" }
]
}
}After the wallet signs and sends it, the escrowed input and the security deposit come back to the owner.
Wallet
generate_wallet
Create a brand-new TON wallet: a fresh 24-word mnemonic, its ed25519 keypair, and the address for the chosen contract version.
This tool returns secrets
The response contains a mnemonic and a private key. Anyone who sees it controls the wallet and everything in it.
- The server never stores or logs the mnemonic or the private key. The only thing written to the server log is the public address.
- In hosted mode the keys are generated on the server and travel back over TLS, so treat every generated wallet as hot: fine for programmatic or ephemeral use, but move any meaningful balance to a hardware or cold wallet.
- Keep the response out of logs, transcripts and shared agent context.
- Losing the mnemonic means losing the funds. There is no recovery.
- Operators who do not want key material flowing through a shared endpoint can unregister the tool entirely with
TONNODE_DISABLE_WALLET_GEN=1. On such a server it will not appear intools/list.
When to use. An agent needs its own wallet to receive or send funds — for example before build_swap_tx.
Each call creates a new, independent wallet. Never re-call the tool to "re-read" a wallet you already made: you will get a different one and orphan any funds sent to the first.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
version |
"v4" | "v3r2" | "v5r1" | "highload_v3" |
no | "v4" |
Wallet contract version |
workchain |
integer | no | 0 |
0 = basechain (normal wallets), -1 = masterchain (rarely what you want) |
subwallet_id |
integer | no | 4269 (0x10ad) |
highload_v3 only. Baked into the address |
timeout_sec |
integer | no | 86400 |
highload_v3 only. Message validity window, baked into the address. Max 4194303 |
Versions.
| Version | Use |
|---|---|
v4 |
The common default — recommended |
v3r2 |
Simple/legacy |
v5r1 |
W5, newest, gasless-capable |
highload_v3 |
Mass payouts for exchanges and payment systems |
The v3r2/v4/v5r1 addresses come from @ton/ton's canonical contracts. highload_v3 is derived from the official contract code plus its data layout.
Example call
const w = await call("generate_wallet", { version: "v4" });
// Never console.log(w) — it contains the mnemonic and private key.
console.log("fund this address:", w.recommended_deposit_address);Response shape (secret fields shown as placeholders — never print real ones)
{
"version": "v4",
"workchain": 0,
"recommended_deposit_address": "UQ…",
"address": {
"bounceable": "EQ…",
"non_bounceable": "UQ…",
"raw": "0:…"
},
"public_key": "<64-char hex>",
"private_key": "<128-char hex>",
"mnemonic": ["word1", "word2", "…24 words total…"],
"warning": "SECRET — anyone with this mnemonic or private key controls the wallet. …"
}For highload_v3 the response additionally carries subwallet_id and timeout_sec.
| Field | Type | Meaning |
|---|---|---|
recommended_deposit_address |
string | The non-bounceable (UQ…) form — use this for incoming funds |
address.bounceable / .non_bounceable / .raw |
string | The same account in all three encodings |
public_key |
string | ed25519 public key, hex (64 chars) |
private_key |
string | ed25519 secret key, hex (128 chars) — secret |
mnemonic |
string[] | 24 words — secret |
subwallet_id, timeout_sec |
number | Present for highload_v3 only |
warning |
string | The safety text, restated in-band for agents that read only the payload |
Two things that cost people money.
- Fund the non-bounceable address. A fresh wallet is uninitialized until its first outgoing transaction deploys it. Receiving does not require deployment — but a bounceable (
EQ…) transfer to an undeployed wallet bounces straight back to the sender. Send the first deposit torecommended_deposit_address. - For
highload_v3, storesubwallet_idandtimeout_secalongside the mnemonic. Unlike seqno wallets, the highload address depends on both — the mnemonic alone cannot reproduce an address built with non-default parameters.
Errors
Tool-level failures come back as an MCP result with isError: true and a single text block:
{"error": "\"UQBOmxbUlIYOTJ4I8UODLUHqmIVYZm5KU8NU1I_kh0OyFF9r\" is not a valid TON address (expected friendly EQ…/UQ… or raw 0:… form)"}Messages are written to be actionable by an agent without a human in the loop.
| Message (excerpt) | Cause | Fix |
|---|---|---|
"…" is not a valid TON address |
Malformed or mistyped address (a hand-edited friendly address fails its checksum) | Re-copy it, or normalize with parse_address |
liteserver query timed out after 15000 ms |
The node did not answer in time | Retry |
lt not in db / cannot find block … point at an archive node |
History deeper than the answering node keeps | Query through an archive-capable endpoint |
no resolver quoted this pair/amount |
Thin liquidity, or an amount too small to cover gas | Increase the amount, or pick a different pair |
quote_id must be the 64-char hex id returned by … |
Truncated or wrong id | Re-quote |
Emulation error: wrong output amount … |
The wallet does not hold the input plus gas, or the quote expired | Fund the wallet, then re-quote and rebuild |
Object not found: quote with id: … |
The backend has never seen that quote (typo, or it aged out) | Re-quote |
swap service is busy (too many concurrent quote/build calls) |
Too many simultaneous swap calls on the server process | Retry in a few seconds |
swap tools are mainnet-only |
The server was started against testnet | Run without --testnet / TON_NETWORK=testnet |
… secret NOT disclosed |
A disclose_crosschain_secret guard tripped |
Read the specific guard above — the secret is still safe |
Transport-level errors (hosted mode) arrive as HTTP status codes with a JSON body:
| Status | Body | Meaning |
|---|---|---|
400 |
no session — first request must be initialize |
You skipped the handshake |
400 |
batch requests not supported |
Send one JSON-RPC message per request |
401 |
invalid, missing or expired API key |
Bad Authorization header, or the key expired |
404 |
unknown or expired session |
The mcp-session-id is stale — re-initialize |
413 |
body too large (max 1 MB) |
Request body over the limit |
429 |
rate limit exceeded (N/min for this key) |
Your plan's rpm ceiling — back off, or move up a plan |
429 |
too many concurrent sessions for this key |
Close idle sessions |
503 |
session limit reached, retry later |
Server-wide session cap |
Idle sessions are swept after 30 minutes. Revocation is not instant: the server re-reads its key registry on a 5-second file poll, and a revoked key's live sessions are dropped on that next reload.
