Multi-chain dApp backend with Blockmetric RPC

Balances, blocks and token transfers on eight chains with one token.

Cheapest plan
RPC Free
Time to build
20 min
Methods
4
eth_getBalance · eth_blockNumber · eth_getLogs
Languages
3
ethers, viem, web3.py
We open each RPC chain, region and plan when enough teams ask for it. This recipe shows exactly how it works. Join the waitlist and we e-mail you when yours opens.

What you get

A backend that reads the native balance of an address on Ethereum, Base, Arbitrum, OP Mainnet, Polygon, BNB Smart Chain, Avalanche and HyperEVM, follows new blocks over WebSocket and reads token transfers in log ranges each chain allows. ethers, viem or web3.py, pointed at Blockmetric RPC.

Build it step by step

  1. Get your endpoint token

    On Account › RPC create a token. The URL is https://rpc.<domain>/<chain>/<token>, or the chain URL with the token in an Authorization header. One token works on every chain.

  2. Point your library at it

    ethers: new JsonRpcProvider(url, chainId, { staticNetwork: true }). viem: http(url, { batch: true }). web3.py: Web3.HTTPProvider(url). Setting the chain id saves a lookup request on every start.

  3. Batch what belongs together

    Balance and block number of one chain in one HTTP request: fewer round trips, same credits.

    curl -s "$BLOCKMETRIC_RPC_URL/base/$BLOCKMETRIC_RPC_TOKEN" -H "Content-Type: application/json" -d '[
      {"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]},
      {"jsonrpc":"2.0","id":2,"method":"eth_chainId","params":[]}
    ]'
  4. Read logs in ranges

    Split wide ranges into steps of the chain limit (10,000 blocks, HyperEVM 50) and filter by address and topics on the server.

  5. Follow new blocks

    One WebSocket subscription (newHeads) keeps a latest-block table fresh; read the other chains over HTTP to stay within your connection limit.

The complete backend

Set this once in the shell that runs the code:

export BLOCKMETRIC_RPC_URL=https://rpc.blockmetric.io
export BLOCKMETRIC_RPC_TOKEN=...   # your endpoint token from Account > RPC
// Multi-chain dApp backend with Blockmetric RPC and ethers v6: the native balance of one address on every chain, the
// latest block per chain, and incoming token transfers read in log ranges the chain allows. One token for all chains.
//
//   npm install ethers
//   export BLOCKMETRIC_RPC_URL=https://rpc.<domain>     # shown on Account > RPC
//   export BLOCKMETRIC_RPC_TOKEN=...                    # your endpoint token (keep it on the server)
//   ADDRESS=0x... node balances-ethers.mjs
//
// Credits: every JSON-RPC call is 1 request (a batch of 8 calls counts as 8). eth_getLogs covers at most 10,000 blocks
// per call (HyperEVM: 50), so wide ranges are read in steps.
import { Contract, JsonRpcProvider, formatEther, formatUnits, id, zeroPadValue } from 'ethers'

const RPC = (process.env.BLOCKMETRIC_RPC_URL ?? '').replace(/\/+$/, '')
const TOKEN = process.env.BLOCKMETRIC_RPC_TOKEN
if (!RPC || !TOKEN) throw new Error('Set BLOCKMETRIC_RPC_URL and BLOCKMETRIC_RPC_TOKEN')
const ADDRESS = process.env.ADDRESS ?? '0x0000000000000000000000000000000000000000'

// slug, chain id, gas token, eth_getLogs block limit
const CHAINS = [
  ['eth', 1, 'ETH', 10_000],
  ['base', 8453, 'ETH', 10_000],
  ['arbitrum', 42161, 'ETH', 10_000],
  ['optimism', 10, 'ETH', 10_000],
  ['polygon', 137, 'POL', 10_000],
  ['bsc', 56, 'BNB', 10_000],
  ['avalanche', 43114, 'AVAX', 10_000],
  ['hyperevm', 999, 'HYPE', 50],
]

// staticNetwork: the chain id is known, so ethers skips a network lookup on every start (saves a request each time).
const provider = (slug, chainId) => new JsonRpcProvider(`${RPC}/${slug}/${TOKEN}`, chainId, { staticNetwork: true, batchMaxCount: 10 })

