Audit fixes: - Wallet names were used unvalidated as file paths, so a name like "../<other-id>/x" reached another user's wallet directory or overwrote bot_settings.json. Names are now validated and every path is resolved and confirmed to sit inside the caller's own directory, which also blocks a planted symlink from escaping. - Tor mode leaked: the mempool.space fee check opened a direct clear-net connection, tying the server's IP to its Bitcoin activity. It now goes through Tor's SOCKS proxy to mempool's onion service, DNS included. - The xpub message was left in the chat forever. It reveals the wallet's whole address and balance history, so it is now deleted like the password is. - Inline buttons stay live on old messages, so a user removed from the allow-list could keep using them. Callbacks re-check authorisation, and auth_check now works for callback queries instead of raising on a null message. - Wallet names echoed back in callback_data are confirmed against the user's real wallet list. - Settings are written via temp file + atomic rename; a crash mid-write used to truncate the file and lose every user's wallet selection. - Replies were sent against messages that had just been deleted; deletion is now best-effort and cannot abort the flow. Deployment: - Allow-list can come from ALLOWED_USERS instead of a bind-mounted file, so a deployment needs no files on disk. Both sources merge when both are present. - A missing bind-mounted allowed_users.txt made Docker create a directory in its place and silently lock everyone out; the entrypoint now detects that and stops with an explanation. - Standard mode honours ELECTRUM_SERVER, which previously only worked in Tor mode despite being documented. - Added VERSION, push.sh, docker-compose.deploy.yml and .env.deploy.example for a two-file deploy off the published image. Docs rewritten in English and Persian around the name Telegtrum, leading with the security model and what it does not protect against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Telegtrum
Telegram + Electrum — a self-hosted Telegram bot for managing Bitcoin wallets with Electrum.
Telegtrum is built for watch-only (xpub) wallets with air-gapped signing. Your private keys never touch the server. The bot can see balances, hand out receiving addresses and build unsigned transactions; only your offline device can sign them.
Security model — read this first
| The server never holds private keys | You import an xpub. It can watch, not spend. |
| Spending needs your offline device | The bot produces an unsigned transaction; you sign it elsewhere and paste it back to broadcast. |
| Access is deny-by-default | Only Telegram user IDs you list may use the bot. An empty list refuses everyone. |
| Secrets are scrubbed from chat | Wallet passwords and your xpub are deleted from the conversation right after they're read. |
| Wallets are isolated per user | Each Telegram user gets their own directory; names are validated so one user cannot reach another's files. |
What this does not protect against. Anyone who controls your Telegram account can see your balances and addresses, and can build transactions (they still cannot sign them). Anyone with root on the server can read wallet files and see wallet passwords in the process list while an Electrum command runs — so treat the server as trusted infrastructure and keep the allow-list short. Your xpub is a privacy secret: whoever has it can see your entire transaction history forever.
Quick deploy (recommended)
No source checkout, no build. Two files and a docker compose up.
mkdir telegtrum && cd telegtrum
curl -O https://git.goyban.com/goyban/telegtrum_bot/raw/branch/main/docker-compose.deploy.yml
curl -o .env https://git.goyban.com/goyban/telegtrum_bot/raw/branch/main/.env.deploy.example
nano .env # add your bot token and your Telegram user ID
docker compose -f docker-compose.deploy.yml up -d
docker compose -f docker-compose.deploy.yml logs -f
Your .env needs two things:
TELEGRAM_BOT_TOKEN=123456:ABC... # from @BotFather
ALLOWED_USERS=123456789 # from @userinfobot, comma-separated for several
Compose refuses to start if ALLOWED_USERS is unset, rather than launching a bot nobody can use.
You should see Electrum daemon started. Launching bot.... Message your bot on Telegram and press Add Wallet.
Updating
docker compose -f docker-compose.deploy.yml pull
docker compose -f docker-compose.deploy.yml up -d
Wallet data lives in the telegtrum-data volume and survives updates.
Prerequisites
- A Linux server with Docker and Docker Compose
- A Telegram bot token — @BotFather
- Your Telegram user ID — @userinfobot
Building from source
Only needed if you want to modify the bot.
git clone https://git.goyban.com/goyban/telegtrum_bot.git
cd telegtrum_bot
cp .env.example .env && nano .env
docker compose up -d --build
The source compose files mount bot.py read-only, so you can edit it and docker compose restart without rebuilding.
Allow-list: file or environment
Either works; if you use both, the two lists are merged.
Environment (simplest — nothing to mount):
ALLOWED_USERS=123456789,987654321
File — allowed_users.txt, one user per line, id, Name:
cp allowed_users.txt.example allowed_users.txt
123456789, Alice
If you bind-mount
allowed_users.txtand the file doesn't exist, Docker silently creates a directory in its place and nobody can log in. The container now detects this and stops with an explanation instead of starting unusable.
Publishing a new image
./push.sh # tag comes from the VERSION file
./push.sh 1.0.1 # or pass one explicitly
Connection modes
Standard — public Electrum servers
The default. Electrum picks public peers. Pin it to one server instead:
ELECTRUM_SERVER=192.168.1.100:50001:t
Local / Umbrel — your own node
cp .env.example .env # set TELEGRAM_BOT_TOKEN and UMBREL_IP
docker compose -f docker-compose.local.yml up -d --build
umbrel.localwill not resolve inside a container — mDNS is host-only. Use the actual IP, e.g.192.168.1.100.
On first start the entrypoint writes ./electrum-data/config pinning that server. An existing config is left alone, so manual edits survive restarts.
If it won't connect, check that the server string in
./electrum-data/configstill ends in:t(TCP). Electrum sometimes rewrites it to:s(SSL). Stop the container, change it back, restart.
Tor
cp .env.tor.example .env
docker compose -f docker-compose.tor.yml up -d --build
Either point it at your own onion node:
ELECTRUM_SERVER=yournode123.onion:50001:t
…or leave it unset to use a public onion server (electrums3lojbuj.onion:50001:t, override with ELECTRUM_ONION_SERVER).
Notes:
- Tor runs inside the same container; the entrypoint waits for the SOCKS proxy before starting Electrum.
- The fee lookup goes through Tor too. It uses mempool.space's onion service, so checking fees cannot tie your server's IP to your Bitcoin activity.
- Wallet data uses its own volume (
electrum-tor-data), kept separate from standard mode. :tmeans TCP — Tor provides the encryption, so SSL is not needed.
Using the bot
A persistent keyboard sits at the bottom of the chat.
| Button | What it does |
|---|---|
| Add Wallet | Import a watch-only wallet: name → xpub → password. Your xpub message is deleted once read. |
| List | Your wallets. |
| Load | Activate a wallet (asks for its password). Needed before Balance / Receive / Send. |
| Balance | Confirmed and unconfirmed balance. Inline buttons switch BTC / mBTC / sats. |
| Receive | A fresh unused receiving address. |
| Send | Builds an unsigned transaction — see below. |
| Close | Unload the active wallet from the daemon. |
| 🌐 Language | English ⇄ Persian. |
Wallet names may use letters, numbers, spaces, dot, dash and underscore (1–64 characters) and must start with a letter or number. Passwords need at least 8 characters with an uppercase letter, a lowercase letter and a digit.
Sending
- Send → choose a fee source: Electrum (its own estimate) or mempool.space (live)
- Enter the fee rate in sat/vB
- Enter the wallet password (message deleted immediately)
- Enter the recipient address
- Enter the amount, or tap Send Max
- You get the unsigned transaction as both a code block and a
.txtfile - Sign it offline — Electrum Android: Send → Paste from clipboard; Electrum desktop: Send → tools icon → Read text from file
- Send → Broadcast, paste the signed transaction
Backup
Everything — wallets, config, blockchain headers — is in one volume.
# Quick deploy
docker run --rm -v telegtrum-data:/data -v "$PWD":/backup debian:bookworm-slim \
tar czf /backup/telegtrum-backup.tar.gz -C /data .
| Mode | Volume |
|---|---|
| Quick deploy | telegtrum-data |
| Build from source | electrum-data |
| Tor | electrum-tor-data |
| Local / Umbrel | ./electrum-data (bind mount) |
A backup is not a substitute for your seed phrase. These are watch-only wallets — the seed on your offline device is what actually recovers your coins.
Troubleshooting
Bot not responding — docker compose logs -f. Check the token, and that the container isn't restarting.
"You are not authorized" — your Telegram ID isn't in ALLOWED_USERS or allowed_users.txt. Add it and restart.
Everyone is refused — the container logs no allowed users configured at startup when both sources are empty.
"Wallet not loaded" — press Load first; Balance, Receive and Send all need an active wallet.
Invalid wallet name — stick to letters, numbers, spaces, dot, dash, underscore.
Daemon won't start — the entrypoint clears the stale lockfile each boot. If it persists, docker compose restart.
Tor: not connecting — bootstrap can take ~30s on first run.
Verifying what you run
The image builds Electrum from the official source tarball and verifies its GPG signature against pinned release-key fingerprints (ThomasV, sombernight, Emzy). If the signature is missing or the fingerprint doesn't match, the build fails rather than shipping an unverified wallet.
Reproduce it yourself with ./push.sh — or read install.sh, it's 60 lines.