Skip to main content

Migrate from Ponder to HyperIndex

Need help? Reach out on Discord for personalized migration assistance.

Migrating from Ponder to HyperIndex is straightforward — both frameworks use TypeScript, index EVM events, and expose a GraphQL API. The key differences are the config format, schema syntax, and entity operation API.

If you are new to HyperIndex, start with the Getting Started guide first.

Prefer AI-assisted migration?

For an assistant-led workflow, see How to Migrate Using AI, which includes a shared process for Cursor and Claude Code.

Why Migrate to HyperIndex?

  • Up to 158x faster historical sync via HyperSync
  • Multichain by default — index any number of chains in one config
  • Same language — your TypeScript logic transfers directly

Migration Overview

Migration has three steps:

  1. ponder.config.tsconfig.yaml
  2. ponder.schema.tsschema.graphql
  3. Event handlers — adapt syntax and entity operations

At any point, run:

pnpm envio codegen   # validate config + schema, regenerate types
pnpm dev # run the indexer locally

Step 0: Bootstrap the Project

pnpx envio init

This generates a config.yaml, a starter schema.graphql, and handler stubs. Use your Ponder project as the source of truth for contract addresses, ABIs, and events, then fill in the generated files.


Step 1: ponder.config.tsconfig.yaml

Ponder

import { createConfig } from "ponder";

export default createConfig({
chains: {
mainnet: { id: 1, rpc: process.env.PONDER_RPC_URL_1 },
},
contracts: {
MyToken: {
abi: myTokenAbi,
chain: "mainnet",
address: "0xabc...",
startBlock: 18000000,
},
},
});

HyperIndex (v3)

# yaml-language-server: $schema=./node_modules/envio/evm.schema.json
name: my-indexer

contracts:
- name: MyToken
abi_file_path: ./abis/MyToken.json
handler: ./src/EventHandlers.ts
events:
- event: Transfer
- event: Approval

chains:
- id: 1
start_block: 0
contracts:
- name: MyToken
address:
- 0xabc...
start_block: 18000000

v2 note: HyperIndex v2 uses networks instead of chains. See the v2→v3 migration guide.

Key differences:

ConceptPonderHyperIndex
Config formatponder.config.ts (TypeScript)config.yaml (YAML)
Chain referenceNamed + viem objectNumeric chain ID
RPC URLIn configRPC_URL_<chainId> env var
ABI sourceTypeScript importJSON file (abi_file_path)
Events to indexInferred from handlersExplicit events: list
Handler fileInferredExplicit handler: per contract

Convert your ABI: Ponder uses TypeScript ABI exports (as const). HyperIndex needs a plain JSON file in abis/. Strip the export const ... = wrapper and as const and save as .json.

Field selection — accessing transaction and block fields

By default, only a minimal set of fields is available on event.transaction and event.block. Fields like event.transaction.hash are undefined unless explicitly requested.

events:
- event: Transfer
field_selection:
transaction_fields:
- hash

Or declare once at the top level to apply to all events:

name: my-indexer

field_selection:
transaction_fields:
- hash

contracts:
# ...

See the full list of available fields in the Configuration File docs.


Step 2: ponder.schema.tsschema.graphql

Ponder

import { onchainTable, primaryKey, index } from "ponder";

export const token = onchainTable("token", (t) => ({
address: t.hex().primaryKey(),
symbol: t.text().notNull(),
balance: t.bigint().notNull(),
}));

export const transferEvent = onchainTable(
"transfer_event",
(t) => ({
id: t.text().primaryKey(),
from: t.hex().notNull(),
to: t.hex().notNull(),
amount: t.bigint().notNull(),
timestamp: t.integer().notNull(),
}),
(table) => ({
fromIdx: index().on(table.from),
}),
);

HyperIndex

type Token {
id: ID!
symbol: String!
balance: BigInt!
}

type TransferEvent {
id: ID!
from: String! @index
to: String!
amount: BigInt!
timestamp: Int!
}

Type mapping:

PonderHyperIndex GraphQL
t.hex()String!
t.text()String!
t.bigint()BigInt!
t.integer()Int!
t.boolean()Boolean!
t.real() / t.doublePrecision()Float!
t.hex().array()Json!

