// This skill should be used when the user asks to "create an indexer", "build a hyperindex", "index blockchain events", "write event handlers", "configure config.yaml", "define schema.graphql", "use envio", "set up hyperindex", "index smart contract events", "create graphql schema for blockchain data", or mentions Envio, HyperIndex, blockchain indexing, or event handler development.
| name | HyperIndex Development |
| description | This skill should be used when the user asks to "create an indexer", "build a hyperindex", "index blockchain events", "write event handlers", "configure config.yaml", "define schema.graphql", "use envio", "set up hyperindex", "index smart contract events", "create graphql schema for blockchain data", or mentions Envio, HyperIndex, blockchain indexing, or event handler development. |
| version | 1.0.0 |
HyperIndex is Envio's blazing-fast, developer-friendly multichain blockchain indexer. It transforms on-chain events into structured, queryable databases with GraphQL APIs.
Initialize a new indexer:
pnpx envio init
Run locally:
pnpm dev
Every HyperIndex project contains three core files:
config.yaml - Defines networks, contracts, events to indexschema.graphql - Defines GraphQL entities for indexed datasrc/EventHandlers.ts - Contains event processing logicAfter changes to config.yaml or schema.graphql, run:
pnpm codegen
Requirements:
Key commands:
pnpm codegen - Generate types after config/schema changespnpm tsc --noEmit - Type-check TypeScriptTUI_OFF=true pnpm dev - Run indexer with visible outputBasic structure:
# yaml-language-server: $schema=./node_modules/envio/evm.schema.json
name: my-indexer
networks:
- id: 1 # Ethereum mainnet
start_block: 0 # HyperSync is fast - start from genesis
contracts:
- name: MyContract
address: 0xContractAddress
handler: src/EventHandlers.ts
events:
- event: Transfer(address indexed from, address indexed to, uint256 value)
Key options:
address - Single or array of addressesstart_block - Block to begin indexing. Use 0 with HyperSync (default) - it's extremely fast and syncs millions of blocks in minutes. Only specify a later block if using RPC on unsupported networks.handler - Path to event handler fileevents - Event signatures to indexFor transaction/block data access, use field_selection. By default, event.transaction is {} (empty).
Per-event (recommended) - Only fetch extra fields for events that need them. More fields = more data transfer = slower indexing:
events:
- event: Transfer(address indexed from, address indexed to, uint256 value)
field_selection:
transaction_fields:
- hash
- event: Approval(address indexed owner, address indexed spender, uint256 value)
# No field_selection - this event doesn't need transaction data
Global - Applies to ALL events. Use only when most/all events need the same fields:
field_selection:
transaction_fields:
- hash
Available fields:
transaction_fields: hash, from, to, value, gasPrice, gas, input, nonce, transactionIndex, gasUsed, status, etc.block_fields: miner, gasLimit, gasUsed, baseFeePerGas, size, difficulty, etc.For dynamic contracts (factory pattern), omit address and use contractRegister.
Define entities without @entity decorator:
type Token {
id: ID!
name: String!
symbol: String!
decimals: BigInt!
totalSupply: BigInt!
}
type Transfer {
id: ID!
from: String!
to: String!
amount: BigInt!
token_id: String! # Relationship via _id suffix
blockNumber: BigInt!
timestamp: BigInt!
}
Key rules:
String! instead of Bytes!_id suffix for relationships (e.g., token_id not token)@derivedFrom: transfers: [Transfer!]! @derivedFrom(field: "token")@entity decorators neededBasic handler pattern:
import { MyContract } from "generated";
MyContract.Transfer.handler(async ({ event, context }) => {
const entity = {
id: `${event.chainId}-${event.transaction.hash}-${event.logIndex}`,
from: event.params.from,
to: event.params.to,
amount: event.params.amount,
blockNumber: BigInt(event.block.number),
timestamp: BigInt(event.block.timestamp),
};
context.Transfer.set(entity);
});
Entity updates - Use spread operator (entities are immutable):
const existing = await context.Token.get(tokenId);
if (existing) {
context.Token.set({
...existing,
totalSupply: newSupply,
});
}
Dynamic contract registration (factory pattern):
Factory.PairCreated.contractRegister(({ event, context }) => {
context.addPair(event.params.pair);
});
Factory.PairCreated.handler(async ({ event, context }) => {
// Handle the event...
});
When using preload_handlers: true, external calls MUST use the Effect API:
import { S, createEffect } from "envio";
export const getTokenMetadata = createEffect({
name: "getTokenMetadata",
input: S.string,
output: S.object({
name: S.string,
symbol: S.string,
decimals: S.number,
}),
cache: true,
}, async ({ input: address }) => {
// Fetch token metadata via RPC
return { name: "Token", symbol: "TKN", decimals: 18 };
});
// In handler:
MyContract.Event.handler(async ({ event, context }) => {
const metadata = await context.effect(getTokenMetadata, event.params.token);
});
Multichain IDs - Prefix with chainId:
const id = `${event.chainId}-${event.params.tokenId}`;
Timestamps - Always cast to BigInt:
timestamp: BigInt(event.block.timestamp)
Address consistency - Use lowercase:
const address = event.params.token.toLowerCase();
BigDecimal precision - Import from generated:
import { BigDecimal } from "generated";
const ZERO_BD = new BigDecimal(0);
Logging in handlers:
context.log.debug("Detailed info");
context.log.info("Processing transfer", { from, to, value });
context.log.warn("Large transfer detected");
context.log.error("Failed to process", { error, txHash });
Run with visible output:
TUI_OFF=true pnpm dev
Log levels via env vars:
LOG_LEVEL="debug" # Show debug logs (default: "info")
LOG_LEVEL="trace" # Most verbose
Common issues checklist:
await on context.Entity.get()field_selection for transaction dataSee references/logging-debugging.md for structured logging, log strategies, and troubleshooting patterns.
Index data on every block (or interval) without specific events:
import { Ethereum } from "generated";
Ethereum.onBlock(
async ({ block, context }) => {
context.BlockStats.set({
id: `${block.number}`,
number: BigInt(block.number),
timestamp: BigInt(block.timestamp),
gasUsed: block.gasUsed,
});
},
{ interval: 100 } // Every 100 blocks
);
See references/block-handlers.md for intervals, multichain, and preset handlers.
Index the same contract across multiple chains:
networks:
- id: 1 # Ethereum
start_block: 0
contracts:
- name: MyToken
address: 0x...
- id: 137 # Polygon
start_block: 0
contracts:
- name: MyToken
address: 0x...
Important: Use chain-prefixed IDs to prevent collisions:
const id = `${event.chainId}_${event.params.tokenId}`;
See references/multichain-indexing.md for ordered vs unordered mode.
Index events across all contracts (no address specified):
ERC20.Transfer.handler(
async ({ event, context }) => {
context.Transfer.set({
id: `${event.chainId}_${event.block.number}_${event.logIndex}`,
token: event.srcAddress, // The actual contract
from: event.params.from,
to: event.params.to,
});
},
{ wildcard: true }
);
See references/wildcard-indexing.md for topic filtering.
Unit test handlers with MockDb:
import { TestHelpers } from "generated";
const { MockDb, MyContract, Addresses } = TestHelpers;
it("creates entity on event", async () => {
const mockDb = MockDb.createMockDb();
const event = MyContract.Transfer.createMockEvent({
from: Addresses.defaultAddress,
to: "0x456...",
value: BigInt(1000),
});
const updatedDb = await mockDb.processEvents([event]);
const transfer = updatedDb.entities.Transfer.get("...");
assert.ok(transfer);
});
See references/testing.md for complete patterns.
When running pnpm dev, query indexed data via GraphQL at http://localhost:8080/v1/graphql.
Check indexing progress first (always do this before assuming data is missing):
curl -s http://localhost:8080/v1/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ _meta { chainId startBlock progressBlock sourceBlock eventsProcessed isReady } }"}'
progressBlock - Current processed blocksourceBlock - Latest block on chain (target)isReady - true when fully syncedQuery entities:
curl -s http://localhost:8080/v1/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ Transfer(limit: 10, order_by: {blockNumber: desc}) { id chainId from to amount blockNumber } }"}'
Filter by chain (multichain):
curl -s http://localhost:8080/v1/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ Transfer(where: {chainId: {_eq: 42161}}, limit: 10) { id from to amount } }"}'
Common filter operators: _eq, _neq, _gt, _gte, _lt, _lte, _in, _like
Tip: BigInt values must be quoted strings in filters: {amount: {_gt: "1000000000000000000"}}
See references/local-querying.md for comprehensive query patterns, pagination, and debugging tips.
Optimize query performance with @index:
type Transfer {
id: ID!
from: String! @index
to: String! @index
timestamp: BigInt! @index
}
type Swap @index(fields: ["pair", "timestamp"]) {
id: ID!
pair_id: String! @index
timestamp: BigInt!
}
See references/database-indexes.md for optimization tips.
Important: Handlers run TWICE when
preload_handlers: true(default since v2.27).
This flagship feature reduces database roundtrips from thousands to single digits:
// Phase 1 (Preload): All handlers run concurrently, reads are batched
// Phase 2 (Execution): Handlers run sequentially, reads come from cache
MyContract.Event.handler(async ({ event, context }) => {
// Use Promise.all for concurrent reads
const [sender, receiver] = await Promise.all([
context.Account.get(event.params.from),
context.Account.get(event.params.to),
]);
// Skip non-essential logic during preload
if (context.isPreload) return;
// Actual processing (only runs in execution phase)
context.Transfer.set({ ... });
});
Critical rule: Never call fetch() or external APIs directly. Use the Effect API.
See references/preload-optimization.md for the full mental model and best practices.
Deploy to Envio's hosted service for production-ready infrastructure:
# Production config
name: my-indexer
rollback_on_reorg: true # Always enable for production
networks:
- id: 1
start_block: 18000000
confirmed_block_threshold: 250 # Reorg protection
Pre-deployment checklist:
pnpm codegen - Generate typespnpm tsc --noEmit - Type checkTUI_OFF=true pnpm dev - Test locally with visible logsSee references/deployment.md for hosted service setup and references/reorg-support.md for chain reorganization handling.
For detailed patterns and advanced techniques, consult:
Core Concepts:
references/config-options.md - Complete config.yaml optionsreferences/effect-api.md - External calls and RPC patternsreferences/entity-patterns.md - Entity relationships and updatesreferences/preload-optimization.md - How preload works, common footgunsAdvanced Features:
references/block-handlers.md - Block-level indexing with intervalsreferences/multichain-indexing.md - Ordered vs unordered modereferences/wildcard-indexing.md - Topic filtering, dynamic contractsreferences/contract-state.md - Read on-chain state via RPC/viemreferences/rpc-data-source.md - RPC config and fallbackOperations:
references/logging-debugging.md - Logging, TUI, troubleshootingreferences/graphql-querying.md - Query indexed data, check progress, debugreferences/database-indexes.md - Index optimizationreferences/testing.md - MockDb and test patternsProduction:
references/deployment.md - Hosted service deploymentreferences/reorg-support.md - Chain reorganization handlingWorking examples in examples/:
examples/basic-handler.ts - Simple event handlerexamples/factory-pattern.ts - Dynamic contract registration