Generate Ponder indexer handlers from contract ABIs and schema definitions. Use this skill when setting up new event indexing, adding handlers for new contracts, or updating the indexer after contract changes.
Ponder Generator
This skill guides the generation and maintenance of Ponder indexer code for the Sooth Protocol, leveraging Ponder's type-safe, no-codegen approach.
When to Use This Skill
Invoke this skill when:
- Adding indexing for new contracts
- Updating handlers after ABI changes
- Setting up event listeners for new events
- Debugging indexer sync issues
- Optimizing indexer performance
Ponder Project Structure
packages/indexer/
├── ponder.config.ts # Network and contract configuration
├── ponder.schema.ts # Database schema (Drizzle-like)
├── src/
│ └── index.ts # Event handlers
├── abis/ # Contract ABIs (JSON)
└── .env # RPC endpoints
Handler Generation Workflow
Step 1: Add Contract ABI
Copy ABI from Foundry build:
cp packages/contracts-core/out/TickBookPoolManager.sol/TickBookPoolManager.json \
packages/indexer/abis/
Step 2: Configure Contract in ponder.config.ts
import { createConfig } from 'ponder';
import { http } from 'viem';
import TickBookPoolManagerAbi from './abis/TickBookPoolManager.json';
export default createConfig({
networks: {
baseSepolia: {
chainId: 84532,
transport: http(process.env.PONDER_RPC_URL_84532),
},
},
contracts: {
TickBookPoolManager: {
network: 'baseSepolia',
abi: TickBookPoolManagerAbi.abi,
address: '0x8e14f863109cc93ec91540919287556cc368ff0b',
startBlock: 12345678,
},
},
});
Step 3: Define Schema in ponder.schema.ts
import { createSchema } from 'ponder';
export default createSchema((p) => ({
// Markets table
Market: p.createTable({
id: p.string(), // marketId as string
question: p.string(),
creator: p.string(),
createdAt: p.bigint(),
isSettled: p.boolean(),
outcome: p.int().optional(),
}),
// Orders table
Order: p.createTable({
id: p.string(), // txHash-logIndex
marketId: p.string(),
trader: p.string(),
outcome: p.int(), // 0=NO, 1=YES
price: p.bigint(),
amount: p.bigint(),
timestamp: p.bigint(),
}),
// Positions table
Position: p.createTable({
id: p.string(), // marketId-trader-outcome
marketId: p.string(),
trader: p.string(),
outcome: p.int(),
shares: p.bigint(),
}),
}));
Step 4: Generate Handlers in src/index.ts
import { ponder } from 'ponder:registry';
import schema from '../ponder.schema';
// Handle MarketCreated event
ponder.on('LaunchpadEngine:MarketCreated', async ({ event, context }) => {
const { marketId, creator, question } = event.args;
await context.db.insert(schema.Market).values({
id: marketId.toString(),
question,
creator,
createdAt: event.block.timestamp,
isSettled: false,
});
});
// Handle RangeOrderPlaced event
ponder.on('TickBookPoolManager:RangeOrderPlaced', async ({ event, context }) => {
const { marketId, trader, outcome, priceLow, priceHigh, amount, orderId } = event.args;
await context.db.insert(schema.Order).values({
id: `${event.transaction.hash}-${event.log.logIndex}`,
marketId: marketId.toString(),
trader,
outcome: Number(outcome),
price: priceLow, // or use midpoint
amount,
timestamp: event.block.timestamp,
});
});
// Handle OrderFilled event (update position)
ponder.on('TickBookPoolManager:OrderFilled', async ({ event, context }) => {
const { marketId, trader, outcome, shares } = event.args;
const positionId = `${marketId}-${trader}-${outcome}`;
// Upsert position
await context.db
.insert(schema.Position)
.values({
id: positionId,
marketId: marketId.toString(),
trader,
outcome: Number(outcome),
shares,
})
.onConflictDoUpdate({
shares: (existing) => existing.shares + shares,
});
});
// Handle MarketSettled event
ponder.on('LaunchpadEngine:MarketSettled', async ({ event, context }) => {
const { marketId, outcome } = event.args;
await context.db
.update(schema.Market)
.set({
isSettled: true,
outcome: Number(outcome),
})
.where({ id: marketId.toString() });
});
Event-to-Handler Mapping
| Contract Event | Handler Action | Schema Table |
|---|---|---|
MarketCreated |
Insert | Market |
MarketSettled |
Update | Market |
RangeOrderPlaced |
Insert | Order |
SpotOrderPlaced |
Insert | Order |
OrderFilled |
Upsert | Position |
OrderCancelled |
Delete | Order |
Claimed |
Update | Position |
Type Safety
Ponder provides full type inference:
// event.args is fully typed based on ABI
ponder.on('TickBookPoolManager:RangeOrderPlaced', async ({ event }) => {
// TypeScript knows these fields exist
const { marketId, trader, outcome, priceLow, priceHigh, amount } = event.args;
// event.block and event.transaction also typed
const blockNumber = event.block.number;
const txHash = event.transaction.hash;
});
Running the Indexer
cd packages/indexer
# Development (with hot reload)
pnpm dev
# Production
pnpm start
# Check sync status
curl http://localhost:42069/status
GraphQL API
Ponder auto-generates GraphQL API:
# Get all markets
query {
markets(first: 10, orderBy: "createdAt", orderDirection: "desc") {
items {
id
question
creator
isSettled
}
}
}
# Get orders for a market
query {
orders(where: { marketId: "1" }) {
items {
trader
outcome
price
amount
}
}
}
Common Patterns
Handling BigInt
// Convert to string for storage
marketId: marketId.toString(),
// Keep as bigint for numeric ops
shares: shares, // bigint column
Composite IDs
// Create unique IDs from multiple fields
const id = `${marketId}-${trader}-${outcome}`;
const txId = `${event.transaction.hash}-${event.log.logIndex}`;
Upsert Pattern
await context.db
.insert(schema.Position)
.values(newPosition)
.onConflictDoUpdate({
shares: (existing) => existing.shares + newShares,
});
Debugging
# Verbose logging
DEBUG=ponder:* pnpm dev
# Reset and resync
rm -rf .ponder && pnpm dev
# Check indexed blocks
curl http://localhost:42069/metrics
Best Practices
- Start Block: Set
startBlockto contract deployment block - Batch Inserts: Use
context.db.insert().values([...])for bulk - Error Handling: Ponder retries failed handlers automatically
- Schema Migrations: Delete
.ponder/when changing schema - RPC Rate Limits: Use paid RPC for production
You Might Also Like
Related Skills

