Skip to content

TON Wallet Setup & Security

💡
Before you start

Python 3, a terminal, and the file tonaddr.py from the TON introduction tutorial. If you have not built it, open Introduction to TON and do its step 1 first — it is a single file and takes two minutes. Everything else here uses only modules that come with Python. Check Python is ready with python3 --version.

No Toncoin, no wallet app and no internet are needed. The exercise is about the two questions that decide whether your money is reachable: which address does my seed actually control, and is the address in the send box the one I meant. Both are answered offline, with arithmetic.

TON Wallet Basics

A TON wallet is a smart contract deployed on the TON blockchain. Unlike Bitcoin or Ethereum where wallets are simply key pairs, every TON wallet is an actual smart contract with its own code and state. This gives TON wallets programmable features but also means they behave differently.

💡
Wallet versions matter

TON has multiple wallet contract versions (v3R2, v4R2, v5). Each version adds features. Most modern apps use v4R2 or v5. Your wallet app chooses the version automatically, but knowing this helps when troubleshooting.

Choosing a Wallet

Tonkeeper The most popular TON wallet. Available on iOS, Android, and as a browser extension. Supports staking, NFTs, Jettons, dApp browser, and Ledger integration. Open source.
MyTonWallet Lightweight browser extension and web wallet. Good for desktop users who prefer a browser-based experience. Supports staking and Jettons. Open source.
Telegram Wallet (@wallet) Built into Telegram via the @wallet bot. Easiest onboarding for Telegram users but custodial — Telegram holds the keys. Suitable only for small amounts.
TON Space Self-custodial wallet inside Telegram. You control the keys, but the interface is within Telegram. Good balance of convenience and security.
Ledger (Hardware) Ledger Nano S Plus and Nano X support TON natively. Keys never leave the device. Connect through Tonkeeper or MyTonWallet for the best security.
⚠️
Custodial vs self-custodial

The Telegram @wallet bot is custodial — Telegram holds your private keys. If Telegram is hacked, goes down, or freezes your account, you lose access. Use self-custodial wallets (Tonkeeper, MyTonWallet, Ledger) for any significant amount.

Setting Up Tonkeeper

1
Download from official sources only

Get Tonkeeper from the official website (tonkeeper.com) or your device's official app store. Never download wallet apps from third-party sites or links in messages.

2
Create a new wallet

Tap "Get Started" then "New Wallet." The app generates a 24-word recovery phrase.

3
Write down your recovery phrase

Write all 24 words on paper in the exact order shown. Never screenshot, photograph, or store digitally. Verify by entering the words back.

4
Set a passcode and biometrics

Enable a strong PIN or passcode. Enable biometric unlock (Face ID/fingerprint) for convenience, but the passcode is your primary protection.

5
Fund your wallet

Your wallet address is shown on the main screen. Send a small test transaction first to verify everything works before sending larger amounts.

TON Wallet Security Best Practices

  • Use a hardware wallet for large holdings: Connect a Ledger to Tonkeeper for the best security. The private key never touches your phone or computer
  • Never share your 24-word phrase: No legitimate service, support team, or airdrop will ever ask for it. Anyone who asks is a scammer
  • Verify transaction details: Before confirming any transaction, check the recipient address, amount, and any smart contract interactions
  • Be cautious with dApp connections: The Tonkeeper dApp browser lets sites request transaction approval. Only connect to trusted, verified dApps
  • Keep your wallet app updated: Security patches are released regularly. Enable auto-updates or check manually
  • Use multiple wallets: Keep a "hot" wallet with small amounts for daily use and a "cold" wallet (hardware) for savings
  • Beware of TON DNS phishing: Scammers register .ton domains that look like legitimate services. Always verify URLs independently

Backup Strategies

  • Metal backup: Stamp or engrave your 24 words on a metal plate. Survives fire and water damage
  • Multiple locations: Store copies in at least two physically separate, secure locations
  • Never store digitally: No photos, no cloud storage, no password managers, no notes apps. Digital storage is vulnerable to malware and cloud breaches
  • Test recovery: Periodically verify you can restore from your backup by importing into a fresh wallet app (then delete the test wallet)

Common Mistakes

⚠️
TON-specific pitfalls

Because TON wallets are smart contracts, they need a small amount of Toncoin for storage fees. A wallet with exactly 0 TON may become frozen. Always keep a small balance (0.05 TON) to keep the contract active.

Now Prove Which Address Your Seed Controls, in Five Steps

The most common panic in TON is a person restoring their seed phrase into a wallet app and finding an empty account. The coins are not gone, and nothing was hacked — the app opened a different address from the same seed. In the next twenty-five minutes you will reproduce that exactly, learn the four address forms and what their first two characters mean, then watch a piece of malware produce an address that matches the first six characters of yours in about ten seconds. Every line of output came from running these files.

1
Get three different addresses out of one seed phrase

