WritingBuilding a Production Custom Blockchain Indexer with Ponder — LinkIT
5 min readponder, custom-indexer, blockchain-indexing, typescript, advanced

Building a Production Custom Blockchain Indexer with Ponder

Ponder indexer architecture: schema design, event handlers, RPC data, deployment, monitoring, and reorg testing.

A custom indexer may be useful when hosted subgraphs cannot support required data models, handler logic, or API behavior. Ponder provides a TypeScript framework for building that layer.

This is an advanced guide for teams that have already evaluated hosted subgraphs and decided they need more control.

When a Custom Indexer Is the Right Call

Before investing in a custom indexer, confirm that subgraphs genuinely cannot meet your requirements. The most common legitimate reasons to build custom:

  • Your handler logic requires conditional branching, external lookups, or business logic that is difficult to express in AssemblyScript
  • You need to expose a custom REST or WebSocket API alongside GraphQL
  • Your data model requires joins or aggregations that subgraph GraphQL does not support efficiently
  • You are indexing a chain or contract type not well-supported by The Graph
  • You need to combine on-chain data with off-chain data sources in the same query layer

If TypeScript is the main requirement, compare managed alternatives such as Envio before taking on hosting and operations.

What Ponder Is

Ponder is an open-source TypeScript indexing framework. It ingests chain data, decodes events, runs handlers, and maintains indexed state through reorgs. Current versions use PGlite for local development or Postgres, with Postgres required for production; see the database guide.

  • RPC data ingestion and event decoding
  • Typed handler inputs based on contract ABIs
  • Database access and schema definitions
  • Query APIs over indexed data
  • Reorg detection and rollback within the configured window

For a self-hosted deployment, the team owns capacity planning, monitoring, backups, and release operations. Ponder supports separate HTTP serving instances; scaling those is different from distributing indexing work.

Building a Custom Blockchain Indexer: Schema Design in Ponder

The following schema and handler examples use the legacy pre-0.7 API. They illustrate the data model and event flow, but need migration before use with a current release. Ponder 0.7 introduced the Drizzle-based onchainTable API; consult the migration guide.

import { createSchema } from "@ponder/core";
 
export default createSchema((p) => ({
  Pool: p.createTable({
    id: p.string(),
    token0: p.string(),
    token1: p.string(),
    totalVolumeUSD: p.float(),
    swapCount: p.int(),
    createdAt: p.int(),
  }),
  Swap: p.createTable({
    id: p.string(),
    poolId: p.string().references("Pool.id"),
    amountInUSD: p.float(),
    amountOutUSD: p.float(),
    timestamp: p.int(),
    blockNumber: p.int(),
  }),
}));

Current Ponder versions use Drizzle-based schema definitions. With Postgres, indexed tables can also be queried through SQL; account for deployment-specific database schemas.

Handler Patterns in Ponder

Handlers in Ponder are TypeScript functions. The framework provides typed event objects based on your contract ABIs:

import { ponder } from "@/generated";
 
ponder.on("UniswapV3Pool:Swap", async ({ event, context }) => {
  const { Pool, Swap } = context.db;
 
  const pool = await Pool.findUnique({ id: event.log.address });
  if (!pool) return;
 
  await Pool.update({
    id: event.log.address,
    data: {
      swapCount: pool.swapCount + 1,
      totalVolumeUSD: pool.totalVolumeUSD + calculateUSD(event.args),
    },
  });
 
  await Swap.create({
    id: `${event.transaction.hash}-${event.log.logIndex}`,
    data: {
      poolId: event.log.address,
      amountInUSD: calculateUSD(event.args),
      amountOutUSD: calculateUSD(event.args),
      timestamp: Number(event.block.timestamp),
      blockNumber: Number(event.block.number),
    },
  });
});

TypeScript handlers can call shared application modules. Keep indexing logic deterministic and account for replay before introducing external calls or side effects.

Handling Invalid RPC Data

RPC responses can be incomplete or inconsistent. Use the framework’s validation and recovery behavior, and add domain checks where the application needs them. Do not silently skip failed events and treat the resulting index as complete.

Defensive handler patterns:

  • Check numeric values against domain constraints before storing derived results
  • Log failures with block, transaction, and event context; rethrow errors that must stop processing rather than swallowing them
  • Check zero divisors, precision, and conversions in numeric calculations

Deployment Architecture

Ponder runs as a long-lived Node.js process. For production deployment:

Compute: Benchmark backfill and live indexing with representative contracts and RPC limits. Size CPU and memory from measured throughput rather than a generic instance recommendation.

Database: Use Postgres for production and test the selected provider’s connection limits and deployment behavior. Keep database latency low and isolate deployment schemas as described in the self-hosting guide.

Restart strategy: Configure a supervisor and test crash recovery with the same configuration and database schema. Recovery can roll back recent unfinalized work before replaying; do not assume it resumes at the exact last processed event.

Monitoring: Track handler failures, RPC errors, and sync lag. Set alerts from the freshness the application requires and the chain’s block cadence.

Reorg Handling Verification

Ponder's reorg handling is built in, but verify it explicitly before launch. Run your indexer against a testnet or local Anvil fork, manually cause a reorg by mining competing blocks, and confirm that entity state rolls back correctly. The specific behavior to verify: an entity updated in block N should revert to its pre-block-N value after a reorg that removes block N.

When to Switch Away from Ponder

Reassess the deployment when:

  • Your event volume is high enough that a single Node.js process cannot keep up with chain tip
  • You need horizontal scale across multiple indexer workers
  • Your team does not have the capacity to maintain the deployment and monitoring

Check whether tuning, separate query-serving capacity, or a different indexing service meets the requirement. Compare migration and operational costs before switching.


Discuss a custom blockchain indexer with LinkIT.