coding-agent
Run Codex CLI, Claude Code, OpenCode, or Pi Coding Agent via background process for programmatic control.
openclaw
add-uint-support
Add unsigned integer (uint) type support to PyTorch operators by updating AT_DISPATCH macros. Use when adding support for uint16, uint32, uint64 types to operators, kernels, or when user mentions enabling unsigned types, barebones unsigned types, or uint support.
pytorch
at-dispatch-v2
Convert PyTorch AT_DISPATCH macros to AT_DISPATCH_V2 format in ATen C++ code. Use when porting AT_DISPATCH_ALL_TYPES_AND*, AT_DISPATCH_FLOATING_TYPES*, or other dispatch macros to the new v2 API. For ATen kernel files, CUDA kernels, and native operator implementations.
pytorch
skill-writer
Guide users through creating Agent Skills for Claude Code. Use when the user wants to create, write, author, or design a new Skill, or needs help with SKILL.md files, frontmatter, or skill structure.
pytorch
implementing-jsc-classes-cpp
Implements JavaScript classes in C++ using JavaScriptCore. Use when creating new JS classes with C++ bindings, prototypes, or constructors.
oven-sh
implementing-jsc-classes-zig
Creates JavaScript classes using Bun's Zig bindings generator (.classes.ts). Use when implementing new JS APIs in Zig with JSC integration.
oven-sh