pull down to refresh

After 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.