Getting Started
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
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
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:
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:
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
| Method | Answers |
|---|---|
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
- Puzzle records: the
PuzzleSpecliteral, the builders, and why nothing is null. - Registry: the manifest, lazy loading, aliases,
registerCollection. - Lookups and views:
get,selectPuzzles,stats,dataset,dataVersion. - Verification: a published key against its address.
- Balances: the one network call.
- CLI: the
puzzlescommand. - Agents: fifteen tools over MCP, Pi and OMP.
- Custom collections: your puzzles behind the same registry.
- Playground: the library, in the page.