Okay, so you're staring at your seed phrase, thinking "this should work." But nope. Turns out, you derived the wrong damn address. Happens to me all the time when I switch wallets. The thing is, crypto addresses aren't random strings-they come from your seed via these derivation paths. Mess it up, and you're sending to a ghost address. No funds lost, just invisible to the wrong wallet. Sound familiar?
Why does this matter? Because one path like m/44'/60'/0'/0/0 gets your main ETH address. Change that 44 to 49, and poof-totally different keys. Same seed, different tree branch. In my experience, knowing this saved my ass during a Solana migration. Let's fix that. I'll walk you through deriving addresses step by step, hands on. Grab your seed (never share it), a computer, and some libraries. Ready?
Everything starts with a seed phrase-12 or 24 words. That's your master. From there, HD wallets (hierarchical deterministic) grow a tree of keys. No need for thousands of separate seeds. One seed rules them all.
Derivation paths? They're the GPS coordinates in that tree. Like m/purpose'/coin'/account'/change/index. The ' means hardened-extra secure, can't peek from public keys. Soft ones (no ') let you derive publicly without privates exposed.
Honestly, I usually skip the math. But basically: seed → master private → child keys via HMAC SHA512 math. Paths tell it where to branch.
So m/44'/0'/0'/0/0 = your first BTC legacy receive address. Change to /1/0 for first change address. Pretty much unlimited addresses without new seeds.
Install pip: pip install mnemonic bip32utils ecdsa hashlib. Boom. Or Node: iancoleman.io/bip39 for playground-no code needed. I usually test there before scripting.
Let's derive a Bitcoin segwit address. Use test seed: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about". Real one? Generate offline.
from mnemonic import Mnemonic
from bip32utils import BIP32Key, BIP32_HARDEN mnemo = Mnemonic("english")
seed = mnemo.to_seed("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
master_key = BIP32Key.fromEntropy(seed)
That's your m/ root.
purpose = masterkey.ChildKey(84 + BIP32HARDEN)
coin = purpose.ChildKey(0 + BIP32_HARDEN)
accountkey = coin.ChildKey(0 + BIP32HARDEN)
Now account_key is your xpub base.
receivechain = accountkey.ChildKey(0) # change=0
firstreceive = receivechain.ChildKey(0) # index=0
btcaddr = firstreceive.Address() # P2WPKH
print(btc_addr) # bc1qcr8te4kr609gcawutmrza0nj2nazwqf93l8q0v
Run it. You'll get bc1q.. That's your first segwit receive. Send test BTC there. What's next? Verify balance on block explorer.
Don't guess. Wallets stick to standards. Wrong path = lost coins (not really lost, just wrong wallet).
| Coin | Legacy | Segwit/Taproot | Account HD Path |
|---|---|---|---|
| Bitcoin | m/44'/0'/0'/0/0 | m/84'/0'/0'/0/0 (segwit) m/86'/0'/0'/0/0 (taproot) | m/84'/0'/0' |
| Ethereum | m/44'/60'/0'/0/0 | Same for all EVM | m/44'/60'/0' |
| Solana | m/44'/501'/0'/0' | m/44'/501'/0' | |
| Litecoin | m/44'/2'/0'/0/0 | m/84'/2'/0'/0/0 | m/84'/2'/0' |
Pro tip: Ledger uses these. Exodus too. But Phantom Solana tweaks sometimes-m/44'/501'/0' vs full. Check wallet docs.
Okay, switch to ETH. Account model, not UTXO mess. Addresses from public hash.
# Same master
ethaccount = masterkey.ChildKey(44 + BIP32HARDEN).ChildKey(60 + BIP32HARDEN).ChildKey(0 + BIP32_HARDEN).ChildKey(0).ChildKey(0)
ethaddr = ethaccount.PublicKey().format().lower() # 0x..
print("0x" + eth_addr.hexdigest()[:40]) # Rough, use keccak for real
Wait, proper way needs hashlib.sha3_256. But iancoleman gives 0xD2D1af1b.. for test seed. Gas? ~21k for simple tx, 0.0005 ETH at 2 gwei.
In my experience, ETH paths rarely change. But LedgerJS example: eth.getAddress("44'/60'/0'/0/0"). Plug your transport.
Software fun, but real money? Hardware. Ledger: Connect, use Ledger Live → Accounts → Edit → Advanced → freshAddressPath. Shows m/86'/0'/0'/0/0 for taproot.
Script it:
from ledgereth import LedgerETH # pip install ledgereth? Nah, use ledgerblue
Issue: Path mismatch. Solution: List addresses in wallet, match explorer. Exodus scans multiples on restore.
ETH? Simple account. Balance accrues to one address forever.
BTC? UTXO jungle. Each input/output new UTXO. Wallets generate fresh receive addresses per tx-privacy. Change goes to /1/index.
Example: You receive to /0/0, /0/1, /0/2. Send? Picks UTXOs, change to /1/0. Next send /1/1. Gap limit: Wallets scan ~20 empty addresses ahead. Don't skip too many or funds "lost" till rescan.
SOL derivation? m/44'/501'/0'/0'. But Phantom drops last /0 sometimes. Trust Wallet m/44'/501'/0'. Test: Import seed to both, check first address match.
Fees dirt cheap: ~0.000005 SOL per sig. Derive ed25519 keys-different curve, but bip32 works.
Now, multi chain seed? One seed, derive BTC at /0', ETH /60', etc. Genius.
1. Gap limit panic. Wallet shows 0 balance after 25 receives? Force rescan or enter exact path/index.
2. Hardened mixup. Forgot '? No privkey derive. Always harden purpose/coin/account.
3. Custom paths. MEW: Access → Mnemonic → Add Path → m/44'/60'/1'/0/0 for second ETH account. Addresses shift.
4. Multisig. Paths start with master fingerprint, not m/. Like 12345678/84'/0'/0'. Advanced, but Unchained does it clean.
5. Fees? BTC ~5 sat/vB, testnet free ish. Always simulate tx.
Question: Balance missing? Check change chain /1/*. Tools like sparrow wallet scan all.
Here's a beast. Paste, tweak seed/path, run.
import hashlib
from mnemonic import Mnemonic
from bip32utils import * def deriveaddress(seedphrase, path): mnemo = Mnemonic("english") seed = mnemo.toseed(seedphrase) master = BIP32Key.fromEntropy(seed) levels = path.split('/') = master for level in levels[1:]: # Skip m idx = int(level.rstrip("'")) if "'" in level: idx += BIP32HARDEN =.ChildKey(idx) # BTC hack - adjust for coin if 'btc' in path.lower(): return.Address() return "0x" +.PublicKey().format().hexdigest()[:40] # ETH rough print(deriveaddress("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", "m/84'/0'/0'/0/0"))
Outputs your segwit. Extend for ETH keccak, SOL ed25519. Boom, personal tool.
Want balance without privkeys? Export xpub from m/purpose'/coin'/account'. Wallets like Electrum import it, derive /0/ and /1/ publicly.
Ledger: btc.getWalletXpub({path: "84'/0'/0'"}). Safe- no privs leak.
In experience, multisig setups shine here. Each cosigner xpub, combine descriptors.
Exodus BTC: m/84'/0'/0'/0/0. Ledger taproot: m/86'. Import seed to both? Might need path tweak.
Fix: Restore with custom path in MEW/Coinomi. Or electrum-pick derivation on import.
Table of wallet quirks:
| Wallet | BTC Default | ETH | SOL Notes |
|---|---|---|---|
| Ledger | m/84'/0'/0'/0/0 | m/44'/60'/0'/0/0 | Custom app |
| Exodus | m/84'/0'/0'/0/0 | m/44'/60'/0'/0/0 | m/44'/501'/0'/0' |
| Phantom | - | - | m/44'/501'/0'/0' |
| Trezor | m/84'/0'/0'/0/0 | m/44'/60'/0'/0/0 | Via suite |
Pro move: Always note your path. Paper backup: "Seed: words.. Path: m/44'/60'/0'/0/0 addr: 0xabc.."
Why new receive every time? Links tx graph less. Chainalysis hates it.
But generate ~20 ahead? Wallets auto. Manual? Increment index.
Question: Change privacy? Mixers or coinjoin, but paths stay same.
That's the core. Practice on testnet. Derive 10 addresses, send 0.0001 BTC around. Muscle memory. Hit snags? Paths first suspect. You've got this-now go secure those bags.