Go: open a terminal in the folder that already holds tonaddr.py from the introduction tutorial.

Do: save this as versions.py and run python3 versions.py. On TON your address is derived from a hash of the wallet contract’s code together with your public key, so a different wallet version is a different address. This models that faithfully.

import hashlib
from tonaddr import encode

def address_of(wallet_code: bytes, public_key: bytes) -> str:
    """TON derives the account id from a hash of the wallet's code AND its data."""
    account = hashlib.sha256(wallet_code + public_key).digest()
    return encode(account, bounceable=False)

pubkey = hashlib.sha256(b"one seed phrase").digest()

for version in (b"wallet-v3r2-code", b"wallet-v4r2-code", b"wallet-v5r1-code"):
    print(f"{version.decode():18s} -> {address_of(version, pubkey)}")

You should see: one seed, three unrelated addresses:

wallet-v3r2-code   -> UQC7PjV83BzxZNOoVDK6tvl3ejqr_JZpNc_TyWh2C4MN0rq7
wallet-v4r2-code   -> UQABKyBrZcaKontdREkPW1PAtA6c3TX4gkaOmzdIF1kWllHe
wallet-v5r1-code   -> UQDNK7Mkw-cAUGsrU-W6nvtc3ZCEmQhuAH6IpffOGfb4VQ1z

This is the whole explanation for the empty-wallet panic. Your seed is correct, your keys are correct, and the app simply opened the address belonging to a different wallet version. Good wallet apps check several versions and offer you the one holding a balance; if yours does not, look for a “wallet version” or “add wallet” setting rather than concluding the money is lost.

If not: ModuleNotFoundError: No module named 'tonaddr' means this file is not beside tonaddr.py. If all three addresses match, the loop variable is not reaching address_of.

2
Generate all four address forms and read their first two characters

Go: same folder.

Do: save this as forms.py and run it. The flags byte carries two independent bits: bounceable or not, and mainnet or testnet.

import base64, hashlib
from tonaddr import crc16

def encode_full(account, workchain=0, bounceable=True, testnet=False):
    flags = (0x11 if bounceable else 0x51) | (0x80 if testnet else 0x00)
    body = bytes([flags, workchain & 0xFF]) + account
    return base64.urlsafe_b64encode(body + crc16(body).to_bytes(2, "big")).decode()

account = hashlib.sha256(b"finkatana practice account").digest()
for testnet in (False, True):
    for bounce in (True, False):
        label = ("testnet" if testnet else "mainnet") + (", bounceable" if bounce else ", plain")
        print(f"{label:22s} {encode_full(account, bounceable=bounce, testnet=testnet)}")

You should see: the same account wearing four different labels:

mainnet, bounceable    EQA58lkROuTea1y6Q8cBPluIC8WQqX_kE6bKetiN4maxCkOX
mainnet, plain         UQA58lkROuTea1y6Q8cBPluIC8WQqX_kE6bKetiN4maxCh5S
testnet, bounceable    kQA58lkROuTea1y6Q8cBPluIC8WQqX_kE6bKetiN4maxCvgd
testnet, plain         0QA58lkROuTea1y6Q8cBPluIC8WQqX_kE6bKetiN4maxCqXY

Learn these two characters and you can read any TON address at a glance: EQ mainnet bounceable, UQ mainnet plain, kQ and 0Q the testnet equivalents. If somebody sends you a kQ or 0Q address and asks for real Toncoin, they have either made a mistake or are testing whether you check — a testnet address is not a real-money destination.

If not: if the testnet lines start EQ/UQ, the | (0x80 ...) is not being applied — it must be the bitwise or pipe, not a comma.

3
Forge an address that matches the start of yours

Go: same folder. Clipboard-hijacking malware swaps a copied address for the attacker’s. People defend against it by glancing at the first few characters, so attackers grind out an address whose first few characters match.

Do: save this as collide.py and run it. It takes roughly ten seconds; leave it running.

import hashlib, time
from tonaddr import encode

target = "UQC7PjV83BzxZNOoVDK6tvl3ejqr_JZpNc_TyWh2C4MN0rq7"
prefix, suffix = target[:6], target[-4:]

start = time.time()
for n in range(5_000_000):
    account = hashlib.sha256(f"attacker-{n}".encode()).digest()
    candidate = encode(account, bounceable=False)
    if candidate.startswith(prefix):
        print("the address you meant to pay :", target)
        print("the one malware put in its place:", candidate)
        print(f"found after {n:,} tries in {time.time() - start:.1f} seconds")
        print(f"first 6 characters match: {candidate[:6] == prefix}")
        print(f"last 4 characters match : {candidate[-4:] == suffix}")
        break

You should see: a convincing impostor. The number of tries is fixed, but the time depends on your machine:

