COLDCARD vulnerability what happened, and what to do
Informational only, and this site never asks for your recovery words. details

Informational only. This is independent analysis and an evidence-backed explainer, not financial, security or legal advice, and not a substitute for professional advice about your own situation. It is not affiliated with, endorsed by, or speaking for Coinkite, Block, or any other party named here. Published estimates are attributed, and differing scenarios are kept separate with their assumptions. Act on your own judgement. Editorial standards and corrections.

Do not disclose recovery material to a website, form, message or support account. This site never asks for it. Deliberate recovery on independently verified offline equipment is a separate operation. Seed-word safety.

Chain-derived COLDCARD RNG postmortem

kelbie-rng-postmortem

https://raw.githubusercontent.com/Kelbie/coldcard-rng-postmortem/main/README.md

Organisation
Kelbie
Evidence role
Independent primary analysis
Published
2026-07-31
Source changes
2
Detected differences
2
Unreviewed
0
Copies held
3

Rapidly updated independent postmortem built from frozen chain data and public reports. It documents its derivation and attribution rules, but its wave grouping, entity attribution and counterfactual conclusions remain the author's analysis rather than facts established by the chain alone.

Every check is recorded, including checks that found no text change. A detected edit is therefore bounded between two checks. The publisher's exact save time is not observable from this record. Last checked 2 Aug 2026, 00:59 UTC.

  1. source content difference between 1 Aug 2026, 16:07 UTC and 1 Aug 2026, 17:12 UTC Current source content +43 -3

    The README added derivation sections on grouping waves into families by build traits rather than configuration dials, on fee pricing and its lack of correlation with value, on wave colour, and on re-applying Block's published seven-property fingerprint across the whole record, plus a new fingerprint script in the file map.

    seen 1 Aug 2026, 17:12 UTC · Captured here text sha256 ac8e83750aeffd96d8361a16 15,900 chars
    What changed from the previous capture 46 lines
       pooled with whom; episodes say what happened at the same time as what, which is what the headline
       "X BTC in Y minutes" is a claim about. The threshold is not tuned: the longest quiet inside the
       30 July burst is 22 minutes and the next sweeping is more than a day later, so anything between
    -  those gives the same answer. The headline figures and the block cascade read the first episode;
    -  everything else carries all of it.
    +  those gives the same answer. The headline figures read the first episode; everything else carries
    +  all of it.
     The two graphs both had to grow something to show a wave that arrived a day late and never pooled,
     rather than being scoped away:
     - The **clock replay** runs on plain linear time: one second of the axis is one second of 30 and 31
       rather than repeating a timestamp; the handful pinned to a post use the `ANCHOR` constants in
       `timeline.tsx`, written once and shared with the entry itself. A finding with no anchor is about
       the present, and renders under the money flow instead.
    +- **Which waves were built by the same program** — `fingerprints[]` splits what a transaction
    +  reveals into two kinds of evidence and groups the waves on the first kind only. A **build** trait
    +  is something the program does regardless of who runs it: the order it writes inputs in, where it
    +  gets a size estimate from, what it puts in nLockTime, whether it grinds its signatures. A
    +  **config** trait is a dial — fee rate, replace-by-fee, whether coins are pooled into a collector
    +  or scattered one address per sweep. Waves form a **family** when no build trait *either could
    +  show* disagrees, and split into **variants** inside it by their dials. Traits a wave cannot
    +  exhibit — input ordering, when every sweep spends one input — come back null and are skipped, so
    +  a small wave joins on the evidence it has rather than being split off for evidence it could never
    +  have produced. Where that leaves it fitting more than one family, `alsoFits` says so and the page
    +  prints it as an open question rather than a placement.
    +  A family is a claim about software and nothing more. Two people can run one program; one person
    +  can run two. Nothing here converts a shared build into a shared operator.
    +- **The fee** — priced from a size table, and from nothing else. It is not a share of what was
    +  taken: inside wave 960345 alone the fee runs from a ninety-thousandth of the value to nineteen
    +  times it, a span of 1.7 million to one, and across the record 27 sweeps paid more to move the
    +  coins than the coins were worth. Nor does it track size or position in the run. Where a wave uses more than one rate, the split is resolved rather than
    +  reported as a range, because the three cases are different findings: wave 960345 pays 50 sat/vB on
    +  all 1,214 of its single-address sweeps and 8 on both of its two multi-address sweeps, so there the
    +  rate is bound to the code path; wave 960352 has one stray sweep; wave 960518 uses nine rates
    +  across sixteen sweeps, whose strongest correlation with the value taken, the size signed or the
    +  order it went out in is r = −0.38 — a number the derivation measures rather than a "tracks
    +  nothing" this file asserts.
    +- **Wave colour** — read from exactly those two numbers: hue is the family, shade is the variant.
    +  Colour used to be the running order, which made the palette a second time axis the page already
    +  had three of, and left the one thing colour could have said unsaid. Hue is an identity rather than
    +  a distance — families take bands in the order they open, so refreshing the data cannot repaint a
    +  wave that has not changed, and a new program appends rather than shuffling. How *close* two
    +  families are is a question the trait table answers and a colour wheel cannot.
    +- **Block's published fingerprint, re-applied** — Block scanned 888,661 transactions for seven
    +  shared properties: version 2, locktime 0, final sequence on every input, one output and inputs
    +  from one source address, a P2WPKH destination, one homogeneous supported input type and a
    +  30 sat/vB pre-signing estimate. Those seven are re-run here against every wave. They match the
    +  three waves they were derived from and miss half the record, because three of the seven — final
    +  sequence, one source address, and the 30 sat/vB constant — are dials rather than build traits. An
    +  operator who turns the fee down or enables RBF drops out of that scan while running the same
    +  program, which is what happened the next morning. Their published caveat was that the matches
    +  might not be related to the drain at all; this is the opposite problem, and it is the argument
    +  that every total here is a floor.
     - **The signing fingerprint** — `signing[]` in `chain.json` records, per wave, what the software
       chose rather than what the chain forced: the locktimes, the input sequences, how many signatures
       carry a 33-byte R, and the fee rate *solved for* rather than measured. The last one is the useful
     ```
     scripts/fetch-chain.ts   pulls mempool.space → src/data/chain.json
     scripts/lib/discover.ts  derives the attacker set from the evidence
    +scripts/lib/fingerprint.ts  groups the waves by what built them
     scripts/lib/attribution.ts  builds the attribution case from the result
     src/data/evidence.ts     victim reports + published addresses — the only input
     src/data/schema.ts       types for the frozen dataset
     src/data/chain.json      generated; the only source of numbers on the page
     src/data/timeline.tsx    curated events, verbatim quotes, sourcing notes
     src/components/          Social (X + Reddit chrome), SweepReplay, MoneyFlow, Attribution,
    -                         VictimLedger, RootCause, Forensics, Timeline
    +                         Fingerprint, VictimLedger, RootCause, Forensics, Timeline
     ```
     ## Sourcing notes
     Tweet timestamps are decoded from the snowflake ID — `ts_ms = (id >> 22) + 1288834974657` — which is
    
    Extracted text as captured
    # COLDCARD RNG — an ongoing postmortem
    A postmortem of the COLDCARD (Coinkite) random-number-generator flaw: five years of predictable wallet
    seeds, the 1,082 BTC swept across three waves on 30 July 2026, and the higher-entropy tiers still
    exposed. Reconstructed
    from primary sources — the Bitcoin blockchain, the vendor and Block advisories, and the original posts
    as they were made.
    **This documents a live situation.** The 30 July waves took the cheapest-to-crack tier. The same
    defect leaves ~72-bit Mk4/Q/Mk5 seeds reachable with more compute, Block has reported a second earlier
    tranche of ~488 BTC it declined to attribute — traced here to two earlier waves that reconcile to the
    satoshi — and no firmware update can repair a seed that already exists. The sweeping also resumed on
    31 July, hours after the advisories, with different tooling and no pooling, and is in the ledger here
    only because one victim photographed his wallet history. Every figure here is a floor, not a total — re-run the fetch scripts to refresh it.
    Everything on the page is either derived from chain data frozen into the repository or quoted verbatim
    with a source link. Nothing calls an API at runtime.
    ## Running it
    ```bash
    bun install
    bun dev          # http://localhost:3000
    ```
    ```bash
    bun run build      # static bundle to dist/
    bun start          # production server
    bun run typecheck
    ```
    ## Refreshing the data
    Two generators, both writing into `src/data/`. Run them after any change to the address registry or
    the media list:
    ```bash
    bun run fetch     # transactions → src/data/chain.json
    bun run media     # avatars + screenshots → src/data/media.json
    bun run commits   # GitHub commit metadata → src/data/commits.json
    ```
    `fetch` caches raw API responses under `.cache/`, so re-running after a change to
    classification or rollup logic costs nothing. Set `NO_CACHE=1` to force a fresh pull, or
    `PAGE_DELAY_MS` to back off harder against the public explorer's rate limit.
    ### Chain data
    `src/data/chain.json` is generated, not hand-written. Point the fetcher at your own node instead of
    the public API:
    ```bash
    MEMPOOL_API=https://your.node/api bun run fetch

    Excerpt only. The complete copy is held offline and backs quotations on this site. The original publication remains the canonical public source.

  2. source content difference between 1 Aug 2026, 05:59 UTC and 1 Aug 2026, 16:07 UTC source content +4 -4

    The README renamed waves from ordinal numbers to block heights throughout, so 'wave 3' became 'wave 960188' and Block's 'waves 1 and 2' became 'waves 960183 and 960185'.

    seen 1 Aug 2026, 16:07 UTC · Captured here text sha256 cb5ada8b2fdb9115c43164a1 12,208 chars
    What changed from the previous capture 8 lines
     }
     ```
     One report is enough to derive a collector, its vault, and every other victim that paid into it.
    -The first report in the file yields wave 3's collector, wave 3's vault, and 500 victims.
    +The first report in the file yields wave 960188's collector, its vault, and 500 victims.
     `txids` is a list because a wallet emptied one UTXO at a time is one event with one transaction per
     UTXO, and splitting that across several reports would lose the fact that they belong together. The
     grouping matters: it is what stops an operator who never pools from being derived as one wave per
     |---|---|
     | `attacker-controlled` | the source names it as part of the theft |
     | `pattern-match-unconfirmed` | the source found it by fingerprint and said it was unconfirmed |
    -Block published waves 1 and 2 as a fingerprint match and said in the same thread that they "have not
    +Block published waves 960183 and 960185 as a fingerprint match and said in the same thread that they "have not
     confirmed that these indeed are related to drain". That caveat survives into the UI rather than being
     flattened into "attacker address".
     Then `bun run fetch`. Everything downstream rebuilds: the flow diagram, the replay, the ledger, the
       the total would otherwise be seven identical hairlines under a column of overlapping labels. The
       lane ends in "not consolidated" instead of a vault, which is the third column telling the truth
       rather than drawing a vault that holds nothing.
    -- **Labels** — "Wave 1 collector A" is generated from the wave and role, with a letter only where
    +- **Labels** — "Wave 960183 collector A" is generated from the wave and role, with a letter only where
       there is more than one.
     - **Justifications** — the sentence explaining why an address is in the set is emitted by the same
       code that found it, so the two cannot drift apart.
     | `sweep` | external inputs paying a tracked address — stolen funds |
     | `consolidation` | inputs come only from tracked addresses — the operator pooling their own outputs |
     | `graffiti` | inbound value arriving *after* that wave consolidated — onlookers sending dust to a now-public address |
    -The last one is decided by **time, not amount**. Wave 1 swept outputs worth 1,200 satoshis — smaller
    +The last one is decided by **time, not amount**. Wave 960183 swept outputs worth 1,200 satoshis — smaller
     than the dust strangers later sent for fun — so any size threshold misfiles real thefts.
     Totals are reconciled against the figures other people published, at the precision they published
     them. If they ever stop agreeing, the table says `DRIFT` and the fetch warns, rather than quietly
    
    Extracted text as captured
    # COLDCARD RNG — an ongoing postmortem
    A postmortem of the COLDCARD (Coinkite) random-number-generator flaw: five years of predictable wallet
    seeds, the 1,082 BTC swept across three waves on 30 July 2026, and the higher-entropy tiers still
    exposed. Reconstructed
    from primary sources — the Bitcoin blockchain, the vendor and Block advisories, and the original posts
    as they were made.
    **This documents a live situation.** The 30 July waves took the cheapest-to-crack tier. The same
    defect leaves ~72-bit Mk4/Q/Mk5 seeds reachable with more compute, Block has reported a second earlier
    tranche of ~488 BTC it declined to attribute — traced here to two earlier waves that reconcile to the
    satoshi — and no firmware update can repair a seed that already exists. The sweeping also resumed on
    31 July, hours after the advisories, with different tooling and no pooling, and is in the ledger here
    only because one victim photographed his wallet history. Every figure here is a floor, not a total — re-run the fetch scripts to refresh it.
    Everything on the page is either derived from chain data frozen into the repository or quoted verbatim
    with a source link. Nothing calls an API at runtime.
    ## Running it
    ```bash
    bun install
    bun dev          # http://localhost:3000
    ```
    ```bash
    bun run build      # static bundle to dist/
    bun start          # production server
    bun run typecheck
    ```
    ## Refreshing the data
    Two generators, both writing into `src/data/`. Run them after any change to the address registry or
    the media list:
    ```bash
    bun run fetch     # transactions → src/data/chain.json
    bun run media     # avatars + screenshots → src/data/media.json
    bun run commits   # GitHub commit metadata → src/data/commits.json
    ```
    `fetch` caches raw API responses under `.cache/`, so re-running after a change to
    classification or rollup logic costs nothing. Set `NO_CACHE=1` to force a fresh pull, or
    `PAGE_DELAY_MS` to back off harder against the public explorer's rate limit.
    ### Chain data
    `src/data/chain.json` is generated, not hand-written. Point the fetcher at your own node instead of
    the public API:
    ```bash
    MEMPOOL_API=https://your.node/api bun run fetch

    Excerpt only. The complete copy is held offline and backs quotations on this site. The original publication remains the canonical public source.

  3. Earliest copy held
    seen 1 Aug 2026, 05:59 UTC · Captured here text sha256 4097db7aec2c29d391d770c1 12,188 chars
    Extracted text as captured
    # COLDCARD RNG — an ongoing postmortem
    A postmortem of the COLDCARD (Coinkite) random-number-generator flaw: five years of predictable wallet
    seeds, the 1,082 BTC swept across three waves on 30 July 2026, and the higher-entropy tiers still
    exposed. Reconstructed
    from primary sources — the Bitcoin blockchain, the vendor and Block advisories, and the original posts
    as they were made.
    **This documents a live situation.** The 30 July waves took the cheapest-to-crack tier. The same
    defect leaves ~72-bit Mk4/Q/Mk5 seeds reachable with more compute, Block has reported a second earlier
    tranche of ~488 BTC it declined to attribute — traced here to two earlier waves that reconcile to the
    satoshi — and no firmware update can repair a seed that already exists. The sweeping also resumed on
    31 July, hours after the advisories, with different tooling and no pooling, and is in the ledger here
    only because one victim photographed his wallet history. Every figure here is a floor, not a total — re-run the fetch scripts to refresh it.
    Everything on the page is either derived from chain data frozen into the repository or quoted verbatim
    with a source link. Nothing calls an API at runtime.
    ## Running it
    ```bash
    bun install
    bun dev          # http://localhost:3000
    ```
    ```bash
    bun run build      # static bundle to dist/
    bun start          # production server
    bun run typecheck
    ```
    ## Refreshing the data
    Two generators, both writing into `src/data/`. Run them after any change to the address registry or
    the media list:
    ```bash
    bun run fetch     # transactions → src/data/chain.json
    bun run media     # avatars + screenshots → src/data/media.json
    bun run commits   # GitHub commit metadata → src/data/commits.json
    ```
    `fetch` caches raw API responses under `.cache/`, so re-running after a change to
    classification or rollup logic costs nothing. Set `NO_CACHE=1` to force a fresh pull, or
    `PAGE_DELAY_MS` to back off harder against the public explorer's rate limit.
    ### Chain data
    `src/data/chain.json` is generated, not hand-written. Point the fetcher at your own node instead of
    the public API:
    ```bash
    MEMPOOL_API=https://your.node/api bun run fetch

    Excerpt only. The complete copy is held offline and backs quotations on this site. The original publication remains the canonical public source.

How to check this yourself

Each copy above is identified by the SHA-256 of its extracted text, shown beside it, and the diffs are plain unified diffs. To verify a quotation, compare it against the page itself or against the Internet Archive's copies, which are independent of this project.

Complete captures are held offline rather than mirrored here, so this page shows diffs and excerpts. If a quotation is ever disputed, the full copy can be produced. Ask.