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

@pulsadev/multicall

v0.2.0

Published

High-performance, provider-agnostic multicall for EVM chains

Readme

@pulsadev/multicall

High-performance, provider-agnostic multicall for EVM chains.

Batch hundreds of contract reads into a single RPC call. Zero runtime dependencies. Works with viem, ethers v5, ethers v6, or a plain RPC URL.

Features

  • Provider agnostic — viem, ethers v5, ethers v6, EIP-1193, or raw RPC URL
  • Multicall3 — uses aggregate3 with per-call allowFailure
  • Auto-chunking — splits large batches by call count and data size
  • Retry + fallback — exponential backoff with optional fallback to individual calls
  • Revert decoding — human-readable Error(string) and Panic(uint256) messages
  • Result caching — TTL-based cache to avoid redundant calls
  • 70+ chains — Ethereum, Arbitrum, Optimism, Base, Polygon, BSC, zkSync, and more
  • AbortSignal — cancel in-flight requests
  • Zero dependencies — pure keccak256 implementation, no external packages
  • Tiny — ~31 KB bundled (ESM + CJS + full TypeScript declarations)

Install

npm install @pulsadev/multicall
# or
pnpm add @pulsadev/multicall
# or
yarn add @pulsadev/multicall

Quick Start

With viem

import { Multicall } from '@pulsadev/multicall'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'

const client = createPublicClient({ chain: mainnet, transport: http() })
const mc = new Multicall(client)

const results = await mc.call([
  {
    target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
    allowFailure: true,
    callData: '0x18160ddd', // totalSupply()
  },
  {
    target: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
    allowFailure: true,
    callData: '0x06fdde03', // name()
  },
])

for (const r of results) {
  if (r.status === 'success') {
    console.log('Result:', r.result)
  } else {
    console.log('Reverted:', r.error?.reason)
  }
}

With ethers v6

import { Multicall } from '@pulsadev/multicall'
import { JsonRpcProvider } from 'ethers'

const provider = new JsonRpcProvider('https://eth.llamarpc.com')
const mc = new Multicall(provider)

const balance = await mc.getEthBalance('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045')
console.log('Balance:', balance)

With a plain RPC URL

import { Multicall } from '@pulsadev/multicall'

const mc = new Multicall('https://eth.llamarpc.com')

const blockNumber = await mc.getBlockNumber()
console.log('Block:', blockNumber)

API

new Multicall(provider, options?)

Create a new multicall instance.

