How to Generate a TON Wallet Programmatically (v4, v5, highload)
Generate a TON wallet programmatically with one generate_wallet call: v3r2, v4, v5r1, highload. Mnemonic, keys, address, and a safe first deposit.
You're rolling out 500 promo codes and every payout needs its own TON wallet. Or you're building an AI agent that spins up a hot wallet on the fly, takes a deposit, and does something with it. The first thing you find online is a pile of SDK examples: pull in @ton/ton, figure out how WalletContractV4 differs from WalletContractV5R1, call mnemonicNew() by hand, derive the keys, compute the address, don't forget the subwallet_id on highload. Half a day gone on plumbing code that has nothing to do with your actual task. And if the wallets are created by an AI agent, it has to do all of this itself — without you hardcoding cryptography into it.
Here's how to create a TON wallet programmatically with a single call to the generate_wallet tool, make sense of the v3r2, v4, v5r1, and highload_v3 versions, and avoid losing your first deposit to the classic bounceable-address mistake.
Why generate a TON wallet programmatically (and why not via an SDK)
Doing it by hand through an SDK is fine right up until your first production deploy. At scale, the problems surface:
- Versions. TON doesn't have one "wallet" — it has several contracts: v3r2, v4, v5r1, highload. Each has its own logic and produces a different address from the same key. Pick the wrong version and you get the wrong address.
- Address formats. The same wallet has an EQ form and a UQ form. Mix them up when handing out a deposit address and the money "bounces" right back (more on that below).
- AI agents. You can't "hand a library" to an agent (Claude, Cursor, ChatGPT/Codex) — it needs a tool it can call by description. Over MCP (Model Context Protocol), the agent calls
generate_walletlike a regular function and gets a structured response back. All the version, key, and format logic lives on the MCP server side. - Polyglot stacks. You don't want to maintain a TON SDK in Go, Python, and Node at the same time — MCP gives you one interface for all of them.
generate_wallet is one of the 16 tools in TONNode, a hosted MCP server for TON. It does exactly one thing: creates a new wallet and hands you everything you need to own it. If you're new to the whole approach, start with the MCP for TON guide.
generate_wallet in a single call: what exactly you get back
generate_wallet creates a new TON wallet and returns the full ownership-and-recovery kit:
- the mnemonic (seed phrase) — a human-readable set of words from which everything else is deterministically derived;
- the private key — for signing transactions;
- the public key;
- the wallet address — in the standard TON formats.
The prompt to your agent is literally this:
Create a new v5r1 TON wallet with generate_wallet
and show me the address and the mnemonic.
The agent calls the tool with the version parameter and returns a structure that looks roughly like this (values are illustrative; Ed25519 keys in TON are plain hex, no Ethereum-style 0x prefix):
{
"version": "v5r1",
"mnemonic": ["word1", "word2", "...", "word24"],
"public_key": "e3f1a2…",
"private_key": "9b7c4d…",
"address": {
"bounceable": "EQ…",
"non_bounceable": "UQ…",
"raw": "0:abcd…"
}
}
No SDK, no manual derivation. Where you store it from here is up to you. The key point: the server does not keep this mnemonic or these keys — more on that in the non-custodial section below.
Wallet versions v3r2, v4, v5r1, and highload_v3 — which one to pick
generate_wallet supports four wallet contract versions. This is not a "newer is better" situation: each has its own niche.
v5r1 (W5) — the default for new projects
The newest version. Supports extensions and gasless scenarios — where a third-party relayer can cover the fee on the user's behalf. That's a property of the v5 contract itself, not a TONNode service: the server merely creates the wallet and does not provide any gasless relayer. If you're building a modern agent from scratch and don't specifically need highload, go with v5r1.
v4 — the one with plugins
The previous mass-market generation. Supports plugins: you can attach extension contracts to the wallet (for subscriptions and deferred payments, for example). It was the de-facto standard for a long time and enjoys wide support across wallets and services. Take it if you depend on the v4 plugin model.
v3r2 — a simple, bare-bones wallet
A minimal contract with no extensions or plugins. Predictable and cheap on gas. A good fit for utility addresses where the entire logic is "receive and send".
highload_v3 — for mass payouts
A different class entirely. A wallet built for high throughput: a single external message can carry many transfers. This is what you take for batch payouts, drops, reward distributions, payment gateways, exchange withdrawals. It pays for that efficiency with a special message-accounting model (query_id / expiration), so it demands careful handling — see the section on storing its parameters.
| Version | When to pick it |
|---|---|
v5r1 |
New projects, extensions, gasless |
v4 |
You need plugins (subscriptions etc.), maximum compatibility |
v3r2 |
A simple utility wallet with no bells and whistles |
highload_v3 |
Mass/batch payouts, high throughput |
Example prompt for payouts:
Generate a highload_v3 wallet for batch payouts and return
the mnemonic, keys, and address.
The first deposit: why you send to the non-bounceable UQ address
The most common and most painful mistake after creating a wallet is losing the first deposit. Here's the mechanic: a freshly generated wallet is not deployed to the blockchain yet — there is physically no contract at that address until the first transfer arrives and deploys the code.
In TON, an address carries a "bounceable" flag:
- EQ (bounceable) — if there's no live contract at the address, the network bounces the transfer back to the sender. For a brand-new wallet, that's exactly your situation.
- UQ (non-bounceable) — the transfer "sticks" to the address even if the contract isn't deployed yet. Precisely what you want for the first deposit.
The rule is simple: send the first top-up for a new wallet to its UQ address. Sent to EQ, it comes back, and you'll be left wondering why the balance is empty. Once the wallet makes its first outgoing transaction and gets deployed, you can safely use the bounceable form.
How do you get the UQ form reliably? With the offline parse_address tool — it converts and validates the EQ/UQ/raw formats without touching the network:
Use parse_address to convert this address to its non-bounceable
UQ form for the first deposit
After topping up, check the state with get_account_state — it shows the account status (uninitialized / active), the flags, and the latest transaction:
Check get_account_state for UQ… — is the wallet deployed
and what's the balance
Until the contract goes active, the state will show you that the deposit hasn't "woken up" the wallet yet. For a deep dive into the formats, see the article on EQ/UQ/raw address formats.
Highload wallets: pin down subwallet_id and timeout yourself
A separate warning for anyone who picked highload_v3. The mnemonic alone is not enough here to correctly recreate a working wallet and reproduce its behavior. Beyond the key, a highload contract has two more parameters that are set at creation time and go into its initial data (state init):
subwallet_id— the subwallet identifier (lets you derive several distinct addresses from a single seed phrase);timeout— the lifetime window for external messages, which the wholequery_id/ expiration logic is built on.
Important: generate_wallet returns the mnemonic, keys, and address — but subwallet_id and timeout are values you choose and pin down as the highload wallet's construction parameters. It is on you to record the exact values the wallet was created with.
A highload wallet tracks which messages are still "alive" based on timeout. If you restore the wallet from the seed phrase alone, without subwallet_id and timeout, then:
- you get the wrong address — both
subwallet_idandtimeoutare part of the state init, so changing either one changes the contract's initial data and therefore its address; - you won't reproduce the correct expiration and message-deduplication logic — and you risk breaking the accounting of outgoing transfers.
Practical rule: for highload_v3, store subwallet_id and timeout right next to the mnemonic, as part of the same wallet record. These are not optional metadata — they are part of the wallet's identity.
# pseudo secret record
mnemonic: "word1 word2 … word24"
version: "highload_v3"
subwallet_id: <the value you set at creation>
timeout: <the value you set at creation>
For v3r2/v4/v5r1 there's no such requirement — the mnemonic and the version are enough.
Non-custodial: the server never stores or signs your keys
The big question whenever a wallet gets created "somewhere on a server": who ends up owning the keys? The answer here is unambiguous.
generate_wallet is strictly non-custodial:
- the server never stores private keys, mnemonics, or funds;
- the server never signs transactions on your behalf;
- the generated wallet — mnemonic, keys, address — is handed over to you in full in the response and is not persisted server-side.
There is no database of other people's seed phrases, no operator key signing things for you. In essence, generate_wallet is a convenient wrapper around deterministic generation: the cryptography happens, the result ships off to you, and nothing remains on the server. That's the fundamental difference from custodial agent wallets, where the service holds the key (or a piece of it) and signs transactions itself. We break down that difference separately: custodial vs non-custodial MCP.
The practical takeaway: save the generate_wallet response to your secure storage immediately. The server can't "fetch" the same wallet for you a second time — it doesn't remember it, and recovering it "through support" will be impossible. That's a feature, not a bug.
How to plug it in and create your first wallet
Setup takes a minute. Run the @tonnode/mcp package locally and for free via npx; the full read toolset works without a key.
MCP client config (Claude Desktop, Cursor, any MCP client):
{
"mcpServers": {
"ton": {
"command": "npx",
"args": ["-y", "@tonnode/mcp"]
}
}
}
The @tonnode/mcp package is open source (MIT), lives on npm and GitHub (tonnode/mcp), and speaks TON's native ADNL protocol — no HTTP middlemen between your agent and the network. Restart your client and all 16 tools, including generate_wallet, parse_address, and get_account_state, become available to the agent. For client-specific setup, see the guide on connecting Claude and Cursor to TON.
generate_wallet is part of all 16 tools and available on every plan, including the free Hobby tier — 60 requests per minute, no card required. You get a free key right after signing in. You only pay for throughput, never for access to tools.
From there, three tools cover the entire "create and safely fund" scenario:
generate_wallet— create a wallet of the right version, grab the mnemonic, keys, and address.parse_address— get the UQ form of the address for the first deposit (offline).get_account_state— confirm the deposit actually deployed the contract.
A full example prompt for the agent:
Create a v5r1 wallet with generate_wallet. Then use
parse_address to give me its UQ address for the first top-up.
After I send the coins, check with get_account_state
that the wallet is deployed.
When you need a hosted key
The local npx run uses the public config and is great for development. If you need guaranteed throughput under load (those same mass payouts with highload), switch to the hosted endpoint with your own key:
{
"mcpServers": {
"ton": {
"type": "http",
"url": "https://mcp.tonnode.io/mcp",
"headers": { "Authorization": "Bearer tn_live_…" }
}
}
}
Wrapping up
generate_walletcreates a TON wallet in a single call and returns the mnemonic, private/public keys, and address — one call with a version parameter, not a hand-assembled SDK setup.- The default version for a new project is v5r1; for mass payouts, highload_v3; v4 is for plugins; v3r2 when you want a simple wallet.
- Send the first deposit to the non-bounceable UQ address, or it bounces back;
parse_addressgives you the UQ form andget_account_statechecks the state. - For highload, pin down
subwallet_idandtimeoutyourself and store them alongside the mnemonic — both are part of the state init and affect the address. - The server is non-custodial: it doesn't store or sign keys or funds — the keys are yours.
Grab a free Hobby key and create your first wallet with generate_wallet in a couple of minutes — tonnode.io/dashboard?plan=hobby.
Give your agent TON access
16 MCP tools: reads, non-custodial swaps, cross-chain and wallets. Free tier — 60 req/min, no card required.