Build EVM blockchain data indexers using Ponder (ponder.sh) - an open-source TypeScript framework for indexing smart contract events, transactions, and traces into custom database schemas with type-safe APIs. Use when the user mentions ponder, blockchain/EVM indexing, onchain data pipelines, subgraph replacement, or wants to index smart contract events into a queryable database.
98
99%
Does it follow best practices?
Impact
98%
1.25xAverage score across 5 eval scenarios
Passed
No known issues
// ponder.config.ts
export default createConfig({
database: {
kind: "postgres",
connectionString: process.env.DATABASE_URL,
poolConfig: {
max: 30,
ssl: { rejectUnauthorized: false }, // If using SSL
},
},
// ...
});Or rely on the DATABASE_URL environment variable (auto-detected when set).
Production requires schema isolation to prevent conflicts between deployments:
# Via CLI flag:
ponder start --schema my_app_v1
# Or via environment variable:
DATABASE_SCHEMA=my_app_v1 ponder startEach schema name creates isolated Postgres schemas. Multiple instances can share the same database with different schema names.
# List all Ponder schemas in the database:
ponder db list
# Remove unused schemas:
ponder db pruneUse the views pattern to swap between schema versions without downtime:
# Deploy new version:
ponder start --schema my_app_v2 --views-schema my_app_views
# The --views-schema creates Postgres views that point to the active schema.
# When v2 finishes backfill, the views automatically switch from v1 to v2.
# Your API (ponder serve) reads from the views schema, so it serves v1 data
# until v2 is ready, then seamlessly serves v2 data.ponder serve --schema my_app_views (reads from views)ponder start --schema my_app_v1 --views-schema my_app_viewsponder start --schema my_app_v2 --views-schema my_app_views| Endpoint | Behavior | Use For |
|---|---|---|
/health | Always returns HTTP 200 | Liveness probes (is the process alive?) |
/ready | Returns 503 during backfill, 200 when caught up | Readiness probes (is the API serving current data?) |
/status | Returns JSON with indexing progress per chain | Monitoring dashboards |
{
"mainnet": {
"ready": false,
"block": {
"current": 18500000,
"target": 19000000
},
"progress": 0.974
}
}When Ponder restarts with the same schema name, it automatically resumes from the last checkpoint:
This means ponder start --schema X is safe to run with auto-restart supervisors (systemd, Docker restart policies, Kubernetes, Railway, etc.).
ponder start --schema X (writes data, runs indexing functions)ponder serve --schema X (read-only API, no indexing)ponder serve is stateless and horizontally scalable. It reads from the same Postgres database that the indexer writes to.
# Indexer (single instance):
ponder start --schema my_app
# API replicas (scale horizontally):
ponder serve --schema my_app# Indexer:
ponder start --schema my_app_v1 --views-schema my_app_views
# API replicas (point to views):
ponder serve --schema my_app_viewsFor large indexing jobs, increase Node.js memory:
NODE_OPTIONS="--max-old-space-size=8192" ponder start --schema my_appmultichain and omnichain ordering: single-threaded indexingexperimental_isolated ordering: uses up to 4 cores (one per chain, parallelized)startBlock is recent and you don't use readContract.| Method | Required For |
|---|---|
eth_getLogs | All indexing (core requirement) |
eth_getBlockByNumber | All indexing (core requirement) |
eth_call | readContract in handlers |
debug_traceBlockByNumber | includeCallTraces: true |
Different providers have different max block ranges for eth_getLogs. Ponder auto-detects this, but you can override:
chains: {
mainnet: {
id: 1,
rpc: process.env.PONDER_RPC_URL_1,
ethGetLogsBlockRange: 2000, // Override if auto-detection is wrong
},
}import { http, fallback, loadBalance } from "viem";
chains: {
mainnet: {
id: 1,
// Fallback: tries each in order, moves to next on failure
rpc: fallback([
http(process.env.PONDER_RPC_URL_1_PRIMARY),
http(process.env.PONDER_RPC_URL_1_FALLBACK),
]),
},
base: {
id: 8453,
// Load balance: distributes requests across endpoints
rpc: loadBalance([
http(process.env.PONDER_RPC_URL_8453_A),
http(process.env.PONDER_RPC_URL_8453_B),
]),
},
}Or simply use an array of URLs (automatic fallback):
chains: {
mainnet: {
id: 1,
rpc: [
process.env.PONDER_RPC_URL_1_PRIMARY,
process.env.PONDER_RPC_URL_1_FALLBACK,
],
},
}Available at /metrics in Prometheus exposition format:
curl http://localhost:42069/metricsKey metrics:
ponder_indexing_completed_events - Total events processedponder_indexing_completed_seconds - Time spent in handlersponder_historical_total_blocks - Total blocks to processponder_historical_completed_blocks - Blocks processed so far# Log levels: silent, error, warn, info, debug, trace
ponder start --schema my_app --log-level debug
# JSON format (for log aggregation):
ponder start --schema my_app --log-format jsonUse --log-level trace for maximum detail when debugging RPC or database issues.
startBlock: Should be the contract deployment block, not 0readContract calls. Use cache: "immutable" for static values.poolConfig.max.experimental_isolated for max throughput if cross-chain ordering isn't needed.ethGetLogsBlockRange to a smaller value.DATABASE_URL and network connectivitymax_connections or reduce poolConfig.maxponder-env.d.tsKey breaking changes across Ponder versions:
| Version | Change |
|---|---|
| 0.8 | Package renamed from @ponder/core to ponder |
| 0.9 | API file (src/api/index.ts) required. Must export default Hono app. |
| 0.11 | Config: networks renamed to chains. transport renamed to rpc. |
| 0.12 | All addresses normalized to lowercase. Remove checksums everywhere. |
| 0.16 | Table/schema names limited to 45 characters. |