// 1. Balance and head block on every chain, in parallel (ethers batches the two calls per chain into one HTTP request).
const rows = await Promise.all(
  CHAINS.map(async ([slug, chainId, symbol]) => {
    const p = provider(slug, chainId)
    try {
      const [block, wei] = await Promise.all([p.getBlockNumber(), p.getBalance(ADDRESS)])
      return `${slug.padEnd(10)} block ${String(block).padStart(10)}  ${formatEther(wei)} ${symbol}`
    } catch (e) {
      return `${slug.padEnd(10)} ${e.shortMessage ?? e.message}` // 429: slow down; 403: chain or method not in your plan
    } finally {
      p.destroy()
    }
  }),
)
console.log(rows.join('\n'))

// 2. Incoming USDC transfers on Base over the last 20,000 blocks, read in steps of the chain's log limit.
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
const base = provider('base', 8453)
const usdc = new Contract(USDC_BASE, ['function decimals() view returns (uint8)'], base)
const decimals = await usdc.decimals()
const head = await base.getBlockNumber()
const filter = { address: USDC_BASE, topics: [id('Transfer(address,address,uint256)'), null, zeroPadValue(ADDRESS, 32)] }
let total = 0n
for (let from = head - 20_000; from <= head; from += 10_000) {
  const logs = await base.getLogs({ ...filter, fromBlock: from, toBlock: Math.min(from + 9_999, head) })
  for (const log of logs) total += BigInt(log.data)
}
console.log(`USDC received on Base in the last 20,000 blocks: ${formatUnits(total, decimals)}`)
base.destroy()

Run it:

npm install ethers && node balances-ethers.mjs
npm install viem && node watch-viem.mjs
pip install web3 && python backend_web3.py

Download: balances-ethers.mjs · watch-viem.mjs · backend_web3.py · requests.sh. MIT-0: use it as you like.

Rate limits and pagination

  • 1 credit = 1 JSON-RPC call on every chain; a batch of 8 calls counts as 8.
  • eth_getLogs covers at most 10,000 blocks per call (HyperEVM: 50).
  • WebSocket connections per account: RPC Free 2, Starter 10.
  • X-Blockmetric-Credits on each answer says what the request cost (the sum over a batch).
  • Cache finalised blocks and receipts yourself; they never change.
  • Keep the token on the server; a token in a frontend can be copied by anyone.

Common pitfalls

  • Without staticNetwork (ethers) or a chain (viem) the library asks for the chain id on every start and on reconnects.
  • A 429 means requests per second, not the month: back off for the seconds in Retry-After.

Who it is for

  • dApp teams that need reliable reads on several chains
  • Wallet and portfolio builders
  • Indexers that read logs in ranges

Plan and limits

RPC FreeCheapest
1,000,000 credits a month, 10 requests a second, 2 WebSocket connections.
1,000,000 credits a month, 10 requests a second, 2 WebSocket connections
RPC Starter$49/mo
10,000,000 credits a month, 50 requests a second, 10 WebSocket connections, trace methods.
10,000,000 credits a month, 50 requests a second, 10 WebSocket connections

Compare plans

Honest limits

  • Read-only while the beta runs: transaction submission (eth_sendRawTransaction) stays off until its legal review is done.
  • No latency or uptime promise at launch; support by e-mail.

Next steps

More recipes like this

LiveHyperliquid markets 18sOperationalHyperliquid positions & whales 8sOperationalLiquidation heatmap & levels 7sOperationalLighter markets & liquidations 7sOperationalLighter positions & heatmap 7sOperationalLighter RH positions & heatmap 4sOperationalBinance markets 40sOperationalBybit markets 12sOperationalBinance liquidations 30sOperationalBybit liquidations 30sOperationaldYdX positions & heatmap 6sOperationalGMX positions & heatmap 15sOperationalLong/short & open interest history 57sOperationalOrder books & trade flow 7sOperationalOn-chain transfers 6sOperationalSecurity monitor 38sOperationalBlockmetric Sentiment 11mOperationalOfficial news 4mOperationalAlert delivery 4sOperational10:22:49 UTCData: Blockmetric Research · Binance · Bybit