Custom chains
One class, one file
Every built-in is a concrete class extending the exported abstract Chain, or EVM and Move when the address format is theirs. Yours is the same shape:
import { Chain, InvalidAddressError, register } from "@agntn/chains";
import type { ChainKey, ChainType } from "@agntn/chains";
// The shape of a Nano account. Its checksum stays unchecked here.
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";
override readonly decimals = 30;
readonly explorer = "https://nanexplorer.com/nano";
readonly bip44 = 165;
override assertAddress(address: string): string {
if (!ADDRESS.test(address)) throw new InvalidAddressError(this.key, address);
return address;
}
}
register(Nano);
import "./nano";
import { create, getChain, identify } from "@agntn/chains";
getChain("nano").name; // "Nano"
identify("nano_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3").matches[0]?.key; // "nano"
Two casts, and they are the honest part of this page. ChainKey and ChainType are closed unions of the built-in values, so a new key doesn't type check without as ChainKey, and create("nano") wants the same cast. getChain("nano") takes a plain string and needs none. The unions exist so a typo in a key is a compile error inside the package; the price is paid once, in your class.
The contract
static readonly keyis whatregisterfiles the class under and whatchain.keyreturns. Lowercase, the chain's name rather than its ticker. Registering the same key again replaces the class.name,symbol,typeandexplorerare required.decimals,bip44,chainId,caip2andrpcDefaultare optional and mean unknown when left out, never a default.assertAddressreturns the address or throwsInvalidAddressError(this.key, address). Throw that class and nothing else for a bad address, becauseidentifycounts only that as a miss and lets any other throw come back out as a bug.- Skip
assertAddressandvalidatesAddresssaysfalse, the base method throwsAddressValidationUnsupportedError, andidentifylists your chain as unchecked rather than pretending it said no. - An EVM chain is
class Mine extends EVMwithchainId,caip2and the rest. The format check comes with the family.
Decode when you can
A regex is fine when the width is the whole format, as it is for Octra. When the format is Base58Check with a known byte length, decode and look at the bytes: a character window takes neighbouring chains and half the typos. The built-ins are the reference, src/chains/tron.ts is the short one and src/chains/cardano.ts the long one.
Where it shows up
Once the module is imported, the chain is in chains(), has() says true, getChain finds it by key and by display name, identify asks it like any other. The CLI and the MCP server only load the built-ins. To use a custom chain there you build your own binary around create and createMcpServer. There's no plugin path, and I'd rather say so than invent one that half works.