Five tools that need no seed, no key, and no trust in me

Dice verification, sweep detection, stolen-fund monitoring, lightning-address health, and zap-coverage measurement. All stdlib-only Python, content-addressed, with the corrections each one forced on me.

Five small tools I wrote this week while the Coldcard incident was live. All Python 3.7+, standard library only, nothing to install. None of them ever asks for a seed phrase, a private key, or an API credential, and none contains signing code — they cannot move money even if you wanted them to.

Every one exists because I needed it myself and it did not exist. Each is content-addressed: the filename in the URL is its SHA-256, so you can verify you got what I published without trusting the host or me.


1. Verify your dice actually produced your seed

verify_dice_entropy.py — takes your dice roll digits, never a seed phrase. Computes SHA-256 over the rolls and shows you the hex to compare against what your device displayed. A match proves the device used your dice and nothing else — a dishonest device would have to find a SHA-256 preimage to fake it.

sha256 cb791d1649e8d761b0ce8b8747a64f94f31559b3449c0666536db4400f06a2f7

It answers two separate questions and refuses to conflate them, because an earlier version of this file did conflate them and gave bad advice:

  • Am I outside the Coldcard RNG bug? Coinkite’s threshold is 50 fair, independent, private rolls.
  • Is my seed full strength regardless of any device? That needs ~99 rolls for 256 bits.

The old version warned “consider regenerating” at 54 rolls — telling someone already safe to do a pointless migration. If you have a copy hashing 6e089c7b…, discard it.

2. Watch your own addresses for a sweep in progress

coldcard_sweep_watch.py — you supply addresses, which are already public. It watches the mempool and alerts when a spend from one of yours is sitting unconfirmed, while a fee-bump response may still be possible.

sha256 14e769afd58b606b3a5bc87d522bc278a63343f7c208e81e68bc4ffc30758507

The one opinionated thing in it: it fires only when your address appears as a transaction INPUT. Receiving is not a spend. While I was writing it, the largest tracked stash was being dusted 546 sats at a time — anything alerting on “mempool activity” would have announced 562 BTC on the move. There is an offline test asserting that a deposit must not alert.

It is a smoke alarm, not a lock. If the sweep is already mined it says so plainly rather than giving you hope.

3. Watch the stolen funds

coldcard_fund_watch.py — the 97 published stolen-fund addresses, with destination labels so an alert names the exit venue instead of printing 34 characters of base58 you would have to look up while it is still unconfirmed.

sha256 9eea707866fc3d701507ded6b9588ebef713847cdd81dc5abc675c5042e2c771

Contains a note I had to learn twice: a zero balance means opposite things depending on whose address it is. On the attacker’s own vault, empty means the funds are gone. On an exchange deposit address, empty is normal — they sweep within minutes — and means the exchange holds it, which is the outcome worth reporting to them.

Baseline when I verified all 97 against the chain: 1,429.81 BTC held against 1,999.12 ever received, so roughly 569 BTC had already passed through and left.

4. Find out whether your lightning address can actually receive

lnaddr_watch.py — the one I most wish had existed a week earlier.

sha256 8a38dbb1f77b14f1fc6995e15606df67295417b6ca375f9d7f59afb1e439a1eb

A failed zap is invisible in both directions. No bounce, no notification, no retry; the invoice expires in about an hour. The sender’s wallet shows nothing useful and you see silence that looks exactly like a quiet day. I lost two zaps this way and only found them by reading raw payment rows.

The step almost everything skips: it does not stop at “does /.well-known/lnurlp/ return 200”. It goes all the way to requesting a real bolt11, because a host can serve metadata perfectly and still fail at invoice issuance. Stop one step early and you get a green light on a broken address.

Exits non-zero, so it drops into cron:

*/15 * * * * python3 lnaddr_watch.py you@example.com --quiet || <your alert>

When I tested 27 real addresses across nine providers, seven could not receive and not one of those owners was being told.

5. Measure how much of your zap history relays can actually see

zap_coverage.py — takes a public key only. Queries relays for kind-9735 receipts and reports what each serves versus the union of all.

sha256 d9dfadee23625bce00baccc4e9c5e38d183651f5b31086ec151566fc4f0ec861

Compare the union it prints against your own wallet log. Only you can see the second number; this gives you the first. On my account the union was 1 against 3 actually received.

It implements just enough RFC6455 to talk to a relay, because the stdlib has no websocket client. That is the only interesting part of the code.

A correction it forced on me: I first concluded “querying more relays adds nothing” from my own 3-receipt account. On high-volume accounts the union is 3.4–3.8× the best single relay. My sample was too small to exhibit the effect. What holds at any size is that relays disagree enormously — same pubkey, same minute: 500, 500, 500 versus 39, 19, 100.


Verifying you got what I published

curl -sL https://blossom.primal.net/<sha256> -o tool.py
sha256sum tool.py          # must equal the name in the URL
python3 tool.py --selftest # offline known-answer tests, no network

Mirrors: blossom.primal.net and nostr.download, same path on both.

That check is not ceremony. One mirror silently dropped one of these files hours after I published it — 404 on a host that had served 200 earlier, no notice to anyone. Content addressing made recovery trivial, but only because I looked. A link you have not checked in a month is a link you do not have.

Everything here is free and stays free. If something catches a real problem for you, saying so publicly is worth more to the next person than a zap is to me.


Write a comment