You need nothing but Python 3 and a terminal. It is already installed on
macOS and Linux; on Windows, install it from python.org and tick
“Add python.exe to PATH”. Confirm it is there by opening a terminal and
running python3 --version, which should print something like
Python 3.12.3. No extra libraries are needed — everything below uses
Python’s built-in hashlib.
Do not type your own recovery phrase into any of this. The exercise uses a phrase published in the BIP-39 standard as a test case, so your output will match this page line for line. A real phrase typed into any computer is a real phrase that has been exposed — and the whole point of the next fifteen minutes is to understand why.
What is a Seed Phrase?
A seed phrase (also called a recovery phrase or mnemonic) is a sequence of 12 or 24 ordinary English words that encodes all the information needed to reconstruct your entire wallet. It is the human-readable form of your master private key.
Example of what a seed phrase looks like (do NOT use this):
abandon ability able about above absent absorb abstract absurd abuse access accident
These 12 words, in this exact order, would generate specific private keys and addresses. Change a single word or swap the order, and entirely different keys are produced.
How Seed Phrases Work (BIP-39)
Seed phrases follow the BIP-39 standard (Bitcoin Improvement Proposal 39):
- The wallet generates a large random number (128 bits for 12 words, 256 bits for 24 words)
- This random data is converted into words from a standardized list of 2,048 English words
- The final word includes a checksum that verifies the phrase was recorded correctly
- From this seed, a master key is derived, and from that master key, an unlimited number of private keys and addresses can be generated deterministically
A single seed phrase can generate addresses for Bitcoin, Ethereum, and many other cryptocurrencies simultaneously. This is why losing your seed phrase can mean losing access to all your crypto assets at once.
Seed Phrases vs Private Keys
Why 12 or 24 Words?
The number of words determines the entropy (randomness) of your seed:
- 12 words = 128 bits of entropy = 2128 possible combinations (more than the number of atoms in the observable universe)
- 24 words = 256 bits of entropy = 2256 possible combinations (astronomically more secure)
Both are considered secure against brute-force attacks with current and foreseeable technology. 24-word phrases provide additional margin against future advances in computing.
The Rules of Seed Phrase Security
Breaking any of these rules can result in permanent, irrecoverable loss of all cryptocurrency associated with the seed phrase.
- NEVER share your seed phrase with anyone. No legitimate service, support team, or software will ever ask for it. Anyone who asks is trying to steal your funds.
- NEVER type it into a computer, phone, or website. Malware, keyloggers, and phishing sites can capture it instantly.
- NEVER photograph it or screenshot it. Photos are synced to cloud services, backed up automatically, and accessible to malware.
- NEVER store it in a password manager, email, or cloud note. If that service is compromised, your crypto is gone.
- NEVER enter it into any software that is not your own verified wallet. Fake wallet apps and browser extensions exist specifically to harvest seed phrases.
How to Store Your Seed Phrase
- Write it on paper using a pen (not pencil, which fades). Store in a waterproof bag in a secure location.
- Stamp it on metal using a steel seed phrase backup kit. Resistant to fire, water, and corrosion. The most durable option.
- Store in a secure location such as a home safe, bank safety deposit box, or other tamper-evident container.
- Consider geographic distribution — store copies in two physically separate locations to protect against localized disasters.
What Happens If Someone Gets Your Seed Phrase
If anyone obtains your seed phrase, they can:
- Reconstruct your entire wallet on their own device
- Access every cryptocurrency address derived from that seed
- Transfer all your funds to their own addresses instantly
- This is irreversible — there is no way to undo it or recover the funds
If you ever suspect your seed phrase has been compromised, immediately transfer all funds to a new wallet with a freshly generated seed phrase.
Now Run the Check Your Wallet Runs, in Five Steps
Everything above describes BIP-39 from the outside. In the next fifteen minutes you will implement the part of it that matters most — the checksum — and use it to answer a question nobody usually gets a straight answer to: if I copy one word wrong, will anything warn me? You will find that the answer is “usually, but not always”, measure exactly how often “not always” is, and finish by turning a phrase into the master secret it stands for. Every number printed below came from running these exact files.
Go: open a terminal in a folder you can write to, for example
cd ~/Desktop (Windows: cd %USERPROFILE%\Desktop).
Do: save this as bip39.py. It does not print anything yet
— it is the toolbox the next four steps use. A recovery phrase is a list of numbers
dressed up as words, so the code works in numbers and skips the dressing-up.
import hashlib
def to_numbers(entropy: bytes):
"""Turn raw entropy into the word numbers a recovery phrase encodes."""
bits = "".join(f"{b:08b}" for b in entropy)
checksum_len = len(entropy) * 8 // 32
checksum = f"{hashlib.sha256(entropy).digest()[0]:08b}"[:checksum_len]
full = bits + checksum
return [int(full[i:i + 11], 2) for i in range(0, len(full), 11)]
def checksum_ok(numbers):
"""Re-run the check your wallet runs when you type a recovery phrase."""
full = "".join(f"{n:011b}" for n in numbers)
ent_len = len(full) * 32 // 33
entropy = int(full[:ent_len], 2).to_bytes(ent_len // 8, "big")
want = f"{hashlib.sha256(entropy).digest()[0]:08b}"[:len(full) - ent_len]
return full[ent_len:] == want
You should see: nothing at all. Run python3 bip39.py and it
should return you straight to the prompt with no output and no error. That silence is the
pass condition — it means the file parses.
If not: IndentationError means the four-space indents inside
the functions were lost in copying; every line under a def must be indented.
SyntaxError: invalid syntax on the f"..." lines means you are on
Python 2 — check python3 --version and use python3
explicitly.
Go: same folder, next to bip39.py.
Do: save this as step2.py and run
python3 step2.py. The entropy is fixed rather than random so that your output
matches this page exactly.
from bip39 import to_numbers, checksum_ok
entropy = bytes.fromhex("0c1e24e5917779d297e14d45f14e1a1a")
numbers = to_numbers(entropy)
print("entropy (128 bits):", entropy.hex())
print("word numbers :", numbers)
print("checksum valid :", checksum_ok(numbers))
You should see: twelve numbers, each between 0 and 2047, and a passing check:
entropy (128 bits): 0c1e24e5917779d297e14d45f14e1a1a
word numbers : [96, 1929, 459, 279, 956, 1866, 764, 333, 559, 1107, 1076, 423]
checksum valid : True
Those twelve numbers are the phrase. Your wallet looks each one up in the same published list of 2,048 English words that every BIP-39 wallet uses, and shows you the word instead of the number. Nothing else happened: 128 bits of randomness went in, twelve look-up numbers came out.
If not: ModuleNotFoundError: No module named 'bip39' means
step2.py is not in the same folder as bip39.py — run
ls (Windows: dir) and confirm both names are there. If the numbers
differ, one character of the hex string was mistyped; it must be exactly 32 characters.
Go: same folder. Never trust an implementation you have not tested — including this one.
Do: save this as step3.py and run it. BIP-39 publishes test
cases, and the simplest is entropy of all zeros, whose phrase is the famous
“abandon” × 11 then “about”. In the official word list
abandon is number 0 and about is number 3.
from bip39 import to_numbers
print("all-zero entropy ->", to_numbers(bytes(16)))
You should see: eleven zeros and a three — exactly the phrase the standard says it should be:
all-zero entropy -> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3]
Notice where the 3 came from: the last word carries the leftover entropy bits plus the four checksum bits. That is why the final word of a recovery phrase is never freely chosen — it is partly a check digit, like the last digit of a credit card number.
If not: if the last number is not 3, the checksum slice in
to_numbers is wrong — compare that line against step 1 character by
character. If you get twelve zeros, the checksum is not being appended at all.
Go: same folder. This is the step that changes how you handle paper.
Do: save this as step4.py and run it. It swaps a single
word for its neighbour in the list — the exact error a tired person makes reading their
own handwriting — and then repeats that mistake ten thousand times at random.
import random
from bip39 import checksum_ok
good = [96, 1929, 459, 279, 956, 1866, 764, 333, 559, 1107, 1076, 423]
typo = list(good)
typo[4] = 957
print("phrase as written :", checksum_ok(good))
print("word 5 mistyped :", checksum_ok(typo))
random.seed(7)
accepted = 0
for _ in range(10000):
trial = list(good)
trial[random.randrange(12)] = random.randrange(2048)
accepted += checksum_ok(trial)
print(f"{accepted} of 10000 single-word errors were NOT caught")
print(f"that is 1 in {10000 // accepted}")
You should see: the typo caught — and then the uncomfortable number:
phrase as written : True
word 5 mistyped : False
628 of 10000 single-word errors were NOT caught
that is 1 in 15
The checksum is four bits, so roughly one wrong phrase in sixteen passes it by chance, and the measured result lands exactly there. Read what that means: about 94% of the time your wallet will say “invalid recovery phrase” and save you. The other 6% of the time it will open a perfectly valid wallet that is not yours, show you a zero balance, and say nothing was wrong. “The wallet accepted it” is therefore not proof that your backup is correct.
If not: if accepted comes out as 0 you will get a
ZeroDivisionError on the last line — that means checksum_ok
is rejecting everything, so re-check it against step 1. A slightly different count than 628
means your random.seed(7) line is missing; the proportion should still be close
to one in sixteen.
Go: same folder. This is the last conversion in the chain, and it is what your wallet does the moment you finish typing.
Do: save this as step5.py and run it. The words are stirred
2,048 times with PBKDF2 to produce 512 bits — the seed every key in the wallet grows
from.
import hashlib, unicodedata
phrase = " ".join(["abandon"] * 11 + ["about"])
seed = hashlib.pbkdf2_hmac("sha512",
unicodedata.normalize("NFKD", phrase).encode(),
b"mnemonic", 2048)
print("phrase :", phrase)
print("seed :", seed.hex()[:64])
print(" ", seed.hex()[64:])
You should see: the standard’s own published answer for this test phrase:
phrase : abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
seed : 5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc1
9a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4
Now the whole picture is in front of you: twelve words → twelve numbers → 128 bits of entropy → one 512-bit seed → every address you will ever use. There is no account, no server and no password reset anywhere in that chain. It also explains why any wallet, from any maker, can restore the same coins from the same words: they all run the computation you just ran.
If not: a different seed means the phrase string differs — it needs
single spaces between words and no trailing space. If you see
TypeError: pbkdf2_hmac() argument 'salt' must be bytes, the b
before "mnemonic" is missing.
Without scrolling up: your friend restores a wallet, the app accepts the phrase without complaint, and the balance shows zero. Name the two possibilities, and the one test that tells them apart. Answer: either the phrase is correct and the coins were moved (or were never there), or the phrase is one of the roughly one-in-sixteen wrong phrases that still passes the checksum, and the app has opened a different empty wallet. The test is to compare a receiving address from the restored wallet against an address they know belongs to the original — if the addresses differ, the phrase is wrong, not the balance. Doing that comparison on the day you write the phrase down, not on the day you need it, is the entire discipline.
Now do it without the page: pick any twelve numbers between 0 and 2047
out of your head, run them through checksum_ok, and keep adjusting only the
last number until it returns True. Exactly 128 of the 2,048 possible
last words work, so you will hit one roughly every sixteen tries.
You have just hand-forged a valid recovery phrase — which is the clearest possible proof
that validity says nothing about whose wallet it opens.
Summary
- A seed phrase is the master backup for your entire crypto wallet
- It follows the BIP-39 standard and can derive unlimited private keys
- 12-word and 24-word phrases are both secure against brute force
- Never share, photograph, or digitally store your seed phrase
- Store it on paper or metal in a physically secure location
- If compromised, move all funds to a new wallet immediately
Next, learn how to build a robust backup strategy to protect your crypto keys against all scenarios.