Custom providers
One class
import { Provider, register, type Balance, type ChainKey, type Transaction } from "@agntn/explorers";
class MyExplorer extends Provider {
static readonly key = "my-explorer";
get capabilities() {
return {
balances: true,
txHistory: true,
txDetail: false,
contractInfo: false,
tokenBalances: false,
tokenTransfers: false,
gasData: false,
blockInfo: false,
};
}
async getBalance(address: string, chain?: ChainKey): Promise<Balance> {
const answer = await this.getJSON<{ balance: string }>(`https://my.explorer/api/${address}`);
return this.snapshotBalance({
address,
chain: chain ?? "ethereum",
balance: answer.balance,
balanceFormatted: formatWei(answer.balance, 18),
symbol: "ETH",
});
}
async getTxHistory(address: string, chain?: ChainKey): Promise<Transaction[]> {
// map the explorer's rows onto Transaction
}
}
register(MyExplorer, { chains: ["ethereum"], capabilities: ["balances", "txHistory"] });
Three things the base class gives you. this.getJSON() and this.postJSON() go through the shared client, so the configured timeout, the explorers/<version> user agent, the out-of-range integer preservation and normalizeError all apply. this.snapshotBalance() stamps fetchedAt and fills blockNumber and blockHash with what you pass or null. And this.name is your static readonly key.
Optional operations are optional: define getTxDetail and the rest only when they are real, and keep capabilities in step. The one rule is the honest one, a flag that is true has a method behind it, a flag that is false has none.
Registration
register(providerClass, meta) takes the class and what the registry needs to answer without an instance: chains, optional capabilities, optional defaultURL. After that create("my-explorer"), resolveProvider(undefined, "ethereum") and the CLI's --provider my-explorer all see it. Capabilities are optional in meta for backward compatibility, but leave them out and selection by capability will skip your provider, so put them in.
Inside the package
A provider that ships with the package follows five steps, and test/unit/registry.test.ts fails when one is skipped:
- A class extending
Providerinsrc/providers/, with one uniquestatic readonly key. - Balances and history implemented, and only the optional methods that really work advertised.
- The class exported.
- An entry in
builtinsinsrc/providers/index.tswith its chains, capabilities, public endpoint and aloadthat imports the module. - The file added to
build.config.ts, so it ships as its own bundle andcreate()can import it alone.
The test compares the list against the files on disk, against the key of the class each entry loads, and against the build inputs. Metadata cannot drift away from the implementation without a red test.
Nothing runs on import
Library modules evaluate to declarations only. A derived value, a new Set(), a prebuilt map, a decoder, waits for its first use. That is what keeps every bundle except the CLI under a kilobyte of side effects, and what lets a consumer import one provider without paying for twelve. A custom provider that keeps to the same rule stays tree-shakable in the bundle that carries it.
Agents
The same nine read-only tools over MCP, the Pi extension and the OMP extension, how a provider is chosen for a tool call, and what a tool never does.
Explorer
The block explorer behind explorers.agntn.dev, a search box and address, transaction and block pages on 23 chains, running the library live through a Cloudflare Worker.