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:

JSON
{
  "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:

JSON
{
  "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:

JSON
{
  "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:

TOML
[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.

JavaScript
// 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:

Bash
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

JavaScript
await call("get_masterchain_info");

Example response

JSON
{
  "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

JavaScript
await call("get_balance", {
  address: "-1:3333333333333333333333333333333333333333333333333333333333333333"
});

Example response

JSON
{
  "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

JavaScript
await call("get_account_state", {
  address: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs" // USDT master
});

Example response

JSON
{
  "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, 130

Example call

JavaScript
await call("get_transactions", {
  address: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
  limit: 2
});

Example response

JSON
{
  "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 point TON_LITESERVERS at 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

JavaScript
await call("run_get_method", {
  address: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs",
  method: "get_jetton_data"
});

Example response

JSON
{
  "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:

JavaScript
// 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-ZmG2M

Jettons

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

JavaScript
await call("get_jetton_balance", {
  owner: "-1:3333333333333333333333333333333333333333333333333333333333333333",
  jetton_master: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"
});

Example response

JSON
{
  "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:

JavaScript
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

JavaScript
await call("get_jetton_info", {
  jetton_master: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs"
});

Example response (mainnet USDT — note the semi-chained metadata)

JSON
{
  "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

JavaScript
await call("parse_address", {
  address: "-1:3333333333333333333333333333333333333333333333333333333333333333"
});

Example response

JSON
{
  "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:

  1. get_swap_quote → a firm quote with an id.
  2. 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, 1500 (500 = 5%)

"TON", "toncoin" and "native" are accepted as aliases for "GRAM".

Example call

JavaScript
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

JSON
{
  "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

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

JSON
{
  "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:

JavaScript
// 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:

JSON
{"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.

Diagram
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:

  1. Keep htlc_secret until the trade completes. The server does not store it — it exists only in the build_crosschain_swap_tx response. Without it the trade cannot settle (funds stay refundable after the timeout, but the swap is lost).
  2. 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.
  3. 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

JavaScript
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

JSON
{
  "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 sha256copy 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:

JSON
{"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_secret together with quote_id can 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

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

JSON
{
  "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

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

JSON
{
  "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:

JavaScript
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:

JSON
{"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:

  1. The order exists on-chain (it probes for up to 6 seconds).
  2. The chosen execution's destination position is at ready_for_private_completion or ready_for_public_completion.
  3. The secret matches that execution's on-chain hash (checked against both keccak256 and sha256).
  4. now + 60s is still before the destination position's private_rollback_at.

Each failure returns an explicit error ending in secret NOT disclosed:

Text
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 disclosed

Example call

JavaScript
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

JSON
{
  "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

JavaScript
const refund = await call("build_crosschain_refund", {
  quote_id: xq.quote_id,
  wallet: "UQ…your TON wallet…"
});
await tonConnectUI.sendTransaction(refund.tonconnect);

Response shape

JSON
{
  "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 in tools/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

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

JSON
{
  "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.

  1. 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 to recommended_deposit_address.
  2. For highload_v3, store subwallet_id and timeout_sec alongside 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:

JSON
{"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 blockpoint 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.