Guide

Getting Started

Install the package. Load one puzzle. Read its address and its key range. Every puzzle is a record and every answer comes from a method on it.
The API, the CLI flags and the data model can still move. Pin exact versions if you build on it now.Every key in here was already public, usually posted by the solver. A record tells you what's known. It doesn't tell you whether the address still holds anything. That's a balance call, the only one that leaves the process.

Why this exists

The Bitcoin puzzle transaction. The WarpWallet challenges. Peter Todd's hash collision bounties. Zden's image puzzles. Each one has a thread somewhere with the addresses, the prizes, who solved what and when. And every scanner, tracker and bot re-types that data from the thread. Slightly differently each time.

So it lives here once, as code. A puzzle is a TypeScript record built by puzzle(). A collection is a list of them. The type checker reads the data before a test does. The library, the CLI, the MCP server and the Pi and OMP extensions read one registry. Fix a record once and it lands everywhere.

Install

shell
pnpm add @agntn/puzzles

Node.js 26 or newer for the CLI. The library is neutral ESM on the Fetch API, so browsers and edge workers run it too. The playground is it, running in your tab.

First call

tsfirst.ts
import { get, collectionKeys, hasCollection } from "@agntn/puzzles";

collectionKeys(); // ["80-bit", "bits", ...] straight from the manifest, nothing loaded
hasCollection("peter_todd"); // true, a historical alias for hash-collision

const puzzle = await get("bits/71"); // imports the bits module and nothing else

puzzle?.address().value; // "1PWo3JeB9jrGwfHDNpdGK54CRas7fsVzXU"
puzzle?.status(); // "unsolved"
puzzle?.prize(); // 7.10195986
puzzle?.prizeCurrency(); // "BTC"
puzzle?.keyRange(); // [2n ** 70n, 2n ** 71n - 1n]
puzzle?.hasPubkey(); // false, the address has never spent

get(id) loads the one collection the identifier names. Anything it doesn't know comes back as undefined. Want an exception instead? requirePuzzle(id) throws PuzzleNotFoundError. Identifiers are collection/name. The 26 singletons, such as gsmg, genesis and 80-bit, are just the key.

Importing the package evaluates no puzzle records. The registry is a manifest of 51 keys. The aggregate views are asynchronous because they load on first use:

ts
import { all, stats } from "@agntn/puzzles";

(await all()).length; // every puzzle, every collection loaded once
(await stats()).unsolved; // how many are still waiting for a key

One collection

A collection is its own entry with its class and its canonical instance. Once you have it, everything on it is synchronous:

ts
import { bits } from "@agntn/puzzles/collections/bits";

bits.count(); // every puzzle in bits
bits.require(90).address().value; // NumericCollection takes the number
bits.unsolved().length; // the addresses nobody has cracked
bits.author.name; // "saatoshi_rising"

What is on a puzzle

MethodAnswers
id(), collection(), name()bits/71, bits, 71
chain(), address()The chain key and { value, kind, hash160?, redeem_script?, witness_script? }
status()unsolved, solved, claimed, swept or expired. Written down, never derived
prize(), currency(), prizeCurrency()The amount, the currency when it isn't the native token, and the one to print
pubkey(), hasPubkey()The public key once the address exposed one
key(), keyData(), hasPrivateKey()The key builder, its serialized record, and whether any private key representation is known
hasDerivedKey()Whether the record rebuilt the key from a published recipe, since nobody printed it
keyRange()[2^(bits-1), 2^bits - 1] when the record declares a search width
transactions(), fundingTransaction(), claimTransaction()What happened on chain, in order
startedAt(), solvedAt(), solveTime(), formattedSolveTime()When, and how long it stood: 11y 6mo 16d 14h 13m
solver(), assets(), sourceUrl()Who, what files, and where the puzzle was announced
explorerUrl(), claimExplorerUrl()Links, built from @agntn/chains explorer bases
assetUrl(), assetLinks()The puzzle image's raw GitHub URL at the tag (or a checkout's commit), and every file with its path and URL
toJSON()The serialized record with every absent field left out
balance(options)The live balance through @agntn/explorers, the one method that touches a network

Errors

Everything thrown here descends from PuzzlesError. Catch one type, read fields, skip the message parsing. PuzzleNotFoundError is requirePuzzle on an id nobody has. UnknownCollectionError is requireCollection on a key outside the manifest. InvalidArgumentError is a tool argument outside its limits. Balances have their own family under BalanceError, on the balances page.

Next