Guide

Custom providers

Extend Provider for an explorer this package does not know, register it with its chains and capabilities, and let selection find it.

One class

my-explorer.ts
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:

  1. A class extending Provider in src/providers/, with one unique static readonly key.
  2. Balances and history implemented, and only the optional methods that really work advertised.
  3. The class exported.
  4. An entry in builtins in src/providers/index.ts with its chains, capabilities, public endpoint and a load that imports the module.
  5. The file added to build.config.ts, so it ships as its own bundle and create() 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.

@agntn/explorers·MIT license· Read-only. Addresses you type go to a public explorer API, never to a wallet.