Provider — any of the following:

  • viem PublicClient
  • ethers v6 JsonRpcProvider
  • ethers v5 Provider
  • EIP-1193 provider (window.ethereum)
  • RPC URL string (https://... or wss://...)

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | chainId | number | auto-detected | Override chain ID | | multicallAddress | Address | canonical | Custom Multicall3 address | | blockTag | string \| bigint | 'latest' | Block to query | | allowFailure | boolean | true | Allow individual calls to fail | | chunk.maxCallsPerChunk | number | 256 | Max calls per batch | | chunk.maxDataSizePerChunk | number | 65536 | Max calldata bytes per batch | | retry.maxRetries | number | 3 | Number of retries | | retry.backoffMs | number | 200 | Initial backoff delay | | retry.backoffMultiplier | number | 2 | Backoff multiplier | | retry.fallbackToIndividual | boolean | false | Fallback to individual calls on batch failure | | cache.enabled | boolean | false | Enable result caching | | cache.ttlMs | number | 5000 | Cache TTL in milliseconds | | signal | AbortSignal | — | Abort signal for cancellation |

mc.call(calls, options?)

Execute a batch of calls using aggregate3.

const results = await mc.call([
  { target: '0x...', allowFailure: true, callData: '0x...' },
  { target: '0x...', allowFailure: true, callData: '0x...' },
])
// returns: MulticallResult[]
// { status: 'success', result: '0x...' }
// { status: 'failure', error: { reason: 'Insufficient balance', data: '0x...' } }

mc.aggregate3(calls, options?)

Low-level aggregate3 call. Returns raw { success, returnData } tuples.

const raw = await mc.aggregate3([
  { target: '0x...', allowFailure: true, callData: '0x...' },
])
// returns: { success: boolean, returnData: Hex }[]

mc.tryAggregate(requireSuccess, calls, options?)

Execute using tryAggregate. Set requireSuccess to false for partial failure tolerance.

mc.getBlockNumber(options?)

Get the current block number via Multicall3.

mc.getEthBalance(address, options?)

Get the ETH balance of an address via Multicall3.

mc.getBasefee(options?)

Get the current block base fee via Multicall3.

mc.getChainId()

Get the chain ID (auto-detected or from options).

mc.clearCache()

Clear the internal result cache.

Encoding Helpers

Build calldata without importing ethers or viem:

import { encodeFunctionData, functionSelector, decodeFunctionResult } from '@pulsadev/multicall'

// Get function selector
const sel = functionSelector('balanceOf(address)')
// '0x70a08231'

// Encode full calldata
const data = encodeFunctionData('balanceOf(address)', [
  '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
])

// Decode result
const [balance] = decodeFunctionResult(resultHex, ['uint256'])

Chain Support

All chains with a canonical Multicall3 deployment are supported out of the box. For chains with non-standard deployments (e.g., zkSync Era), addresses are mapped automatically.

import { getSupportedChainIds, getMulticall3Address } from '@pulsadev/multicall'

const chains = getSupportedChainIds()
// [1, 10, 137, 42161, 8453, ...]

const addr = getMulticall3Address(324) // zkSync Era
// '0xF9cda624FBC7e059355ce98a31693d299FACd963'

| Network | Chain ID | Status | |---------|----------|--------| | Ethereum | 1 | ✅ | | Optimism | 10 | ✅ | | BSC | 56 | ✅ | | Polygon | 137 | ✅ | | Fantom | 250 | ✅ | | zkSync Era | 324 | ✅ | | Arbitrum One | 42161 | ✅ | | Avalanche | 43114 | ✅ | | Base | 8453 | ✅ | | Linea | 59144 | ✅ | | Scroll | 534352 | ✅ | | Blast | 81457 | ✅ | | Mantle | 5000 | ✅ | | ... and 60+ more | | ✅ |

For unlisted chains, the canonical Multicall3 address (0xcA11bde05977b3631167028862bE2a173976CA11) is used by default.

Advanced Usage

Auto-chunking

Large batches are automatically split to stay within RPC limits:

const mc = new Multicall(provider, {
  chunk: {
    maxCallsPerChunk: 100,
    maxDataSizePerChunk: 32_768,
  },
})

Retry with fallback

Retry failed batches with exponential backoff. Optionally fall back to individual calls:

const mc = new Multicall(provider, {
  retry: {
    maxRetries: 3,
    backoffMs: 200,
    backoffMultiplier: 2,
    fallbackToIndividual: true,
  },
})

Result caching

Cache results to avoid redundant RPC calls:

const mc = new Multicall(provider, {
  cache: { enabled: true, ttlMs: 10_000 },
})

// First call hits RPC
await mc.aggregate3(calls)

// Second call within 10s returns cached result
await mc.aggregate3(calls)

// Clear manually
mc.clearCache()

Cancellation

Cancel in-flight requests with AbortSignal:

const controller = new AbortController()

setTimeout(() => controller.abort(), 5000)

const results = await mc.call(calls, {
  signal: controller.signal,
})

Benchmarks

Measured on a dedicated VPS (Dallas, 4 vCPU, 8 GB RAM) against Ethereum mainnet via publicnode RPC. Median of 10 runs after 3 warmup runs.

| Calls | @pulsadev/multicall | viem built-in | Difference | |------:|:-------------------:|:------------:|:----------:| | 10 | 55ms | 56ms | — | | 50 | 56ms | 60ms | 8% faster | | 100 | 59ms | 97ms | 39% faster | | 200 | 68ms | 84ms | 20% faster | | 500 | 79ms | 111ms | 29% faster |

Run benchmarks yourself: node bench/bench_final.mjs

Comparison

| Feature | @pulsadev/multicall | viem built-in | ethereum-multicall | ethcall | |---------|:------------------:|:------------:|:-----------------:|:------:| | Provider agnostic | ✅ | ❌ viem only | ❌ ethers v5 | ❌ ethers v6 | | Multicall3 aggregate3 | ✅ | ✅ | ❌ MC2 only | ✅ | | Auto-chunking | ✅ | ✅ bytes only | ❌ | ❌ | | Retry + fallback | ✅ | ❌ | ❌ | ❌ | | Revert reason decoding | ✅ | ❌ | ❌ | ❌ | | Result caching | ✅ | ❌ | ❌ | ❌ | | AbortSignal | ✅ | ❌ | ❌ | ❌ | | Zero dependencies | ✅ | ❌ | ❌ | ❌ | | Bundle size | ~31 KB | part of viem | 131 KB | 231 KB | | Maintained | ✅ | ✅ | ❌ | ❌ |

License

MIT © Yuto Nakamura