Every puzzle, one record.

332 public crypto bounties and puzzles in 10 collections, 907.88 BTC of it still unclaimed. Each one is a typed record: address, key material, prize, what happened on chain. One library behind the CLI, an MCP server and the Pi and OMP extensions, with live balances from the explorers.

Data as code, one file per puzzle

There's no JSON to edit and no build step that generates data. Each puzzle is a TypeScript file with one literal in it, and the type checker reads it before any test does. The panel walks through 14 records from every collection, key material included, the way they sit on disk.

  • A record is a PuzzleSpec literal handed to a factory for its chain: bitcoinPuzzle, decredPuzzle, arweavePuzzle
  • Addresses, keys and transactions come from builders, so an absent field is absent, never null
  • Status is written down, not derived. A claim transaction plus a published key still means solved
Puzzle records
src/collections/b1000/1.ts10 fields
import { bitcoinPuzzle, compressed, hex, p2pkh, Status } from "@agntn/puzzles"; export const b1000Puzzle1 = bitcoinPuzzle({  id: "b1000/1",  address: p2pkh("1BgGZ9tcN4…26SAMH", "751e76e819…433bd6"),  sourceUrl: "https://bitcointalk.org/index.php?topic=521…",  startedAt: "2013-01-09 11:59:15",  status: Status.Solved,  prize: 0.001,  pubkey: compressed("0279be667e…f81798"),  key: hex("0000000000…000001", 1).wif("KwDiBf89Qg…VHnoWn"),  transactions: [ /* 2 recorded */ ],});

A key that derives its address, or a plain no

A published key is a claim, so the library checks it: derive the address, compare. The data gate runs the same check on every record, so a wrong hex digit fails the build instead of shipping. Here it runs on the current sample with the code the CLI ships.

  • verifyPuzzle reads the one secret a record exposes: hex, WIF, BIP38 payload, seed phrase or mini key
  • A solved puzzle with its key published derives the stored address, in your browser, secp256k1 and all
  • No key, an encrypted key, a mini key: unavailable is a verdict, not an exception
How verification works

puzzleb1000/1

secp256k1 · in your browser

  1. secretOf(puzzle.keyData())

    { kind: "hex" }

    a raw private key, verification derives the address from it

  2. puzzle.hasPubkey()

    true

    compressed 0279be667ef9dc…5b16f81798

  3. verifyPuzzle(puzzle)

    { verified: true, derivedAddress: "1BgGZ9tcN4…SZ26SAMH" }

    the derived address equals the stored one

A manifest of keys, records on first use

Importing the package evaluates no puzzle records. The registry is ten keys and ten import() calls. A bundler splits each collection into its own chunk. Only ever ask about b1000? You never download the other nine. This panel is that mechanism running: the walk pulls collections in one at a time.

  • collectionKeys() and hasCollection() answer from the manifest without loading anything
  • get('b1000/71') imports the b1000 module and nothing else; all() loads every collection once
  • registerCollection({ key, load }) adds yours, lazily too, and the aggregate views refresh
Registry and lazy loading

await get("b1000/1")

0 of 10 modules

  1. arweave manifest
  2. b1000 loading
  3. ballet manifest
  4. bitaps manifest
  5. bitimage manifest
  6. gsmg manifest
  7. hash_collision manifest
  8. rushwallet manifest
  9. warp manifest
  10. zden manifest
import("@agntn/puzzles") loaded no records. Every get(id) on this page imported one collection module, 0 of 10 so far, and the other 10 are still just keys in the manifest.

Ten collections, five chains, one page per puzzle

Each collection is a class with an author and its puzzle list, published on its own entry as @agntn/puzzles/collections/<key>. Every puzzle has a page with its record, its transactions, its key material and its live balance. The numbers here come from the library at build time.

  1. Arweave bounties12 puzzles · 4 open · 1900 AR, 1 ETH
  2. Bitcoin puzzle transaction256 puzzles · 77 open · 903.015076 BTC
  3. Ballet wallets3 puzzles · 2 open · 2.00007358 BTC
  4. Bitaps mnemonic challenge1 puzzles · 1 open · 1.00016404 BTC
  5. Bitimage2 puzzles · 1 open · 0.010019 BTC
  6. GSMG.io puzzle1 puzzles · 1 open · 1.25364181 BTC
  7. Hash collision bounties6 puzzles · 4 open · 0.59365 BTC
  8. RushWallet contest30 puzzles · 1 open
  9. WarpWallet challenges6 puzzles · 0 open
  10. Zden's puzzles15 puzzles · 2 open · 0.00933188 BTC
  11. Yours

Six tools, three hosts, one executor each

puzzles mcp serves the tools over stdio, the Pi and OMP extensions render them in the terminal. All three call the same functions, so they answer identically and a fix lands once. Only puzzles_balance reaches out to a block explorer, and it says so in its annotations.

  • puzzles_stats, puzzles_collections, puzzles_show, puzzles_list, puzzles_verify, puzzles_balance
  • The text carries the whole answer: address, status, prize, key range, explorer link
  • Limits live in one facts table and the executors enforce them, so a host that skips schema validation hits the same wall
MCP, Pi and OMP

toolpuzzles_show

MCP · Pi · OMP

input

{
  "id": "b1000/1"
}

output · content[0].text

b1000/1	solved	0.001 BTC	1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
chain: bitcoin  address kind: p2pkh
public key: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
private key known: yes
explorer: https://blockstream.info/address/1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH
source: https://bitcointalk.org/index.php?topic=5218972
key range: 1..1 (hex)

Extend Collection, register a loader

A collection outside the package is the same shape as one inside it: a class extending NamedCollection or NumericCollection with its puzzle list. Register a loader and the registry treats it like a built-in, including the part where nothing loads until someone asks.

  • A static key, an author from party(), and puzzles built with the same factories
  • registerCollection({ key, load }) keeps yours lazy; registerCollection(instance) is fine too
  • get, all, stats and the tools see it on the next call, and the same instance twice is a no-op
Custom collections
mine.ts
import { NamedCollection, bitcoinPuzzle, hex, p2pkh, party, registerCollection } from "@agntn/puzzles"; export const minePuzzleFirst = bitcoinPuzzle({  id: "mine/first",  address: p2pkh("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH"),  sourceUrl: "https://example.com/first",  startedAt: "2026-01-01 00:00:00",  key: hex("00…01", 1),}); export class MineCollection extends NamedCollection {  static readonly key = "mine";  static readonly author = party("you");  static readonly puzzles = [minePuzzleFirst];  constructor() {    super(MineCollection.key, MineCollection.author, MineCollection.puzzles);  }} // Lazy, like the built-ins: the module loads on the first get("mine/first").registerCollection({ key: "mine", load: () => import("./mine").then((m) => new m.MineCollection()) });

Start with one command

Pre-1.0, so pin exact versions. The records are public data about public puzzles. Every key in here was public before it got here. A balance is what the explorer said five minutes ago at most.

@agntn/puzzles·MIT license· Public data about public puzzles. Balances come from the chains' explorers through the worker, cached for five minutes. Every key here was public before it landed in a record.