Recovering a Wallet from an Incomplete Seed Phrase
- 7 minsRecovering a Wallet from an Incomplete Seed Phrase
Sunday night, I woke up to a friend calling my phone. At first, I figured I’d just check in the morning since it was the middle of the night. But after a few calls, I gave in and picked up.
It turned out their laptop had stopped working, and while trying to recover a wallet using a seed phrase written on paper, they realized one word was missing. That single missing word meant they couldn’t access any of the funds. I told them, “Give me 5 minutes while I grab something to drink.”
They explained that the piece of paper with the seed phrase looked something like this:
(For demonstration purposes, this is a sample phrase. Do not use it.)
# zoo mirror
# orange kangaroo
# about setup
# fine memory
# hero obscure
# idle
According to them, only one word was missing. However, they weren’t sure how the phrase had been written down. It could have been listed row by row or top to bottom in columns. To make things more complicated, they didn’t remember where in the sequence the missing word belonged, it could have been the first, last, or somewhere in the middle.
That left us with eleven known words and a lot of uncertainty. We didn’t know the correct order, and we didn’t know the missing word’s position. All we had was a list of eleven valid BIP39 words and the knowledge that one word was missing.
If interpreted row by row, the phrase looks like:
Rowwise:
zoo, mirror, orange, kangaroo, ...
Columnwise:
zoo, orange, about, fine, ...
I figured this was a manageable problem. Brute-forcing seemed like the simplest and most direct approach, but before writing any code, I did some mental math to estimate the scope.
The BIP39 wordlist contains 2048 words. Since one word was missing and it could be inserted in any of the twelve positions, that gave us 2048 × 12 = 24,576 combinations for one layout. With two possible layouts (row-wise and column-wise), that totaled 49,152 phrases to try. This is well within the capabilities of a modern laptop. Each candidate phrase can be generated, validated, and tested in milliseconds.
But seed phrases don’t map to just one wallet address—they produce an entire hierarchy of addresses. A single seed can generate thousands of valid wallet addresses depending on the derivation path. So even if we found the correct 12-word phrase, how would we confirm it?
Fortunately, they remembered their main Ethereum address. That gave us a definitive reference point. All we had to do was regenerate addresses from each candidate mnemonic and check if any matched the known address. If so, we’d know for sure we had the right phrase.
With that, I began writing the brute-force script.
The script tries both layout interpretations, inserts every possible BIP39 word into all twelve positions, validates the mnemonic, derives Ethereum addresses, and checks each against the known address.
from mnemonic import Mnemonic
from bip44 import Wallet
from eth_account import Account
from tqdm import tqdm
TARGET = "0xE2DF586859C9047DfE6F1B9ae970b560cBC88106"
mnemo = Mnemonic("english")
words = mnemo.wordlist
# zoo mirror
# orange kangaroo
# about setup
# fine memory
# hero obscure
# idle
# Define your two lists of 11 known words
columnwise = ["zoo", "mirror", "orange", "kangaroo", "about", "setup", "fine", "memory", "hero", "obscure", "idle"] # Change as needed
rowwise = ["zoo", "kangaroo", "setup", "fine", "memory", "obscure", "mirror", "orange", "about", "hero", "idle"]
for phrase_type, base in {"columnwise": columnwise, "rowwise": rowwise}.items():
known = [w for w in base if w != "___"]
for pos in range(12):
desc = f"{phrase_type} pos {pos+1}/12"
for candidate in tqdm(words, desc=desc):
phrase = known[:]
phrase.insert(pos, candidate)
phrase_str = " ".join(phrase)
if not mnemo.check(phrase_str):
continue
try:
wallet = Wallet(phrase_str)
for idx in range(100):
try:
privkey = wallet.derive_account("eth", account=idx)[0]
addr = Account.from_key(privkey).address.lower()
if addr == TARGET.lower():
print("MATCH FOUND!")
print(f"Interpretation: {phrase_type}")
print(f"Seed Phrase: {phrase_str}")
print(f"Derivation Index: {idx}")
print(f"Address: {addr}")
print(f"Private Key: {privkey.hex()}")
exit()
except Exception:
continue
except Exception:
continue
print("No match found.")
The script is simple in logic. For each layout interpretation, it loops over all possible insertions of each BIP39 word. If the resulting phrase passes the checksum, it derives the Ethereum addresses from that seed using the standard BIP44 path and checks up to the first 100 accounts. If it finds a match, it prints the results and stops.
The total search space, while not trivial, is small enough to complete in minutes. The BIP39 checksum ensures only valid phrases are processed, cutting down wasted computations.
This kind of recovery is only possible if you know 11 of the 12 words and can recognize the correct address. If either condition isn’t met, the problem becomes exponentially harder. But in this case, everything lined up, and the wallet was successfully recovered shortly after.
No funds were lost. But this served as a reminder of how fragile paper backups can be. If your wallet is written down on paper somewhere, now might be a good time to double-check that everything is still legible and complete.
To explain why this process works:
Recovering a wallet from a 12-word seed phrase doesn’t restore a single address—it regenerates a master key, called the BIP32 root key. This root key is derived from the mnemonic using BIP39, which converts the 12 words into a 512-bit seed.
That seed is then used in a deterministic wallet structure defined by BIP44. The standard Ethereum derivation path looks like this:
m / 44' / 60' / 0' / 0 / index
44' = BIP44 standard
60' = Ethereum's coin type
0' = account index (usually 0)
0 = external chain (receiving addresses)
index = address index
The Ethereum address is derived by computing the public key using elliptic curve multiplication (on secp256k1), hashing it with Keccak-256, and taking the last 20 bytes.
Because this process is deterministic and secure, if you generate a valid mnemonic and know the correct derivation path, you can reproduce all the same wallet addresses.
That’s what made brute-forcing the missing word possible. With just 11 known words and a reference address, the rest was just computation.
Best of all this luckly only took around 45 min so I could go back to bed again. :D