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

@anywalletkit/history-evm

v0.1.10

Published

Activity history (HistorySource) for every EVM chain the SDK supports: Etherscan v2, Blockscout PRO and Alchemy Transfers API (API keys) with Blockscout and Routescan keyless fallbacks, merged into one ChainTx per hash with a documented spam heuristic.

Readme

@anywalletkit/history-evm

npm

HistorySource for every EVM chain the SDK supports. One source per chain (chain = "eip155:<id>"), backed by Etherscan v2 when you have an API key, Blockscout PRO and Alchemy (Transfers API) with theirs, and keyless fallbacks (Blockscout /api/v2, Routescan's Etherscan-format mirror) where a public instance exists. Explorer rows are merged into one ChainTx per hash: top-level transaction, ERC-20 transfers and internal native transfers, with fee, status, call selector and a documented spam flag.

Install

npm install @anywalletkit/history-evm @anywalletkit/core
# or: pnpm add @anywalletkit/history-evm @anywalletkit/core  ·  yarn add @anywalletkit/history-evm @anywalletkit/core

On npm: @anywalletkit/history-evm, published from the release workflow with provenance.

import { createEvmHistory } from '@anywalletkit/history-evm'

const history = createEvmHistory({ chainId: 8453, etherscanApiKey: process.env.ETHERSCAN_API_KEY })
const page = await history.listActivity('0x8AbE813577E47415A3CD0Bbc68c7ABCa2c4c55bA', { limit: 50 })
for (const tx of page.items) console.log(tx.hash, tx.status, tx.transfers.length, tx.spam ? 'spam' : '')
const older = await history.listActivity(address, { cursor: page.cursor })
const one = await history.getTransaction(page.items[0].hash, address)
history.explorerTxUrl(one.hash) // https://basescan.org/tx/0x…

Options

| option | default | notes | |---|---|---| | chainId | — | 1, 8453, 42161, 10, 137, 56, 43114, 100, 130, 143, 999, 146, 80094, 59144, 534352, 324, 42220, 9745, 4326, 4663, 480, 1329, 57073, 98866, 196 | | etherscanApiKey | none | Without it Etherscan is never called: since 2026 api.etherscan.io/v2 answers Missing/Invalid API Key to keyless requests. | | etherscanUrl | https://api.etherscan.io/v2/api | Point at a proxy that adds the key server-side. | | blockscoutProApiKey | none | Blockscout PRO multichain key (proapi_…; free tier 100K credits/day, 5 rps, one key for every Blockscout chain). Etherscan-format with chain_id=; tried after Etherscan, so a free Etherscan key + a free PRO key together cover Base, Arbitrum, Polygon, Optimism, Gnosis, Robinhood, Arc, Scroll, zkSync, Ink, Plume without a paid plan. Register at docs.blockscout.com/devs/pro-api. | | blockscoutProUrl / blockscoutProRateLimit | https://api.blockscout.com/v2/api / 5 | Override the endpoint (proxy) / the per-key limiter. | | alchemyApiKey | none | Alchemy key (free tier: 30M CU/month, no card). Enables the alchemy backend: alchemy_getAssetTransfers lists + one JSON-RPC batch of eth_getTransactionByHash/eth_getTransactionReceipt per page. The only free source on Base and BSC; also a second source for Monad, Berachain, Linea, HyperEVM and Robinhood, which otherwise depend on Etherscan alone. Tried after Etherscan and Blockscout PRO. Every network must be enabled for the app in the Alchemy dashboard (see below). | | alchemyNetwork | ALCHEMY_NETWORKS[chainId] when the Transfers API is documented for the chain | Slug override (base-mainnet). Passing it also forces the backend on for a chain the table lists as undocumented (Sonic, Plasma, MegaETH, Sei, Plume, X Layer). | | alchemyUrl | https://{network}.g.alchemy.com/v2/{key} | Host template for a proxy; {network} / {key} substituted. | | alchemyComputeUnitsPerSecond | 300 | Alchemy compute units per second, shared per key across chains. Each call is charged its published cost (Transfers 120, receipt 20, tx 17, block 16, else 26) and a JSON-RPC batch the sum of its members, so concurrent chains on one key stay under the free tier's 500 CU/s. A rate-limit answer is retried up to three times with backoff. | | blockscoutUrl | built-in table | Any Blockscout /api/v2 root; adds or overrides the chain's entry. | | etherscanCompatUrl | built-in table (Routescan) | Keyless Etherscan-format endpoint. | | fetch | globalThis.fetch | Inject for tests / custom transports. | | rateLimit | 3 rps with a key | Etherscan requests per second (free tier 3, Lite 5, Standard 10). One token bucket per key is shared by every source in the process (25 chains do not multiply the quota). Blockscout: 5 rps per host, Routescan: 2 rps. | | prefer | 'etherscan' | 'blockscout' tries Blockscout first to save Etherscan quota; 'alchemy' tries Alchemy first (Base, where it is the only free one, or to spend Alchemy CU instead of Etherscan calls). | | timeoutMs | 15 000 | Per request (AbortSignal.timeout when the runtime has it). |

Backend order: Etherscan v2 (key + chain in the v2 chainlist) → Blockscout PRO (key) → Alchemy (key + documented Transfers API, or alchemyNetwork) → Routescan → Blockscout. prefer moves Blockscout or Alchemy first. A failing backend is retried once on rate limiting (HTTP 429/5xx, Max rate limit reached), then the next backend is tried. Plan / key refusals (Free API access is not supported for this chain, Missing/Invalid API Key, Community Free API Limit reached, unsupported chainid, Blockscout PRO Unauthorized / Featured chain, Alchemy <NETWORK> is not enabled for this app / HTTP 401 / 403 / Unsupported method) skip straight to the fallback. When nothing is left the source throws HistoryUnavailableError with chain, reason and one line per attempts. Chains in NO_EXPLORER_CHAIN_IDS return an empty page with a note instead.

The Etherscan v2 chainlist (/v2/chainlist) is fetched once per process and cached; when it cannot be fetched the table verified on 2026-10-14 is used.

Chain coverage (verified with curl on 2026-10-14)

With a Blockscout PRO key (verified live with a free-tier key on 2026-10-14): served on 1, 10, 100, 130, 534352, 42220, 4326, 4663, 480, 57073, 98866, 5042 and 42161 (Arbitrum). "Featured" chains need the Builder plan ($49/mo): 8453 Base, 137 Polygon, 324 zkSync ({"error":"Featured chain … requires one of the following plans: Builder, Business, Pro"} → classified unavailable, falls through). "Network not supported": 56, 43114, 143, 999, 146, 80094, 59144, 9745, 1329, 196, 5000, 747474, 2818, 14. Net effect with a free Etherscan key + a free PRO key: everything has a free source except Base (paid Etherscan or Blockscout Builder), BSC, X Layer and Morph — add a free Alchemy key (section below) and Base and BSC are covered too; only X Layer (and Sonic / Plasma / MegaETH / Sei / Plume, where Alchemy has no Transfers API) stay on Etherscan alone.

Probe used for Blockscout: GET <root>/api/v2/addresses/0x8AbE813577E47415A3CD0Bbc68c7ABCa2c4c55bA/transactions and /api/v2/transactions, kept only when both answer 200 JSON.

| chain | id | Etherscan v2 | keyless fallback | notes | |---|---|---|---|---| | Ethereum | 1 | yes (free plan ok) | Blockscout eth.blockscout.com, Routescan | | | Base | 8453 | yes, paid plan only | none | base.blockscout.com is behind a Cloudflare JS challenge (403) | | Arbitrum One | 42161 | yes (free plan ok) | none | arbitrum.blockscout.com 403 (Cloudflare) | | OP Mainnet | 10 | yes, paid plan only | Blockscout explorer.optimism.io | | | Polygon | 137 | yes (free plan ok) | none | polygon.blockscout.com 403 (Cloudflare) | | BNB Smart Chain | 56 | yes, paid plan only | none | no public Blockscout found | | Avalanche C-Chain | 43114 | yes, paid plan only | Routescan | | | Gnosis | 100 | yes, paid plan only | Blockscout-compatible gnosisscan.io | gnosis.blockscout.com redirects there | | Unichain | 130 | yes | Blockscout unichain.blockscout.com | | | Monad | 143 | yes | none | | | HyperEVM | 999 | yes (free plan ok) | none | | | Sonic | 146 | yes | none | | | Berachain | 80094 | yes | none | beratrail.io (Routescan UI) answers 202, no JSON | | Linea | 59144 | yes | none | explorer.linea.build has no /api/v2 | | Scroll | 534352 | no (not in chainlist) | Blockscout-compatible scrollscan.com | scroll.blockscout.com redirects there | | zkSync Era | 324 | no | Blockscout zksync.blockscout.com | | | Celo | 42220 | yes, community quota | Blockscout celo.blockscout.com | | | Plasma | 9745 | yes | Routescan | | | MegaETH | 4326 | yes | Blockscout megaeth.blockscout.com | | | Robinhood Chain | 4663 | yes (free plan ok) | none | | | World Chain | 480 | yes | Blockscout worldchain-mainnet.explorer.alchemy.com | | | Sei | 1329 | yes | none | seitrace.com has its own API | | Ink | 57073 | no | Blockscout explorer.inkonchain.com | | | Plume | 98866 | no | Blockscout explorer.plume.org | | | X Layer | 196 | no | none | only OKLink (own API, key); empty page + note |

"free plan ok" = a free-tier key returned rows (or No transactions found) that day; "paid plan only" = Free API access is not supported for this chain. Chains marked "yes" without a qualifier were served but the test wallet has no history there, so the plan tier could not be told apart. Without a key, the chains with "none" in the fallback column throw HistoryUnavailableError (no keyless explorer; pass etherscanApiKey).

Alchemy backend (backend: 'alchemy')

alchemy_getAssetTransfers is called twice per page — fromAddress and toAddress — with category: ['external', 'erc20'] (+ 'internal' on the chains in ALCHEMY_INTERNAL_CHAIN_IDS), order: 'desc', withMetadata: true (block timestamps), excludeZeroValue: false (zero-value address-poisoning transfers are what the spam rule flags; Alchemy hides them by default) and maxCount = limit. Transfers are de-duplicated by uniqueId, grouped by hash, and the Transfers API's gaps are filled from the same endpoint: every unique hash of the page gets eth_getTransactionByHash + eth_getTransactionReceipt in one JSON-RPC batch (≤ 20 requests per HTTP call, one retry) → from, to, value, input → call.selector, status, fee = gasUsed × effectiveGasPrice. Blocks whose transfers carried no metadata.blockTimestamp get an eth_getBlockByNumber in that batch.

Merge rules (mirroring the Etherscan txlist + tokentx + txlistinternal merge): erc20 rows → token transfers (symbol = asset, decimals = rawContract.decimal, amount = rawContract.value); internal rows → native transfers; external rows are the transaction's own value and are already the tx row's nativeValue — they add no second native transfer. Only when the node has no such tx any more (dropped / reorged, null from the batch) is the external row turned into a bare tx row (status: confirmed, no fee) so the item still renders. ERC-721 / 1155 / special-NFT rows are skipped (v1). getTransaction(hash) = the same batch for one hash + ERC-20 Transfer logs of the receipt + eth_getBlockByNumber for the time.

Pagination is by block, not by pageKey. Measured on base-mainnet on 2026-10-14: a request that carries a pageKey ignores toBlock; an unknown or expired pageKey (TTL 10 minutes) silently restarts from the first page; a malformed one is HTTP 400 Invalid pageKey format. None of that fits a cursor that is resumed hours later or re-fetched with a narrower window, so the Alchemy lists page exactly like the Etherscan ones — toBlock = w − 1 from the window cursor, next is a marker — and an Alchemy cursor never expires.

Budget. 120 CU per Transfers call, 15 CU per eth_getTransactionByHash, 15 CU per eth_getTransactionReceipt: a 50-item page ≈ 2 × 120 + 50 × 30 ≈ 1 740 CU, so the free 30M CU/month is ≈ 17 000 pages (≈ 570 a day); a getTransaction ≈ 45–60 CU. Throughput on the free tier is 500 CU/s, hence the 4 rps advice above.

Gotcha: enable the network in the Alchemy app. A new app has only the networks you tick in the dashboard (Apps → your app → Networks). Every other slug answers HTTP 403 {"error":{"code":-32600,"message":"BNB_MAINNET is not enabled for this app. Visit this page to enable the network: …"}} — classified unavailable, no retry, next backend. Verified 2026-10-14 with a free key: eth-mainnet and base-mainnet answered (including the internal category on both); every other slug below answered "not enabled" until ticked.

Slug table (ALCHEMY_NETWORKS; verified 2026-10-14 by eth_chainId on every slug — an existing slug answers the chain id or "not enabled", a non-existent one does not resolve). "Transfers API" = the chain's docs page https://www.alchemy.com/rpc/<chain>.md lists it under "Supported on …" (ALCHEMY_TRANSFERS_CHAIN_IDS); "internal" = the internal category per docs ("available only on Ethereum Mainnet, Polygon Mainnet, Base Mainnet, Arc Mainnet, and Arc Testnet"; ALCHEMY_INTERNAL_CHAIN_IDS).

| chain | id | slug | Transfers API documented | internal category | |---|---|---|---|---| | Ethereum | 1 | eth-mainnet | yes | yes (verified live) | | Base | 8453 | base-mainnet | yes | yes (verified live) | | Arbitrum One | 42161 | arb-mainnet | yes | no | | OP Mainnet | 10 | opt-mainnet | yes | no | | Polygon PoS | 137 | polygon-mainnet | yes (/rpc/matic.md) | yes (docs) | | BNB Smart Chain | 56 | bnb-mainnet | yes | no | | Avalanche C-Chain | 43114 | avax-mainnet | yes | no | | Gnosis | 100 | gnosis-mainnet | yes | no | | Unichain | 130 | unichain-mainnet | yes | no | | Monad | 143 | monad-mainnet | yes | no | | HyperEVM | 999 | hyperliquid-mainnet | yes | no | | Sonic | 146 | sonic-mainnet | no ("Not currently supported") | no | | Berachain | 80094 | berachain-mainnet | yes | no | | Linea | 59144 | linea-mainnet | yes | no | | Scroll | 534352 | scroll-mainnet | yes | no | | zkSync Era | 324 | zksync-mainnet | yes | no | | Celo | 42220 | celo-mainnet | yes | no | | Plasma | 9745 | plasma-mainnet | no | no | | MegaETH | 4326 | megaeth-mainnet | no | no | | Robinhood Chain | 4663 | robinhood-mainnet | yes | no | | World Chain | 480 | worldchain-mainnet | yes | no | | Sei | 1329 | sei-mainnet | no | no | | Ink | 57073 | ink-mainnet | yes | no | | Plume | 98866 | plume-mainnet | no (no Alchemy docs page) | no | | X Layer | 196 | xlayer-mainnet | no | no | | Arc | 5042 | arc-mainnet | yes | yes (docs) |

Only Ethereum and Base were exercised end to end (the test key had only those two networks enabled); the other rows are slug + documentation checks. Chains marked "no" are not tried unless alchemyNetwork is passed.

What a ChainTx contains

  • time from timeStamp (Etherscan) / ISO timestamp (Blockscout) / metadata.blockTimestamp (Alchemy, else eth_getBlockByNumber).
  • status: failed when isError = 1 or txreceipt_status = 0 (Blockscout status = error, receipt status = 0x0), else confirmed.
  • from / to / nativeValue from the transaction row. A hash the explorer only lists as a token transfer to the address (incoming ERC-20) has no transaction row: from/to are then the transfer's, raw.tx is absent.
  • fee = gasUsed × gasPrice (Blockscout fee.value; Alchemy and the Etherscan proxy path: receipt gasUsed × effectiveGasPrice).
  • call = { to, selector (4 bytes, lowercase), data } when the input is not 0x.
  • transfers: the native value (when > 0), every ERC-20 row (standard: 'erc20', symbol/decimals from the explorer or the swap2 asset catalog), every internal native transfer with value > 0. ERC-721 / ERC-1155 rows are skipped in v1.
  • block as bigint, raw = { source, tx, tokens, internal, spam?, spamReason? } with the untouched explorer rows (Alchemy: raw.tx = { tx, receipt } JSON-RPC objects, raw.tokens / raw.internal the Transfers API rows).
  • EvmChainTx.spam mirrors raw.spam.

getTransaction(hash, address) uses Etherscan's proxy module or Alchemy's JSON-RPC (eth_getTransactionByHash + eth_getTransactionReceipt; ERC-20 Transfer logs become transfers; no internal transfers, neither has traces) or Blockscout /api/v2/transactions/<hash> + /token-transfers + /internal-transactions. Returns undefined for an unknown hash.

Pagination

Explorer lists (transactions, token transfers, internal transfers) paginate independently, so the source pages by block window: each page fetches one explorer page of every list and emits only rows above the highest cut block among the lists that still have more rows. Everything emitted is complete across all three lists, so the merge by hash is exact. The cursor (base64 JSON: { v, b: backend, w: exclusive upper block, lists }) carries Blockscout's next_page_params per list; Etherscan lists simply re-query with endblock = w - 1, Alchemy lists with toBlock = w - 1. A cursor from one backend resumes on another by block when the first one fails. limit (default 50, max 100) is the explorer page size, so a page can hold more hashes than limit when token/internal rows add hashes of their own. Degenerate case: when one list has more than limit rows in a single block, that block is emitted once and the remainder skipped.

since is applied client-side on time (explorers page by block, not time): rows older than since are dropped and the page has no cursor.

Spam rule

Never applied to a transaction the address itself sent. A tx with at least one token row is flagged (raw.spamReason) when:

  1. zero-value — every movement in the tx is zero (zero-value transferFrom address poisoning, even with a catalog token such as USDT);
  2. zero-value-unknown-token — a token the catalog does not know moved 0;
  3. bulk-airdrop — 20 or more token rows in one tx and at least one unknown token;
  4. symbol-impersonation — an unknown token whose symbol contains non-ASCII characters (USDΤ) or whose ASCII form equals a catalog symbol on this chain (fake "USDT" at another contract).

"Catalog" = @anywalletkit/swap2-assets (ccChain('eip155:<id>').assets).

Adding a chain

  1. Add the id to SUPPORTED_CHAIN_IDS and its explorer to EXPLORER_TX_BASE in src/chains.ts.
  2. curl https://api.etherscan.io/v2/chainlist — if listed, add to ETHERSCAN_V2_CHAIN_IDS_FALLBACK.
  3. Probe a Blockscout root (/api/v2/transactions must be 200 JSON) → BLOCKSCOUT_URLS; Routescan (https://api.routescan.io/v2/network/mainnet/evm/<id>/etherscan/api?module=account&action=txlist&address=…) → ETHERSCAN_COMPAT_URLS.
  4. Alchemy: curl -d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' https://<slug>.g.alchemy.com/v2/<key> → ALCHEMY_NETWORKS; https://www.alchemy.com/rpc/<chain>.md lists "Transfers API" → ALCHEMY_TRANSFERS_CHAIN_IDS (and ALCHEMY_INTERNAL_CHAIN_IDS when the internal category is documented).
  5. Nothing answers → NO_EXPLORER_CHAIN_IDS.
  6. Update the tables above with the date, run pnpm --filter @anywalletkit/history-evm test (the chain-table tests enforce that every id is covered and has an Alchemy slug).

Tests

pnpm --filter @anywalletkit/history-evm test — no network: fixtures in tests/fixtures/ are real Etherscan v2 (chainid=1, captured with a key — keyless capture is no longer possible) and eth.blockscout.com responses for the public test wallet 0x8AbE813577E47415A3CD0Bbc68c7ABCa2c4c55bA, plus a Routescan sample for Avalanche and synthetic rows for a 3-leg swap, a failed tx, a 20-row airdrop and a fake-USDT dust transfer. Alchemy fixtures (alchemy-base-*.json, base-mainnet, same wallet, captured with a free key on 2026-10-14): the fromAddress / toAddress Transfers pages (incl. internal rows, an Aave supply and withdraw, inbound-via-internal payments and non-ASCII spoofed tokens), the tx + receipt batch for every hash in them (calldata longer than 2 KB and receipts with many logs trimmed, marked _inputTrimmed / _logsTrimmed), and the is not enabled for this app envelope. LIVE_HISTORY=1 (and optionally ETHERSCAN_API_KEY / ALCHEMY_API_KEY) enables the live checks.

Hermes-safe: fetch only, guarded AbortSignal.timeout, own base64 (no Buffer/btoa), no URL in src/.