npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agntn/chains

v0.2.1

Published

Canonical blockchain classes with registry, aliases, and address validation

Readme

@agntn/chains

Canonical blockchain classes, aliases, and address validation.

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. So they live here once, as classes.

Stack

TypeScript, ESM-only. The core imports nothing at runtime. The CLI adds citty and consola, the MCP server adds @modelcontextprotocol/sdk, and the agent extensions need typebox and @earendil-works/pi-coding-agent. The MCP server and the extensions describe their parameters with the same typebox schemas.

Installation

pnpm add @agntn/chains

Usage

import { Ethereum, EVM, create, getChain } from "@agntn/chains";

const ethereum = create("eth");

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");

Architecture

Chain (abstract)
├── EVM (abstract)
│   ├── Ethereum
│   ├── Base
│   ├── Arbitrum
│   └── ...
├── Move (abstract)
│   ├── Aptos
│   └── Sui
├── Bitcoin
├── Solana
├── Ton
├── Tron
└── Octra

Each chain is its own class holding its own metadata. EVM and Move own the family type and the address format, and everything else is declared per class, down to the coin type all thirteen EVM chains repeat. Importing the package registers all of them.

Registration runs on side-effect imports. Set sideEffects: false and the bundler eats the register() calls, so create("eth") throws on a key that is right there in the source. Bundler docs and package linters both suggest that flag, and nothing complains until a production build hands you an empty registry.

API

Base classes

  • Chain is the abstract contract for metadata and address validation
  • EVM and Move hold what their concrete classes share
  • ChainConstructor is what the registry accepts

Registry

  • register(ChainClass) registers a concrete class under its static key
  • create(key) builds a fresh instance of a registered class
  • chains() returns the registered keys in registration order
  • has(key) checks whether a class is registered
  • getChain(input?) takes a key, symbol, or alias and gives you an instance, defaulting to Ethereum

getChain matches keys, symbols, and the aliases people actually type, so matic, btc and arb all work. Display names work too, read straight off the registered classes, so whatever chain.name prints resolves back to the same chain — Arbitrum One, BNB Chain, zkSync Era. That round trip matters for agents, which get a name out of one call and put it into the next. Symbols stay out of the automatic index: six chains report ETH, so matching on them would depend on registration order.

getChain() with no argument still means Ethereum. getChain("") or a blank string does not — that is a caller mistake, and it throws rather than quietly answering about the wrong chain.

Instance behavior

chain.assertAddress(address) returns the address when it fits the chain's format and throws when it doesn't. It's a format check, not a checksum, and not proof the address exists on chain. Chains without a validator throw instead of quietly saying yes — chain.validatesAddress tells you which ones those are before you ask. A false green light costs more than a false alarm when the caller is about to send funds.

Two of the validators do more than match a shape, because a shape is not enough. Solana decodes base58 and requires exactly 32 bytes: character length cannot separate an account from a Bitcoin or TRON address, since those are 34 characters and 25 bytes, while the System Program is 32 characters and 32 bytes. Bitcoin's bech32 branch uses the BIP-173 charset, which has no 1, b, i or o, and treats all-lowercase and all-uppercase as valid while rejecting mixed case — uppercase is what QR encoders emit, so rejecting it would fail addresses that spend fine.

Errors

Everything thrown here descends from ChainsError, so you catch one type and read fields instead of parsing message strings.

  • UnknownChainError when create() got a key with no registered class, carries .key
  • UnsupportedChainError when getChain() got input matching no alias or name, carries .input; the message quotes the value, so blank and control-character input stays visible in a log
  • InvalidAddressError when an address failed its format check, carries .address and .chain
  • AddressValidationUnsupportedError when the chain has no validator, carries .chain

Careful with .chain on InvalidAddressError: it names the validator that rejected the address, not the chain you asked about. Every EVM chain reports "EVM", because they share one validator. Use the key you passed to create() if you need to know which chain it was.

CLI

chains list --type evm            # every registered EVM chain
chains info matic                 # canonical metadata, add --json for a machine
chains resolve btc                # bitcoin
chains validate eth 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984

resolve, info and validate print a message and exit 1 when they fail. list warns and exits 0 when a --type filter matches nothing, so don't use it as a check in a script.

MCP server

chains mcp

Speaks MCP over stdio and exposes the same three tools as the agent extensions: chains_lookup, chains_validate_address and chains_list. Point a client at it:

{
  "mcpServers": {
    "chains": { "command": "npx", "args": ["-y", "@agntn/chains", "mcp"] }
  }
}

An MCP client sees the text a tool returns and nothing else, so the text carries the whole answer: every metadata field on a hit, and the registered keys when resolution fails, so the next call has somewhere to go. chains_list is there for the same reason — without it the only way to learn what the registry holds is to send a value you expect to fail. Absent fields say so out loud (bip44: none) rather than vanishing, because a missing coin type reads as "not shown" and invites the caller to supply one from memory.

A rejected address is an answer, not a tool error. Only an unresolvable chain or a chain with no validator sets isError, because then nothing was checked.

createMcpServer() is exported from @agntn/chains/mcp for hosts that bring their own transport.

Agent extensions

Pi and OMP extensions live in packages/pi/extensions and packages/omp/extensions. They expose chains_lookup for resolving a chain into its metadata, chains_validate_address for checking an address, and chains_list for the registry.

All three surfaces call the executors in src/tool-operations.ts, so the MCP server and the two extensions answer identically. The extensions add the details the harnesses render; MCP drops them and keeps the text.

The extensions prefer the built executors and fall back to source only when dist/ is missing, because the internal imports use .js specifiers that a plain TypeScript-stripping runtime can't resolve back to .ts. Without pnpm build the tools still register and the first call dies with a module-resolution error.

Not in scope

No RPC calls, no wallets, no transaction building. Those belong in rpcx, ubichain and webri.

Chain fields

| Field | Type | Description | | ------------ | ----------- | ---------------------------- | | key | ChainKey | Canonical class key | | name | string | Human-readable name | | symbol | string | Native token symbol | | type | ChainType | Blockchain family | | 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.

Supported chains

eth, base, arbitrum, optimism, polygon, bsc, avalanche, fantom, gnosis, linea, zksync, scroll, bera, bitcoin, solana, aptos, sui, ton, tron, and oct.