> For the complete documentation index, see [llms.txt](https://docs.envio.dev/llms.txt).

![Cover Image What is Multichain Indexing?](/blog-assets/what-is-multi-chain-indexing.png)

:::note TL;DR

- Multichain indexing reads events from several blockchains into one database and serves them through one API, so your app runs one indexer instead of one per chain.
- In [HyperIndex](https://docs.envio.dev/docs/HyperIndex/overview) you define a contract once and list each chain under `chains` in `config.yaml`, with its own start block and address. The same handlers run on every chain.
- Each chain is processed in order, and chains don't wait for each other, so a slow chain doesn't hold back data from the others.
- When one chain is missing events, check that chain's start block, address and reorg setting before the handler.
- The three-chain indexer below tracks every Uniswap V3 pool on Ethereum, Polygon and Arbitrum in three files. Copy it, or [hand it to your coding agent](#build-it-with-your-coding-agent) with our prompt.

:::

We wanted to see how much work it takes to index one contract on three chains at once, so we built a small indexer that records every pool the Uniswap V3 factory has created on Ethereum, Polygon and Arbitrum. It came to three short files and one `pnpm envio dev`. The parts worth explaining are the ones that differ per chain, because that is where a multichain indexer needs the most care once it runs in production.

Multichain describes an app or protocol that lives on more than one blockchain. Multichain indexing is how you read its data. Rather than running one indexer and one API per chain and stitching the results together in your app, a single indexer reads every chain, writes to one database, and serves it all through one [GraphQL API](https://docs.envio.dev/docs/HyperIndex/navigating-hasura). New to indexers in general? Start with [what a blockchain indexer does](https://docs.envio.dev/blog/what-is-a-blockchain-indexer).

## How One Indexer Reads Several Chains

A HyperIndex `config.yaml` has two halves. The `contracts` section describes a contract once, with its events and handler file. The `chains` section lists each chain by ID, and each entry says where that contract lives on that chain and which block to start from. Your handlers don't change per chain. Every event carries `event.chainId`, so a handler always knows where it came from.

Each chain is read on its own. Events on one chain are processed in block order, and chains move independently of each other, so Polygon doesn't wait for Ethereum to finish a block before its events are indexed. That keeps the data close to each chain's head, which matters if you need real-time data from several chains at once. The trade-off is that events from two different chains are not processed in a guaranteed order relative to each other. The [multichain docs](https://docs.envio.dev/docs/HyperIndex/multichain-indexing#ordered-multichain-mode) explain why there is no ordered mode and show the pattern to use when two chains feed one record, such as a bridge message.

One indexer covers one chain family. EVM chains index together in the same indexer. Solana and Fuel are each their own indexer.

## A Working Config for Ethereum, Polygon and Arbitrum

The Uniswap V3 factory sits at the same address on all three chains, `0x1F98431c8aD98523631AE4a59f267346ea31F984`, and emits `PoolCreated` each time a pool is deployed. We looked up the block of the first `PoolCreated` event on each chain with [HyperSync](https://docs.envio.dev/docs/HyperSync/overview) and used those as the start blocks.

You need Node.js 22.15 or newer, [Docker](https://www.docker.com/products/docker-desktop/) for the local Postgres and Hasura, and an [API token](https://docs.envio.dev/docs/HyperSync/api-tokens).

```bash
mkdir uniswap-v3-pools-multichain && cd uniswap-v3-pools-multichain
pnpm init
pnpm pkg set type=module
pnpm add envio
```

### config.yaml

```yaml
# yaml-language-server: $schema=./node_modules/envio/evm.schema.json
name: uniswap-v3-pools-multichain
disable_default_cross_chain: true
contracts:
  - name: UniswapV3Factory
    handler: src/EventHandlers.ts
    events:
      - event: "PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)"
chains:
  - id: 1 # Ethereum
    start_block: 12369739
    contracts:
      - name: UniswapV3Factory
        address: "0x1F98431c8aD98523631AE4a59f267346ea31F984"
  - id: 137 # Polygon
    start_block: 22757913
    contracts:
      - name: UniswapV3Factory
        address: "0x1F98431c8aD98523631AE4a59f267346ea31F984"
  - id: 42161 # Arbitrum
    start_block: 185
    contracts:
      - name: UniswapV3Factory
        address: "0x1F98431c8aD98523631AE4a59f267346ea31F984"
```

`disable_default_cross_chain: true` turns on [per-chain data mode](https://docs.envio.dev/docs/HyperIndex/multichain-indexing#per-chain-data-mode), which the docs recommend for multichain indexers. Every per-chain entity gets a `chainId` column, and the same ID on two chains is stored as two rows, so you don't need to prefix IDs with the chain.

### schema.graphql

```graphql
type Pool {
  id: ID!
  token0: String!
  token1: String!
  fee: Int!
  tickSpacing: Int!
  createdAtBlock: Int!
}
```

### src/EventHandlers.ts

```typescript
import { indexer } from "envio";

indexer.onEvent(
  { contract: "UniswapV3Factory", event: "PoolCreated" },
  async ({ event, context }) => {
    context.Pool.set({
      id: event.params.pool,
      token0: event.params.token0,
      token1: event.params.token1,
      fee: Number(event.params.fee),
      tickSpacing: Number(event.params.tickSpacing),
      createdAtBlock: event.block.number,
    });
  },
);
```

`fee` and `tickSpacing` arrive as BigInt like every numeric event parameter, so they're wrapped in `Number()` to fit the `Int!` fields.

### Running It

Put your token in a `.env` file in the project root, then start the indexer.

```bash
echo "ENVIO_API_TOKEN=your_token_here" > .env
pnpm envio dev
```

The GraphQL endpoint is at `http://localhost:8080/v1/graphql` (the local Hasura admin secret is `testing`). The `_meta` query from the [observability docs](https://docs.envio.dev/docs/HyperIndex/observability#indexing-status) shows each chain's progress separately, which is the quickest way to see whether one chain is behind.

```graphql
{
  _meta {
    chainId
    progressBlock
    eventsProcessed
    isReady
  }
}
```

To read one chain, filter on `chainId`.

```graphql
{
  Pool(where: { chainId: { _eq: 137 } }, limit: 2) {
    id
    chainId
    token0
    fee
  }
}
```

When we ran it, all three chains reached the head and the indexer held more than 140,000 pools. To check nothing was dropped, we counted the factory's `PoolCreated` logs on each chain straight from HyperSync up to a fixed block and compared them with the indexer's rows below that block.

| Chain | Counted up to block | HyperSync logs | Indexed pools |
| --- | --- | --- | --- |
| Ethereum | 26,256,000 | 74,139 | 74,139 |
| Polygon | 96,043,000 | 38,842 | 38,842 |
| Arbitrum | 517,570,000 | 27,136 | 27,136 |

*Uniswap V3 pools created per chain, counted on 23rd October 2026.*

Every chain matched exactly. Each pool address appears once per chain, and with per-chain data mode on, the same address on two chains would still be stored as two separate rows.

## Getting Each Chain's Config Right

Most of a multichain config is shared, so the settings that change from chain to chain are the ones to get right. The [missing events checklist](https://docs.envio.dev/docs/HyperIndex/common-issues#events-not-appearing-in-indexed-data) covers setup in general. These are the settings to check for each chain you add.

- **A start block copied from another chain.** Block numbers don't line up across chains. Copy Ethereum's `12369739` onto Arbitrum in the config above and the indexer skips every pool Arbitrum created before its own block 12,369,739. Set `start_block` per chain, at or before that chain's deployment.
- **A different address on one chain.** Some protocols deploy at the same address everywhere, many don't. Check every chain's address against the protocol's own deployment list.
- **One chain overwriting another.** Without per-chain data mode, an entity ID that appears on two chains is one row, so the second chain's write replaces the first and it looks like events went missing. Turn on `disable_default_cross_chain: true`, or prefix IDs with `event.chainId`.
- **Reorgs on Arbitrum and OP Mainnet.** [Reorg rollback](https://docs.envio.dev/docs/HyperIndex/reorgs-support) covers 200 blocks on most chains, but Arbitrum and OP Mainnet default to 0, so a reorg there isn't rolled back. Set `max_reorg_depth` on those chains if you want it.
- **An event signature that differs on one chain.** If one chain runs an older version of the contract, its event can have a different signature or different `indexed` keywords, and those logs are skipped without an error. Add a separate contract entry for that version, with its own address and matching signature.

## Adding a Chain to a Running Indexer

Adding a chain is a config change. Add a new entry under `chains` with its ID, start block and contract address, and the handlers pick it up as they are.

On [Envio Cloud](https://docs.envio.dev/docs/HyperIndex/hosted-service-deployment#adding-a-new-chain-to-your-indexer), push the change to your deployment branch. That starts a new deployment that indexes every chain from its start block, including the new one, while the previous deployment keeps serving queries until the new one has synced, so the API your app reads from stays up throughout. Locally, restart with `pnpm envio dev -r`, which clears the database and indexes every chain from scratch.

## Build It With Your Coding Agent

If you'd rather hand this to a coding agent, give it current docs first. Connect the [Envio MCP server](https://docs.envio.dev/docs/HyperIndex/mcp-server) or let it run `envio tools search-docs`, so it writes current HyperIndex syntax instead of what it remembers. Then paste the prompt below.

<details>
<summary>Show the full prompt</summary>

```text
Build a HyperIndex multichain indexer that records every Uniswap V3 pool
created on Ethereum (1), Polygon (137) and Arbitrum (42161).

Before writing code, read the current HyperIndex docs through the Envio MCP
server or `envio tools search-docs`, especially the multichain indexing page
and the configuration file page. Do not rely on memory for config keys.

Setup
- Node.js 22.15 or newer and Docker are required.
- mkdir uniswap-v3-pools-multichain && cd uniswap-v3-pools-multichain
- pnpm init, pnpm pkg set type=module, pnpm add envio
- Put ENVIO_API_TOKEN=<token> in .env in the project root. Ask me for the
  token if you don't have one.

Config (config.yaml in the project root, with `name: uniswap-v3-pools-multichain`)
- One contract, UniswapV3Factory, with `handler: src/EventHandlers.ts`, defined once under `contracts`, with the
  event "PoolCreated(address indexed token0, address indexed token1,
  uint24 indexed fee, int24 tickSpacing, address pool)". Keep every
  `indexed` keyword exactly as written, or logs are skipped silently.
- Three entries under `chains`, all with address
  0x1F98431c8aD98523631AE4a59f267346ea31F984 and these start blocks:
  Ethereum 12369739, Polygon 22757913, Arbitrum 185. Do not reuse one
  chain's start block on another.
- Set `disable_default_cross_chain: true` at the top level so entities are
  stored per chain. This adds a `chainId` column to every entity, which
  the queries below filter on. Do not prefix ids with the chain id.

Schema and handler (schema.graphql in the project root, handler in
src/EventHandlers.ts)
- One entity, Pool: id (pool address), token0, token1, fee (Int),
  tickSpacing (Int), createdAtBlock (Int, from event.block.number).
- Use `indexer.onEvent` from "envio". Numeric params arrive as BigInt, so
  wrap fee and tickSpacing in Number().

Run and check
- Start with `pnpm envio dev`. GraphQL is at http://localhost:8080/v1/graphql, admin
  secret "testing". `_meta` can be missing for the first few seconds while
  the tables are set up, so retry.
- Query `_meta { chainId progressBlock eventsProcessed isReady }` until all
  three chains report isReady: true. If the run stops, `pnpm envio dev`
  resumes from where it was. Only `-r` starts again from scratch.
- Report back the pool count per chain from
  Pool_aggregate(where: { chainId: { _eq: <id> } }) { aggregate { count } }
  and the block each chain reached. Report the numbers you actually got,
  not figures from a blog. Stop the indexer with `pnpm envio stop` when
  you're done.
```

</details>

## Frequently Asked Questions

### Why Is My Multichain Indexer Missing Events on One Chain?

Check that chain's entry in `config.yaml` first. The usual causes are a start block copied from another chain, a contract address that differs on that chain, or an event signature that doesn't match the version deployed there. If events seem to disappear after being indexed, two chains may be writing the same entity ID, which per-chain data mode prevents. On Arbitrum and OP Mainnet, reorg rollback is off by default.

### How Do I Get Real-Time Data From Several Chains at Once?

Run one HyperIndex indexer with every chain listed under `chains`. Each chain is processed as soon as its events are available, without waiting for the other chains, so each one stays close to its own head. The `_meta` GraphQL query shows how far each chain has got, which you can use to check that a chain has caught up before reading its data.

### Can One HyperIndex Indexer Cover EVM Chains and Solana Together?

No. One indexer covers one chain family. Any number of EVM chains can share an indexer, and Solana and Fuel each need their own.

### Does Adding a Chain Mean Re-Indexing the Others?

Yes. A new deployment on Envio Cloud indexes every chain from its start block, including the new one. The previous deployment keeps serving queries until the new one has synced, so your API stays up during the switch. Locally, `pnpm envio dev -r` clears the database and starts again.

### Is There a Production Multichain Indexer I Can Study?

The [Uniswap V4 indexer](https://docs.envio.dev/docs/HyperIndex/example-uniswap-v4-multi-chain-indexer) that powers [v4.xyz](https://v4.xyz) is the reference we point people to. It indexes the v4 PoolManager across many chains from one config, and the [source is on GitHub](https://github.com/enviodev/uniswap-v4-indexer).

### How Does Envio's Multichain Support Compare With Other Indexers?

Our [comparison page](https://envio.dev/compare) covers multichain support alongside benchmarks and other capabilities, with every cell linked to its source. For HyperIndex configuration in depth, see the [multichain docs](https://docs.envio.dev/docs/HyperIndex/multichain-indexing).

## Build With Envio

Envio builds custom blockchain APIs and analytics. Teams use [HyperIndex](https://docs.envio.dev/docs/HyperIndex/overview) to turn contract activity across EVM chains, Solana and Fuel into a real-time GraphQL API, and run it on [Envio Cloud](https://docs.envio.dev/docs/HyperIndex/hosted-service), from a free development plan up to Dedicated deployments with an SLA. If you're building a multichain app, come talk to us about your data needs.

[Subscribe to our newsletter](https://envio.beehiiv.com/subscribe?utm_source=envio.beehiiv.com&utm_medium=newsletter&utm_campaign=new-post)

[Website](https://envio.dev/) | [X](https://twitter.com/envio_indexer) | [Discord](https://discord.gg/envio) | [Telegram](https://t.me/+5mI61oZibEM5OGQ8) | [GitHub](https://github.com/enviodev) | [YouTube](https://www.youtube.com/channel/UCR7nZ2yzEtc5SZNM0dhrkhA) | [Reddit](https://www.reddit.com/user/Envio_indexer)