Primary keys: HyperIndex requires a single id: ID! string field on every entity. For composite PKs (e.g. owner + spender), construct the ID string manually: `${owner}_${spender}`.

Indexes: Replace index().on(column) with an @index directive on the field.

Relations: Replace Ponder's relations() call with @derivedFrom on the parent entity:

type Token {
id: ID!
transfers: [TransferEvent!]! @derivedFrom(field: "token_id")
}

See the full Schema docs.

Step 3: Event Handlers

Handler registration

Ponder

import { ponder } from "ponder:registry";

ponder.on("MyToken:Transfer", async ({ event, context }) => {
// ...
});

HyperIndex

import { MyToken } from "generated";

MyToken.Transfer.handler(async ({ event, context }) => {
// ...
});

Event data access

DataPonderHyperIndex
Event parametersevent.args.nameevent.params.name
Contract addressevent.log.addressevent.srcAddress
Chain IDcontext.chain.idevent.chainId
Block numberevent.block.numberevent.block.number
Block timestampevent.block.timestamp (bigint)event.block.timestamp (number)
Tx hashevent.transaction.hashevent.transaction.hash ⚠️ needs field_selection

Entity operations

IntentPonderHyperIndex
Insertcontext.db.insert(t).values({...})context.Entity.set({ id, ...fields })
Updatecontext.db.update(t, pk).set({...})get → spread → context.Entity.set({ ...existing, ...changes })
Upsert.insert().values().onConflictDoUpdate()context.Entity.getOrCreate({ id, ...defaults })set
Read (nullable)context.db.find(table, pk)context.Entity.get(id)
Read (throws)manual null checkcontext.Entity.getOrThrow(id)

Full handler example

Ponder

ponder.on("MyToken:Transfer", async ({ event, context }) => {
await context.db.insert(transferEvent).values({
id: event.id,
from: event.args.from,
to: event.args.to,
amount: event.args.amount,
timestamp: Number(event.block.timestamp),
});

await context.db
.update(token, { address: event.args.to })
.set((row) => ({ balance: row.balance + event.args.amount }));
});

HyperIndex

import { MyToken } from "generated";

MyToken.Transfer.handler(async ({ event, context }) => {
context.TransferEvent.set({
id: `${event.transaction.hash}_${event.logIndex}`,
from: event.params.from,
to: event.params.to,
amount: event.params.amount,
timestamp: event.block.timestamp,
});

const token = await context.Token.getOrThrow(event.params.to);
context.Token.set({
...token,
balance: token.balance + event.params.amount,
});
});

Important: Entity objects from context.Entity.get() are read-only. Always spread (...existing) and set new fields — never mutate directly.

See the Event Handlers docs for the full API reference.

Extra Tips

Factory contracts (dynamic registration)

Replace Ponder's factory() helper in config with a contractRegister handler:

import { MyFactory } from "generated";

// Registers each newly deployed contract for indexing
MyFactory.ContractCreated.contractRegister(({ event, context }) => {
context.addMyContract(event.params.contractAddress);
});

In config.yaml, omit the address field for the dynamically registered contract.

External calls

Replace context.client.readContract(...) with the Effect API to safely isolate external calls from the sync path:

import { createEffect, S } from "envio";

export const getSymbol = createEffect(
{
name: "getSymbol",
input: S.schema({ address: S.string, chainId: S.number }),
output: S.string,
cache: true,
},
async ({ input }) => {
/* viem call here */
},
);

// In handler:
const symbol = await context.effect(getSymbol, {
address,
chainId: event.chainId,
});

Multichain

Add multiple entries under chains: and namespace your entity IDs by chain to prevent collisions:

const id = `${event.chainId}_${event.params.tokenId}`;

See Multichain Indexing for configuration details.

Wildcard indexing

HyperIndex supports wildcard indexing — index events by signature across all contracts on a chain without specifying addresses.

Validating Your Migration

Use the Indexer Migration Validator CLI to compare entity data between your Ponder and HyperIndex endpoints field-by-field.

Getting Help