# Building a Production Custom Blockchain Indexer with Ponder

> An advanced guide to building a production-grade custom blockchain indexer using Ponder — schema design, handler patterns, deployment, and operational concerns.

- **Published:** 2026-05-15
- **Author:** Clixo
- **Reading time:** 6 min read
- **Tags:** ponder, custom-indexer, blockchain-indexing, typescript, advanced
- **Canonical URL:** https://clixo.sh/blog/production-custom-blockchain-indexer-ponder

Subgraphs solve 80% of indexing problems well. The remaining 20% — multi-contract state reconstruction, complex business logic in handlers, data shapes that subgraph schema cannot cleanly express, or requirements for custom API logic — is where teams reach for a custom indexer. Ponder is currently the most mature TypeScript-native framework for this work.

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 your reason is "we want TypeScript instead of AssemblyScript," Envio is a managed alternative worth evaluating first — it gives you TypeScript handlers with managed infrastructure.

## What Ponder Is

Ponder is an open-source framework for building custom blockchain indexers in TypeScript. You define your schema using Ponder's schema DSL, write event handlers in TypeScript, and run the indexer locally or in a production environment. Ponder handles:

- Block ingestion from your RPC endpoints
- Event decoding using viem and your contract ABIs
- Database writes via Drizzle ORM to a SQLite (local) or Postgres (production) backend
- A generated GraphQL API for the entities in your schema
- Reorg detection and rollback

What Ponder does not handle: running the indexer in a managed cloud environment, scaling the sync horizontally across multiple workers, or alerting on data drift. That is operational work you own.

```mermaid
flowchart LR
  A["RPC Endpoint"] --> B["Block Ingestion"]
  B --> C["Event Decoding\nviem + ABI"]
  C --> D["TypeScript Handlers"]
  D --> E[("Postgres DB\nDrizzle ORM")]
  E --> F["GraphQL API"]
  B --> G["Reorg Detection"]
  G --> E
```

## Building a Custom Blockchain Indexer: Schema Design in Ponder

Ponder's schema is defined in `ponder.schema.ts` using a TypeScript DSL:

```typescript
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(),
  }),
}));
```

The generated Drizzle schema maps directly to Postgres tables. This means you can query your indexed data with raw SQL or any Postgres-compatible analytics tool alongside the generated GraphQL endpoint.

## Handler Patterns in Ponder

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

```typescript
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),
    },
  });
});
```

Because handlers are plain TypeScript, you can import utility libraries, call your own modules, and write conditional logic without the constraints of AssemblyScript. This is the primary advantage over subgraphs.

## Handling Invalid RPC Data

One operational risk with custom indexers: RPC providers occasionally return malformed or invalid responses — incorrect transaction receipts, missing logs, or inconsistent block data. Unlike a managed subgraph platform that may absorb these errors silently, a custom indexer will crash or write corrupt state if you do not validate inputs.

Defensive handler patterns:

- Validate that numeric values are within expected bounds before writing to the database
- Wrap handler logic in try-catch and log errors with full context (block number, transaction hash, event name) for investigation
- For critical numeric operations, check for negative values, zero divisors, and overflow conditions explicitly

## Deployment Architecture

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

**Compute**: A single Ponder process on a dedicated VPS or container is sufficient for most protocols. AWS EC2 `t3.medium` or `c5.large` covers the majority of cases. CPU and memory requirements scale with event volume per block.

**Database**: Use a managed Postgres instance. PlanetScale, Supabase, Neon, or AWS RDS all work. Ponder's Drizzle layer handles the schema migration. Enable connection pooling (PgBouncer or equivalent) if you have many concurrent readers hitting the database.

**Restart strategy**: Ponder checkpoints its sync progress. On restart, it resumes from the last indexed block. Use a process supervisor (PM2, systemd, or a container orchestrator) to auto-restart on crash.

**Monitoring**: Export handler error rates and sync lag (current block minus indexed block) to your observability stack. Alert when sync lag exceeds two minutes for a live chain, or when handler errors spike.

## 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

Ponder is the right tool when you need custom handler logic and are willing to own the operational surface. You may outgrow it 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

At that point, evaluate whether a managed service with custom handler support (such as Goldsky Mirror feeding into your own database) meets the requirement with lower operational overhead.

---

Custom blockchain indexers give you complete control over your data layer at the cost of ownership. If your team is evaluating whether to build custom or use a managed service — or needs help designing and deploying a production Ponder setup — [Clixo](https://clixo.sh/#contact) builds production indexing infrastructure for protocols at scale.

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
