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.
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
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
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.
Tap "Get Started" then "New Wallet." The app generates a 24-word 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.
Enable a strong PIN or passcode. Enable biometric unlock (Face ID/fingerprint) for convenience, but the passcode is your primary protection.
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
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.
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.
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.
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.
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.
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.
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
You can now securely store, send, and receive Toncoin. Next, learn about staking to earn rewards on your holdings.