Bitcoin xpub watchdog with transaction notifications
https://gist.github.com/m0wer/498490d77fd33b5a41eb6bd480c7fae8After the event of the last weeks, I wondered how long would it take to realize that you have been stolen from. Given that people don't manually check all of their balances daily (I guess). And that noticing fast, could help you save part of your stack (maybe).
So I made a tiny Bitcoin Core watchdog.
The idea is simple: import account-level xpubs into watch-only descriptor wallets and let Bitcoin Core notify a small Python script whenever something moves.
By default it alerts twice if appropriate: once when the transaction first appears in your node's mempool, and once when it gets confirmed.
That second alert is important because a transaction does not necessarily have to enter the public mempool. It could, for example, be submitted directly to a miner and first become visible to your node when the block arrives.
The script has no database and keeps no wallet state. Bitcoin Core does all the actual monitoring.
Install it:
```
sudo install -m 755 bitcoin_core_watchdog.py /usr/local/bin/bitcoin_core_watchdog.py
```
Store a Gotify application token somewhere readable by Bitcoin Core:
```
echo 'YOUR_GOTIFY_TOKEN' > /etc/bitcoin-watchdog.token
chmod 600 /etc/bitcoin-watchdog.token
```
Then add to `bitcoin.conf`:
```
keypool=5000
blockfilterindex=1
walletnotify=env GOTIFY_URL=
https://gotify.example.com GOTIFY_TOKEN_FILE=/etc/bitcoin-watchdog.token /usr/local/bin/bitcoin_core_watchdog.py notify %w %s %b %h
```
A self-hosted Gotify instance is ideal if privacy is the goal. The Gotify-specific part is isolated in `send_notification()`, so adapting it to ntfy, Pushover, email, Telegram, etc. should be straightforward.
Test notifications:
```
GOTIFY_URL=
https://gotify.example.com \
GOTIFY_TOKEN_FILE=/etc/bitcoin-watchdog.token \
bitcoin_core_watchdog.py test-gotify
```
Then add the accounts you want to watch.
Native SegWit/BIP84:
```
bitcoin_core_watchdog.py add cold-storage segwit 'zpub...' # or xpub... (also works)
```
Taproot/BIP86:
```
bitcoin_core_watchdog.py add taproot-vault taproot 'xpub...'
```
Don't put seed phrases or private keys on the monitoring node. Export an account-level xpub from the actual wallet and keep the Core wallets watch-only.
For an existing wallet, give Core an approximate birthday so it knows how far back to scan:
```
# get the epoch with `date -d "2026-01-01 00:00:00" +%s`
bitcoin_core_watchdog.py add cold-storage segwit 'zpub...' \
--timestamp 1767222000
```
Or use:
```
--timestamp 0
```
to scan the entire chain. New wallets default to `--timestamp now`.
The `add` command is only a convenience wrapper. You can do the same thing manually with `bitcoin-cli`: create a blank watch-only descriptor wallet with autoload enabled and import the appropriate descriptor.
A normal transaction might therefore produce:
```
Bitcoin mempool activity: cold-storage
sent 0.5 BTC · RBF
Unconfirmed / in local mempool
https://mempool.space/tx/...
```
followed by:
```
Bitcoin confirmed activity: cold-storage
sent 0.5 BTC
Confirmed at block 123456
https://mempool.space/tx/...
```
If the transaction bypasses your mempool and gets sent directly to a miner, you simply get the second notification.
The explorer link defaults to `
https://mempool.space/tx/<txid>`. That's not ideal for privacy, but the script never queries mempool.space. It only puts the URL in the notification, so nothing is sent there unless you actually click it.
If you run your own explorer, set:
```
MEMPOOL_URL=
https://mempool.example.com```
So the whole thing is basically:
Bitcoin Core → watch-only descriptor wallets → mempool/block detection → walletnotify → Gotify
No external address lookup service, no xpubs handed to a third party, and no private keys on the monitoring machine.
https://stacker.news/items/1551517