the address you meant to pay : UQC7PjV83BzxZNOoVDK6tvl3ejqr_JZpNc_TyWh2C4MN0rq7
the one malware put in its place: UQC7PjL2LvpLIaAV_oitY7LblAq0b5fLtK-SlWyT16bmd6lS
found after 518,388 tries in 11.1 seconds
first 6 characters match: True
last 4 characters match : False

Half a million attempts, on one laptop, in a language chosen for readability rather than speed. An attacker with purpose-built software and a graphics card extends that to eight or ten characters without difficulty. Recognising an address by its opening is a habit that feels careful and is not.

If not: if it runs for more than a minute with no output, the bounceable=False argument is missing, so the candidates start EQ and can never match a UQ target. If it finishes instantly, the target string was edited to something the loop finds early.

4
Find a check that survives it

Go: same folder, using the two addresses from step 3.

Do: save this as compare.py and run it.

real = "UQC7PjV83BzxZNOoVDK6tvl3ejqr_JZpNc_TyWh2C4MN0rq7"
fake = "UQC7PjL2LvpLIaAV_oitY7LblAq0b5fLtK-SlWyT16bmd6lS"

def check(name, a, b):
    print(f"{name:34s} {'SAME -- would be accepted' if a == b else 'DIFFERENT -- caught'}")

check("first 4 characters only", real[:4], fake[:4])
check("first 6 characters only", real[:6], fake[:6])
check("first 6 AND last 6", real[:6] + real[-6:], fake[:6] + fake[-6:])
check("the whole address", real, fake)

You should see: the popular habit failing and the good one working:

first 4 characters only            SAME -- would be accepted
first 6 characters only            SAME -- would be accepted
first 6 AND last 6                 DIFFERENT -- caught
the whole address                  DIFFERENT -- caught

Checking both ends is the practical rule, because grinding a match at both ends at once is enormously harder than matching a prefix. Better still, for anything large: send a small amount first, confirm it arrived, and only then send the rest.

If not: if the “first 6 AND last 6” line also says SAME, the two address strings were pasted identically — the second one must be the impostor your own step 3 produced.

5
Write down the one thing your seed phrase does not record

Go: same folder. This is the step that turns the lesson into a habit.

Do: save this as recover.py and run it.

import hashlib
from tonaddr import encode

def address_from(seed_phrase, wallet_version):
    pubkey = hashlib.sha256(seed_phrase.encode()).digest()
    return encode(hashlib.sha256(wallet_version + pubkey).digest(), bounceable=False)

seed = "one seed phrase"
print("today, with v4r2      :", address_from(seed, b"wallet-v4r2-code"))
print("in five years, v4r2   :", address_from(seed, b"wallet-v4r2-code"))
print("same seed, wrong guess:", address_from(seed, b"wallet-v3r2-code"))

You should see: the same seed reproducing the same address forever — provided you know which version to ask for:

today, with v4r2      : UQABKyBrZcaKontdREkPW1PAtA6c3TX4gkaOmzdIF1kWllHe
in five years, v4r2   : UQABKyBrZcaKontdREkPW1PAtA6c3TX4gkaOmzdIF1kWllHe
same seed, wrong guess: UQC7PjV83BzxZNOoVDK6tvl3ejqr_JZpNc_TyWh2C4MN0rq7

So your backup needs three things, not one: the seed phrase, the wallet version, and your receiving address. The address is not secret — you give it to everyone who pays you — and writing it beside the phrase turns any future recovery into a two-second check: restore, compare, done. Without it you are guessing which of several valid addresses was yours.

If not: if the first two lines differ, something random is leaking into the derivation — every input here must come from the arguments, never from secrets or the clock.

🎉
Check yourself before moving on

Without scrolling up: a friend restores their seed into a new TON wallet app and the balance shows zero, though they are certain the phrase is right. Give them the two things to check, in order, and say which one is far more likely. Answer: first, the wallet version — the app has almost certainly opened a different version’s address from the same keys, so look for a wallet-version or add-wallet setting and compare the resulting address against the one they used to receive funds. Second, and much less likely, a mistyped word in the phrase, which would produce a valid but unrelated wallet. The version is the likelier cause by a wide margin, and it is the one that costs nothing to fix, which is why it is checked first.

Now do it without the page: change collide.py to match seven characters instead of six and time it, then estimate from your two measurements how long eight would take. Extrapolate to what a machine a thousand times faster manages overnight. That estimate is the real answer to “how many characters do I need to check?” — and it is why the answer is “both ends, or all of it”.

Summary

  • TON wallets are smart contracts, not just key pairs
  • Tonkeeper and MyTonWallet are the recommended self-custodial options
  • The Telegram @wallet is custodial — use only for small amounts
  • Hardware wallets (Ledger) provide the strongest security for large holdings
  • Your 24-word recovery phrase is the single most important thing to protect
  • Keep a small TON balance to prevent wallet contract from freezing
🎉
Your TON wallet is ready!

You can now securely store, send, and receive Toncoin. Next, learn about staking to earn rewards on your holdings.