Getting Started
Why this exists
Every web3 library I write needs the same handful of facts: Polygon's chain ID, Bitcoin's coin type, which explorer to link, whether an address even looks right. Re-declared in every one of them, and slightly different each time. So they live here once, as classes, and everything else imports them.
The second reason is the address check. A regex that counts characters takes a Bitcoin address, a TRON address and half the typos in between. Here the validators decode: Solana wants 32 bytes behind the base58, TRON wants 25 under version 0x41, a bc1 address has to pass its Bech32 checksum. Rejected is an answer with the chain key on it.
Install
pnpm add @agntn/chains
First call
import { Ethereum, EVM, create, getChain } from "@agntn/chains";
const ethereum = create("ethereum");
ethereum instanceof Ethereum; // true
ethereum instanceof EVM; // true
ethereum.name; // "Ethereum"
ethereum.symbol; // "ETH"
ethereum.chainId; // "0x1"
ethereum.caip2; // "eip155:1"
// Aliases resolve to the same concrete classes.
const polygon = getChain("matic");
polygon.key; // "polygon"
// Validation lives on the class that knows the format.
ethereum.assertAddress("0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984");
create(key) wants the canonical key and builds a fresh instance. getChain(input) takes whatever spelling you're holding, a key, a display name or an alias like matic, and gives you the same class. With no argument it means Ethereum. With an empty string it throws, because a blank input is a mistake, not a request for the default.
What is on a chain
| Field | Type | Description |
|---|---|---|
key | ChainKey | Canonical class key, ethereum not eth |
name | string | Display name |
symbol | string | Native token symbol |
decimals | number? | Native currency decimal places |
type | ChainType | Family: evm, utxo, move, solana and so on |
bip44 | number? | BIP-44 / SLIP-0044 coin type |
chainId | string? | EVM chain ID in hexadecimal |
caip2 | string? | CAIP-2 identifier |
explorer | string | Block explorer base URL |
rpcDefault | string? | Default public RPC endpoint |
Optional fields stay empty when the chain has no registered value. Octra has no BIP-44 coin type and no CAIP-2 namespace, so both are undefined rather than invented. The full story of each field, decimals included, is in Metadata.
Errors
import { ChainsError, InvalidAddressError, UnsupportedChainError } from "@agntn/chains";
try {
getChain("btc").assertAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5");
} catch (error) {
if (error instanceof InvalidAddressError) {
error.chain; // "bitcoin", the canonical key
error.address; // the string that failed
}
}
Everything thrown here descends from ChainsError, so you catch one type and read fields instead of parsing messages. UnknownChainError is create() with a key nobody registered, UnsupportedChainError is getChain() with a spelling that matches nothing, InvalidAddressError is a failed format check, and AddressValidationUnsupportedError is a chain that carries no validator at all. .chain holds the canonical key on the last two, the same value create() takes.
Next
- Registry and aliases: keys, names, aliases and
register. - Address validation: what each family decodes and which checksums it verifies.
- Identify: one address through every validator.
- Metadata: every field, and where the decimals come from.
- CLI: the
chainscommand. - Agents: four tools over MCP, Pi and OMP.
- Custom chains: extend
Chain, register it. - Playground: the library, in the page.