A self-hosted Bitcoin wallet monitoring daemon — real-time Telegram alerts for mempool and confirmed transactions, BTC price tracking, and new block notifications.
- Overview
- Features
- Architecture
- File Structure
- Installation (Docker)
- Configuration
- Module Reference
- Data Files
- Security Notes
BitTrack is a self-hosted Node.js daemon designed to run on your own infrastructure (e.g., an Umbrel node). It monitors Bitcoin wallets defined by output descriptors by connecting to a local Fulcrum or ElectrumX server via TCP/TLS. When a transaction involving a tracked address appears in the mempool or gets confirmed, BitTrack sends a Telegram notification instantly.
A lightweight web panel (panel.html) is served on port 8585 for configuration, wallet management, and status monitoring — no external dependencies or databases required.
- 🔍 Wallet monitoring via Bitcoin output descriptors (xpub, ypub, zpub, Ypub, Zpub, Taproot, Miniscript,
addr()) - ⚡ Mempool alerts — notified the moment a transaction enters the mempool
- ✅ Confirmation alerts — notified when a transaction is confirmed on-chain
- 📦 New block notifications (optional, configurable interval)
- 💵 BTC price tracking with configurable alert threshold (supports CoinGecko, Binance, Blockchain.info)
- 🔑 Descriptor parsing with zero external npm dependencies — pure Node.js implementation of BIP32, BIP380/BIP389, secp256k1, Base58Check, Bech32/Bech32m
- 🌐 Web panel for full configuration, wallet management, and live status
- 🔄 Hot-reload — changes to
config.jsonandwallets.jsonare applied without restarting the process - 🐳 Docker-ready — single
docker compose up -d --buildto run - 📈 Automatic gap limit scanning for both external (chain 0) and change (chain 1) addresses
- 🔌 TCP/TLS connection to Fulcrum/ElectrumX with automatic reconnection
┌──────────────────────────────────────────────────────┐
│ BitTrack.js │
│ (unified entry point — runs both below) │
│ │
│ ┌─────────────────────┐ ┌────────────────────────┐ │
│ │ panel.js │ │ monitor.js │ │
│ │ HTTP server :8585 │ │ Electrum subscriber │ │
│ │ Config / Wallets │ │ Gap-limit derivation │ │
│ │ Auth / REST API │ │ Tx classification │ │
│ └──────────┬──────────┘ └──────────┬─────────────┘ │
│ │ │ │
│ └──────────┬─────────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ descriptor- │ │
│ │ parser.js │ │
│ │ (BIP32/380/389, │ │
│ │ secp256k1, │ │
│ │ Miniscript) │ │
│ └────────────────────┘ │
└──────────────────────────────────────────────────────┘
│ │
▼ ▼
Telegram Bot API Fulcrum / ElectrumX
(notifications) blockchain.scripthash
.subscribe (TCP/TLS)
Both panel.js and monitor.js run in the same Node.js process, with prefixed console output ([PANEL] / [MONITOR]) for easy log reading.
BitTrack/
├── BitTrack.js # Entry point — starts panel.js and monitor.js together
├── monitor.js # Core daemon — Electrum connection, subscriptions, Telegram alerts
├── descriptor-parser.js # Output descriptor parser and address derivation engine
├── panel.js # HTTP server for the web panel (no Express dependency)
├── panel.html # Web UI dashboard
├── Dockerfile # Docker image (node:20-alpine)
├── docker-compose.yml # Docker Compose deployment
├── descriptors-test.txt # Sample descriptors for CLI testing
└── data/
├── config.json # Main configuration (Electrum, Telegram, notifications)
├── wallets.json # List of monitored wallets and their descriptors
├── state.json # Persisted address states (balances, txids, status hashes)
├── servers.json # Saved Electrum server list for the panel UI
├── panel-auth.json # Panel password hash (SHA-256 + salt)
└── runtime.json # Volatile runtime data (price, Electrum status, UI lock)
- Docker Engine 20.10+
- Docker Compose v2+
- A running Fulcrum or ElectrumX instance reachable from the container
- A Telegram Bot Token (from @BotFather)
- Your Telegram Chat ID
git clone https://github.com/maverick-2009/BitTrack.git
cd BitTrackThe data/ directory ships with example config files that are copied into the container by the Dockerfile. You can edit them before building, or configure everything through the web panel after startup.
docker compose up -d --buildCheck logs:
docker compose logs -f bittrackhttp://<your-server-ip>:8585
Default password: admin
Change it immediately in Settings → Panel Password.
In the panel go to Settings and fill in:
- Electrum server — host, port, and TLS toggle. Use Test Connection to verify.
- Telegram — bot token and chat ID. Use Send Test Message to verify.
The monitor will automatically (re)connect once valid credentials are saved — no restart needed.
In the panel go to Wallets → Add Wallet. Paste your output descriptor. Examples:
wpkh([xxxxxxxx/84'/0'/0']xpub.../<0;1>/*)
wsh(multi(2,[fp1/48'/0'/0'/2']Zpub.../<0;1>/*,[fp2/48'/0'/0'/2']Zpub.../<0;1>/*))
addr(bc1q...)
The panel normalizes the descriptor, previews the derived addresses, and saves to wallets.json. The monitor picks up the change via hot-reload and starts subscribing within 3 seconds.
git pull
docker compose up -d --builddocker compose downAll configuration lives in data/config.json. The monitor reads it from disk on every operation — changes made in the panel take effect without a restart.
| Field | Default | Description |
|---|---|---|
host |
"" |
IP or hostname of your Fulcrum/ElectrumX server |
port |
50002 |
TCP port (50001 = plain, 50002 = TLS) |
tls |
false |
Enable TLS (self-signed certs accepted) |
name |
"Local" |
Display name in the panel |
| Field | Default | Description |
|---|---|---|
token |
"" |
Bot token from @BotFather |
chatId |
"" |
Target chat ID for notifications |
| Field | Default | Description |
|---|---|---|
panelPort |
8585 |
HTTP port for the web panel |
mempoolExplorer |
"https://mempool.space/" |
Base URL for transaction links in alerts |
currency |
"USD" |
Display currency for price info |
timezone |
"America/Sao_Paulo" |
Timezone for timestamps in logs and alerts |
dateLocale |
"pt-BR" |
Locale for date formatting |
| Field | Default | Description |
|---|---|---|
gapLimit |
10 |
Consecutive empty addresses before stopping derivation |
maxIndex |
1000 |
Hard cap on address index to prevent runaway derivation |
pingIntervalSec |
60 |
Keepalive ping interval to the Electrum server |
subscribeDelayMs |
0 |
Delay between consecutive subscribe calls (rate limiting) |
priceApis |
["coingecko","binance","blockchain.info"] |
Price source priority list |
priceCheckIntervalSec |
300 |
How often to fetch the BTC price |
priceRefMaxDeviationPct |
20 |
Max allowed deviation (%) from reference price before resetting it |
priceThresholdMinPct |
1 |
Minimum alert threshold in % |
priceThresholdMaxPct |
50 |
Maximum alert threshold in % |
| Field | Default | Description |
|---|---|---|
mempoolPending |
true |
Alert when a tx enters the mempool |
txConfirmed |
true |
Alert when a tx is confirmed |
everyBlock |
false |
Alert for every new block |
blockIntervalMin |
0 |
Alert only every N minutes (0 = every block) |
priceChange |
false |
Enable price change alerts |
priceThresholdPct |
1 |
Price must move this % from reference to trigger an alert |
priceReference |
null |
Reference price; null = use current price on startup |
Unified entry point. Loads panel.js and monitor.js in the same process and patches console.log/warn/error so that every log line is prefixed with [PANEL] or [MONITOR], preserving all timestamps and emoji from the original modules.
node BitTrack.jsCore monitoring daemon. Connects to Fulcrum/ElectrumX over TCP or TLS and manages all subscriptions. No npm dependencies — uses Node.js built-in net, tls, https, crypto, and fs.
| Feature | Description |
|---|---|
ensureConfig() |
Reads config.json on boot, fills missing fields with defaults, writes the merged result back |
loadConfig() / getCFG() |
Reads config.json from disk on every call — no in-memory cache, so panel changes are always reflected |
| Electrum connection | Persistent TCP/TLS connection using JSON-RPC newline-delimited protocol; auto-reconnects after 10 s on disconnect |
blockchain.scripthash.subscribe |
Subscribes every derived address; Electrum pushes a status hash whenever the address history changes |
| Gap limit scanning | Iterates external (chain 0) and change (chain 1) addresses per wallet, stopping after gapLimit consecutive unused addresses |
| Transaction classification | On a status change, fetches full tx history and raw hex, then classifies each new tx as incoming, outgoing, outgoing + change, or consolidation |
| Anti-duplicate | Every TXID is stored in state.json; the same transaction is never notified twice across restarts |
| Price monitoring | Polls CoinGecko, Binance, and Blockchain.info; uses a median with deviation guard; writes the result to runtime.json |
| Block notifications | Subscribes to blockchain.headers.subscribe; fires a Telegram alert per block or per configurable minute interval |
| Hot-reload (config) | Polls config.json every 3 s; on change, logs the diff and reconnects to Electrum only if server settings changed |
| Hot-reload (wallets) | Polls wallets.json every 3 s; adds new wallets by subscribing their addresses and removes purged wallets from memory and state.json |
runtime.json |
Writes real-time data (price, Electrum status, UI lock) for the panel to consume without a direct process link |
saveState() |
Persists the full address state map to state.json after every relevant change |
Pure Node.js implementation of Bitcoin descriptor parsing and address derivation — zero npm dependencies. Implements secp256k1 point arithmetic, BIP32 public key derivation, Base58Check, Bech32, and Bech32m entirely from scratch.
Supported descriptor types:
| Descriptor | Address type |
|---|---|
pk(...) |
P2PK |
pkh(...) |
P2PKH (Legacy, 1…) |
wpkh(...) |
P2WPKH (Native SegWit, bc1q…) |
sh(wpkh(...)) |
P2SH-P2WPKH (Wrapped SegWit, 3…) |
wsh(multi(...)) |
P2WSH Multisig |
wsh(sortedmulti(...)) |
P2WSH Sorted Multisig |
wsh(<miniscript>) |
P2WSH Miniscript (thresh, and_v, or_b, older, after, sha256, …) |
addr(<address>) |
Single static address (P2PKH, P2SH, P2WPKH, P2TR) |
Supported key prefixes: xpub, ypub, zpub, Ypub, Zpub, tpub, upub, vpub, Upub, Vpub — all normalized to xpub bytes internally for uniform derivation.
Key exports:
| Export | Description |
|---|---|
analyzeDescriptor(descriptor, { deriveSpec }) |
Main entry point. Parses the descriptor and derives addresses for the given deriveSpec ([{ index, chain }]). Returns { scriptType, miniscript, keys, addresses, errors } |
parseNode(descriptor) |
Parses a descriptor string into an AST node (recursive) |
parseKeyExpression(str) |
Parses a single key expression including fingerprint, origin path, and derivation path |
ExtendedKey |
BIP32 extended public key class with fromBase58() and derive(index) |
derivePublicKey(keyExpr, index, chain) |
Derives a compressed public key at a given index and chain from a key expression |
compileScript(node, index, chain) |
Compiles a parsed AST node into a Bitcoin script buffer |
scriptToAddress(compiled, hrp) |
Converts a compiled script to a Bitcoin address string |
scriptToScriptHash(scriptHex) |
Converts a script hex string to the reversed SHA-256 scripthash required by Electrum |
compileRedeemScript(node, index, chain) |
Compiles the inner redeem/witness script for P2SH and P2WSH descriptors |
CLI usage:
# Derive index 0 (default)
node descriptor-parser.js "wpkh([xxxxxxxx/84'/0'/0']xpub.../<0;1>/*)"
# Derive a range of indices
node descriptor-parser.js "wsh(multi(2,...))" --range 0 9
# Derive specific indices on the change chain
node descriptor-parser.js "wsh(...)" --index 0 1 5 --changeLightweight HTTP server built with Node.js built-ins only (http, https, net, tls, fs, crypto) — no Express or other npm frameworks.
Authentication: Session-token based. On login, the submitted password is hashed with SHA-256 + a static salt and compared against panel-auth.json. A 64-byte hex token is issued and must be sent as the X-Auth-Token header on all subsequent API requests. Default password: admin.
REST API:
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/login |
Authenticate; returns a session token |
POST |
/api/logout |
Invalidate the current session token |
POST |
/api/change-password |
Update the panel password |
GET |
/api/config |
Read config.json |
POST |
/api/config |
Write config.json; monitor picks up changes via hot-reload |
GET |
/api/wallets |
Read wallets.json |
POST |
/api/wallets |
Write wallets.json; sets a runtime lock while the monitor indexes new wallets |
GET |
/api/servers |
Read the saved Electrum server list |
POST |
/api/servers |
Save the Electrum server list |
GET |
/api/state |
Read state.json (per-address balances and txids) |
GET |
/api/price |
Read the current BTC price from runtime.json |
GET |
/api/server-status |
Read Electrum connection status from runtime.json |
POST |
/api/test-server |
Open a live TCP/TLS connection to test an Electrum server (server.version handshake) |
POST |
/api/test-telegram |
Send a test Telegram message to verify bot credentials |
POST |
/api/derive |
Normalize a descriptor and return derived addresses for preview |
POST |
/api/wallet-ready |
Poll whether the monitor has finished indexing a newly added wallet |
POST |
/api/gap-ready |
Poll whether a gap limit change has been fully applied to all wallets |
GET/POST |
/api/runtime-lock |
Read or set the UI overlay lock (used during long indexing operations) |
GET |
/ |
Serve panel.html |
Single-file web UI. Communicates with panel.js exclusively via the REST API above. All styling and JavaScript are self-contained — no external CDN dependencies.
| File | Description |
|---|---|
data/config.json |
All settings: Electrum, Telegram, general, monitor, notifications |
data/wallets.json |
Array of wallet objects: { name, descriptor, descriptorChange?, addresses[], startIndex? } |
data/state.json |
Per-wallet, per-address state: { balanceSat, txids[], mempoolTxids[], statusHash } |
data/servers.json |
Saved Electrum servers shown in the panel dropdown |
data/panel-auth.json |
SHA-256 password hash for panel login |
data/runtime.json |
Volatile data written by the monitor: price, Electrum status, UI lock |
Backup tip: copying the entire
data/directory is sufficient to fully restore BitTrack, including all wallet state and configuration.
The data/ directory is stored in a named Docker volume (data) defined in docker-compose.yml and persists across container rebuilds and updates.
- The web panel has no HTTPS by default. Place it behind a reverse proxy (IIS, Nginx, Caddy) with TLS before exposing it outside your local network.
- The default panel password is
admin— change it on first login. wallets.jsonis stored in plain text on disk. Secure access to thedata/Docker volume at the OS level.- BitTrack is read-only with respect to the Bitcoin network — it only subscribes to and reads data from Electrum, it never constructs or broadcasts transactions.
- The Electrum connection accepts self-signed TLS certificates (
rejectUnauthorized: false), which is appropriate for a local node but should be considered if pointing to a remote server.
MIT © maverick-2009