Guide

Getting Started

Install the package. Load a chain. Get a wallet out in five lines.
Package name, public API, the lazy loader, and the tool surface can all change before 1.0. Pin exact versions if you build on it now.This code has never had a security audit. Do not use it in production, with real funds or with sensitive data. It is meant for agents, puzzles and local experiments only. It comes as is, without warranty of any kind, and the authors are not liable for any loss, as the MIT license states. Anything that matters wants an audited library.Every key that passes through the library, the explorer, or an MCP server is plaintext in a process, a log, or an agent transcript. Treat it as burned the moment it exists. Real money belongs on a hardware wallet.

Why this exists

Every chain has its own idea of a key, an address, and a signature. Bitcoin hashes twice and encodes in base58, Ethereum takes the tail of a Keccak hash, Solana just base58 encodes the public key, Cardano wants bech32 with a header byte. Pulling one library per chain into a project means five APIs, five option shapes, and five ways to get an address wrong.

@agntn/keys puts every chain it ships behind one class shape. Same calls, same option object, same wallet type. The chain decides the rules, you decide the key.

Install

shell
pnpm add @agntn/keys

Node.js 26 or newer. Pure JavaScript all the way down, nothing to compile, and no node: import anywhere, so a browser bundle needs no shim. The Keyspace explorer is exactly that bundle.

First wallet

jsmy-wallet.js
import { useBlockchain, blockchains } from "@agntn/keys";

// Concrete classes load on demand. First call takes options, second call imports and constructs.
const bitcoin = await blockchains.bitcoin()();
const ethereum = await blockchains.ethereum()();

const bitcoinChain = useBlockchain(bitcoin);
const ethereumChain = useBlockchain(ethereum);

const btcWallet = bitcoinChain.generateWallet();
const ethWallet = ethereumChain.generateWallet();

console.log(btcWallet.address); // 1...
console.log(ethWallet.address); // 0x...

The double call looks odd the first time. It's deliberate: blockchains.bitcoin({ network: "testnet" }) only records the options, and the second () is the moment the chain module actually gets imported. Nothing you never call ends up in your bundle.

What ships

ChainCurveAddresses
Bitcoinsecp256k1legacy, p2sh, segwit, p2wsh, taproot
Bitcoin Cashsecp256k1Bitcoin's P2PKH hash in CashAddr, starts with bitcoincash:q
Bitcoin Goldsecp256k1four of Bitcoin's five, with G, A and btg1q prefixes, no taproot
Bitcoin SVsecp256k1legacy P2PKH, the same 1 address Bitcoin gives
Litecoinsecp256k1the same five, with L, M and ltc1 prefixes
Dashsecp256k1legacy P2PKH, starts with X
Decredsecp256k1legacy ECDSA P2PKH, starts with Ds
Dogecoinsecp256k1legacy P2PKH, starts with D
Zcashsecp256k1transparent P2PKH, starts with t1
eCashsecp256k1Bitcoin's P2PKH hash in CashAddr, starts with ecash:q
Ethereumsecp256k1EIP-55 hex
Basesecp256k1EIP-55 hex, same as Ethereum
TRONsecp256k1base58check, starts with T
Solanaed25519base58 public key
Stellared25519G StrKey, base32 with a checksum
Aptosed255190x hex, SHA3-256
Suied25519 or secp256k10x hex, Blake2b
Cardanoed25519bech32 base, enterprise, stake
XRP Ledgersecp256k1 or ed25519classic r address, base58check in XRPL's alphabet
NEARed25519implicit account, the public key in hex
Cosmossecp256k1bech32 under the chain's prefix, cosmos by default
Polkadoted25519SS58 under the network prefix, 0 by default and 42 on testnet
Moneroed25519standard, subaddress and integrated, both public keys in block base58

Each chain has its own page under Blockchains with the exact rules and the gotchas. Three of them refuse deriveHDWallet on purpose, Decred, Cardano and Monero, and their pages say why.

The three steps

Every chain does the same three things, with different rules for each.

1. A private key is 32 random bytes

js
const privateKey = bitcoinChain.generateKeyPrivate();

A 64 character hex string. Both curves ask @noble/curves for it, randomSecretKey() on secp256k1 and on ed25519, and noble reads globalThis.crypto underneath. Same code in Node and in the browser.

2. The public key comes from the curve

js
const publicKey = bitcoinChain.getKeyPublic(privateKey);
const keys = bitcoinChain.generateKeys(); // both at once

secp256k1 chains give you a compressed 33 byte key by default. Pass { compressed: false } for the 65 byte one. ed25519 keys are always 32 bytes.

3. The address is a hash, then an encoding

js
const address = bitcoinChain.getAddress(publicKey);
const segwit = bitcoinChain.getAddress(publicKey, "segwit");

The second argument is the address type. Chains with one format ignore it, chains with several use it to pick, and Decred throws if you ask for one it doesn't have.

generateWallet() runs all three and hands back { keys: { private, public }, address }.

Checking an address

js
bitcoinChain.validateAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"); // true

Every built in chain validates its own formats, including checksums where the format has one. It's a format check, not a lookup: a valid address can still have zero history on chain.

Which curve am I on

js
bitcoinChain.curve; // 'secp256k1'
solanaChain.curve; // 'ed25519'
suiChain.curve; // ['ed25519', 'secp256k1']
xrpChain.curve; // ['secp256k1', 'ed25519']

Sui and the XRP Ledger answer with a list. Their pages say how to pick.

Agents

The same code runs as an MCP server over stdio, every tool from keys_electrum_wallet_derive through keys_bip44_generate:

shell
npx -y @agntn/keys mcp
pi install npm:@agntn/keys
omp install @agntn/keys
json
{
  "mcpServers": {
    "keys": { "command": "npx", "args": ["-y", "@agntn/keys", "mcp"] }
  }
}

Wallets, HD derivation, addresses, signing, BIP44 paths, mnemonics in all ten BIP39 lists, WIF both ways and public key conversion, with the same parameters the library takes. The Pi and OMP extensions in extensions run the exact same executors, installed from npm or from a checkout. A generated mnemonic comes back with a line saying it's in the transcript now. That line isn't decoration, it's the whole security model: public puzzle material and throwaway keys only.

Remote MCP

No Node on your side? This site runs the same tools over Streamable HTTP, at https://keys.agntn.dev/mcp. Give it to any MCP client that takes a URL.

shell
claude mcp add --transport http keys https://keys.agntn.dev/mcp
json
{
  "mcpServers": {
    "keys": { "type": "http", "url": "https://keys.agntn.dev/mcp" }
  }
}

It's the Docus MCP server with the key tools added to it. Not copies, though. toToolkitTools from @agntn/tools/toolkit hands Docus the very definitions keys mcp serves. So every call answers exactly like keys mcp, the refusals too. Misspell an option and both turn it down in the same words, with any sneaky bidi character flattened to a space. A new tool? It's here after the next deploy, since the site keeps no list of its own to forget it on. Two more come from Docus. list-pages lists the pages of this site and get-page returns one as Markdown.

Only one thing differs. A web page on another origin gets a 403. Clients that call from their own process, like Claude Code or Cursor, send no origin and get in.

The part that matters more: every argument goes to a Cloudflare worker. Nothing on the site logs or stores it, but that's a promise, not something you can check from your side. A private key sent here has left your machine, one hop further than the transcript. Same rule, so puzzle material and throwaway keys only. Want one hop less? npx -y @agntn/keys mcp runs the tools on your machine.