Balances
The call
import { b1000 } from "@agntn/puzzles/collections/b1000";
const balance = await b1000.require(71).balance();
balance.chain; // "bitcoin"
balance.confirmed; // 710022600n, base units
balance.unconfirmed; // 0n, a signed mempool delta when the provider reports one
balance.total(); // 710022600n
balance.totalUnits(); // 7.100226
balance.decimals; // 8, from @agntn/chains
Every puzzle answers balance(). Collections forward balance(query) and balanceById(id) to it. The providers come from @agntn/explorers and load on the first call, so importing the package carries no network code.
| Chain | Provider | Needs |
|---|---|---|
| Bitcoin, Litecoin | Mempool, mempool.space and litecoinspace.org | nothing |
| Ethereum | Etherscan V2 | an API key |
| Decred | Dcrdata | nothing, and patience: dcrdata answers a fresh address in 6 to 11 seconds |
| Arweave | the arweave.net gateway | nothing |
| Monero | none | rejects with UnsupportedChainError before any request |
Options
await puzzle.balance({
apiKey: "your-etherscan-key",
baseUrl: "https://mempool.example", // another instance of the same provider
timeout: 30_000, // milliseconds, the providers default to 15 seconds
});
That's the whole options object. There's no fetch to inject. The providers read globalThis.fetch when they run, so a test stubs the global and a runtime brings its own. Ethereum without a key rejects with BalanceProviderError before it touches the network. Etherscan would reject it anyway, just slower.
Errors
import {
BalanceError,
BalanceProviderError,
InvalidAddressError,
UnsupportedChainError,
} from "@agntn/puzzles";
All three descend from BalanceError, which descends from PuzzlesError. InvalidAddressError is a provider or @agntn/chains saying the address doesn't fit. UnsupportedChainError is Monero. BalanceProviderError is everything else: a transport failure, a not numeric balance, a rejected key. An API key never appears in an error message. It's replaced with REDACTED, and the original error isn't attached as cause, so it can't leak through a logger either.
CLI and tools
puzzles balance b1000/71 # b1000/71: 7.100226 BTC
puzzles balance arweave/weave11 --api-key … # Ethereum, or set ETHERSCAN_API_KEY
puzzles balance b1000/71 --json # { chain, confirmed, unconfirmed, decimals }
puzzles_balance is the same call for a model. It's the only tool annotated as open world, because it's the only one that reaches beyond the bundled dataset.
On this site
Every puzzle page and the playground show a live balance. The browser doesn't call the explorers itself. The docs worker does, through /api/balance/<id>, with the same puzzle.balance() call and its own Etherscan key. Each answer is cached for five minutes, so a busy page is one request to mempool.space, not a thousand. A failure comes back as the library's own message, Balance lookup failed: …, under the same status code the tool would set isError for.