ID@agntn/chainsv0.5.0
Name a chain. Check an address.
Blockchains as classes, one file each: chain ID, CAIP-2, coin type, explorer. The spellings people actually type resolve to the same class, and every chain checks its own address by decoding it, not by counting characters. Nothing here touches a network.
- Chains
- 36
- 14 EVM · 12 UTXO
- Families
- 11
- 3 with a shared base
- Decoded
- 36 of 36
- 36 check a txid too
$ pnpm add @agntn/chains0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984
The Ethereum sample, run through every validator in the registry. 14 chains take the format. That narrows it to EVM, it doesn't say where the address is used.
matic, btc, arb. Same class every time
getChain("matic") hands you the Polygon class with everything on it, and create("polygon") does the same from the canonical key. Symbols stay out of the automatic index, six chains report ETH and the answer would depend on registration order. This file walks through 36 chains, every value read off the class in your browser.
- Keys name the chain, not the ticker: ethereum, bsc, octra. Tickers are aliases
- Display names round trip, so the name one tool prints resolves in the next call
- getChain('') throws. A blank string is a mistake, not a request for Ethereum
Resolve
import { getChain } from "@agntn/chains";// "eth" resolves to "ethereum"const chain = getChain("eth");chain.key; // "ethereum"chain.name; // "Ethereum"chain.symbol; // "ETH", 18 decimalschain.chainId; // "0x1"chain.caip2; // "eip155:1"chain.bip44; // 60chain.explorer; // "https://etherscan.io"Decode the bytes, not count the characters
A 34-character window takes a Bitcoin address, a TRON address and half the typos in between. So the validators decode. The panel checks the current sample against its own chain, against a chain with another format, and with one character changed. Where the format carries a checksum the last row fails on it, and each chain's page says which checks stay unverified.
- Base58 addresses are decoded: Solana wants 32 bytes, TRON 25 under 0x41, XRP its own alphabet
- Base58Check, Bech32, Bech32m, CashAddr and EIP-55 checksums are all verified, so one character off fails
- Rejected is an answer. InvalidAddressError carries the chain key and the address
accepted once, rejected 2×
evm / ETHcreate("ethereum").assertAddress(address)returned unchangedits own chain, Ethereum
create("bitcoin").assertAddress(address)InvalidAddressErrora chain with another format, Bitcoin
create("ethereum").assertAddress(typo)InvalidAddressErrorone character off: 0x1f9840a8…4201F985
One address, every validator at once
An address of unknown origin gets run through the whole registry. One family lights up for most chains, EVM for the 0x address that 14 chains share, Move for the short 0x1 that Aptos and Sui both write. A match narrows the family. It doesn't say the address is used there, and the tool text says that out loud.
- identify(address) partitions the registry: chains that accept the format, chains with no validator
- An EVM address matches all 14 EVM chains. That's the honest answer, not a bug
- No validator means unchecked, never a silent no. Right now every built-in chain has one
Families
- EVM14 / 14
- UTXO0 / 12
- Solana0 / 1
- Stellar0 / 1
- XRP Ledger0 / 1
- Move0 / 2
- TON0 / 1
- TRON0 / 1
- Octra0 / 1
- Arweave0 / 1
- Monero0 / 1
14 chains read this address as theirs. A format match narrows the family, it doesn't prove the address is used on any of them.
36 chains, 11 families
Each chain is a class in its own file with its own metadata. The family classes hold only what their members share: EVM and Move own the address format, everything else is declared per chain, coin type included. 12 UTXO chains, and a UTXO heritage doesn't mean Bitcoin's encoding: CashAddr, CIP-19, two version bytes on Decred, F4Jumble on Zcash. The page for a chain says which bytes are checked and which checksum is left alone.
Importimport { chains, create } from "@agntn/chains"
| Chain ID or CAIP-2 | ||||
|---|---|---|---|---|
| Ethereumethereum | ETH · 18 | EVM | 0x1 | coin 60 |
| Basebase | ETH · 18 | EVM | 0x2105 | coin 60 |
| Arbitrum Onearbitrum | ETH · 18 | EVM | 0xa4b1 | coin 60 |
| Optimismoptimism | ETH · 18 | EVM | 0xa | coin 60 |
| Polygon PoSpolygon | POL · 18 | EVM | 0x89 | coin 60 |
| BNB Chainbsc | BNB · 18 | EVM | 0x38 | coin 60 |
| Avalanche C-Chainavalanche | AVAX · 18 | EVM | 0xa86a | coin 60 |
| Fantom Operafantom | FTM · 18 | EVM | 0xfa | coin 60 |
| Gnosis Chaingnosis | xDAI · 18 | EVM | 0x64 | coin 60 |
| Linealinea | ETH · 18 | EVM | 0xe708 | coin 60 |
| zkSync Erazksync | ETH · 18 | EVM | 0x144 | coin 60 |
| Scrollscroll | ETH · 18 | EVM | 0x82750 | coin 60 |
| Berachainberachain | BERA · 18 | EVM | 0x138de | coin 60 |
| Bitcoinbitcoin | BTC · 8 | UTXO | bip122:000000000019d6689c085ae165831e93 | coin 0 |
| Litecoinlitecoin | LTC · 8 | UTXO | bip122:12a765e31ffd4059bada1e25190f6e98 | coin 2 |
| Pepecoinpepecoin | PEP · 8 | UTXO | bip122:37981c0c48b8d48965376c8a42ece9a0 | coin 3434 |
| eCashecash | XEC · 2 | UTXO | none registered | coin 899 |
| Cardanocardano | ADA · 6 | UTXO | cip34:1-764824073 | coin 1815 |
| Solanasolana | SOL · 9 | Solana | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp | coin 501 |
| Stellarstellar | XLM · 7 | Stellar | stellar:pubnet | coin 148 |
| XRP Ledgerxrpl | XRP · 6 | XRP Ledger | xrpl:0 | coin 144 |
| Aptosaptos | APT · 8 | Move | aptos:1 | coin 637 |
| Suisui | SUI · 9 | Move | sui:mainnet | coin 784 |
| TON (The Open Network)ton | TON · 9 | TON | ton:-239 | coin 607 |
| TRONtron | TRX · 6 | TRON | tron:0x2b6653dc | coin 195 |
| Octraoctra | OCT · 6 | Octra | none registered | no coin type |
| Arweavearweave | AR · 12 | Arweave | arweave:7wIU | coin 472 |
| Moneromonero | XMR · 12 | Monero | monero:418015bb9ae982a1975da7d79277c270 | coin 128 |
| Decreddecred | DCR · 8 | UTXO | none registered | coin 42 |
| Arcarc | USDC · 18 | EVM | 0x13b2 | coin 60 |
| Dogecoindogecoin | DOGE · 8 | UTXO | bip122:1a91e3dace36e2be3bf030a65679fe82 | coin 3 |
| Bitcoin Cashbitcoincash | BCH · 8 | UTXO | bip122:000000000000000000651ef99cb9fcbe | coin 145 |
| Bitcoin SVbitcoinsv | BSV · 8 | UTXO | none registered | coin 236 |
| Bitcoin Goldbitcoingold | BTG · 8 | UTXO | none registered | coin 156 |
| Dashdash | DASH · 8 | UTXO | bip122:00000ffd590b1485b3caadc19b22e637 | coin 5 |
| Zcashzcash | ZEC · 8 | UTXO | bip122:00040fe8ec8471911baa1db1266ea15d | coin 133 |
5 tools, three hosts
chains mcp serves the tools over stdio, the Pi and OMP extensions render them in the terminal. All three call the same executors, so they answer identically and a fix lands once. Absent fields say so out loud, bip44: none, because a missing coin type reads as not shown and invites a model to supply one from memory.
- chains_lookup, chains_address_validate, chains_txid_validate, chains_address_identify, chains_list
- The text carries the whole answer, and a miss names the keys that exist, so the next call has somewhere to go
- A rejected address or txid is an answer, not a tool error. Only an unknown chain, or one with no txid check, sets isError
Ethereum
The spelling a model typed, answered with every field and a plain none for what the chain doesn't have.
- key
- "ethereum"
- symbol
- ETH · 18 decimals
- caip2
- eip155:1
- bip44
- 60
- explorer
- etherscan.io
Extend Chain, call register
Every built-in is a concrete class extending the exported abstract Chain, or EVM when the format is Ethereum's. Yours is the same shape, one file. Throw InvalidAddressError with your key and the address, and identify treats your chain like any other. No plugin manifest.
- A static key, the metadata fields, and assertAddress when the format is known
- register(MyChain) puts it behind create, getChain, identify and has
- Nothing registers itself on import. The registry is one list, so a bundler can drop what you never touch
import { Chain, InvalidAddressError, register } from "@agntn/chains";import type { ChainKey, ChainType } from "@agntn/chains";const ADDRESS = /^nano_[13][13456789abcdefghijkmnopqrstuwxyz]{59}$/;class Nano extends Chain { static readonly key = "nano" as ChainKey; readonly type = "nano" as ChainType; readonly name = "Nano"; readonly symbol = "XNO"; readonly explorer = "https://nanexplorer.com/nano"; override assertAddress(address: string): string { if (!ADDRESS.test(address)) throw new InvalidAddressError(this.key, address); return address; }}register(Nano);Start with one command
One install gives you the library, the chains CLI and the MCP server. Hand it whatever spelling you have and get the class back, with the checks that go with it.
- PinPre-1.0, so pin exact versions.
- FormatA green check means the string fits the format. Not that it exists.
- OfflineNothing here talks to a node, an explorer or anything else.
First lookup
$ pnpm add @agntn/chainsimport { getChain, identify } from "@agntn/chains";const chain = getChain("btc");chain.caip2; // "bip122:000000000019d6689c085ae165831e93"chain.assertAddress("bc1qw508…v8f3t4"); // returned unchangedidentify("bc1qw508…v8f3t4").matches.length; // 1