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.
Install
npx skillscat add ladderchaos/tora-skills/ponder-gen Install via the SkillsCat registry.
SKILL.md
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 endpointsHandler 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/statusGraphQL 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 columnComposite 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/metricsBest 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