ponder-gen

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

  1. Start Block: Set startBlock to contract deployment block
  2. Batch Inserts: Use context.db.insert().values([...]) for bulk
  3. Error Handling: Ponder retries failed handlers automatically
  4. Schema Migrations: Delete .ponder/ when changing schema
  5. RPC Rate Limits: Use paid RPC for production
Weekly Installs
2
First Seen
Jan 20, 2026
Installed on
opencode2
kilo2
gemini-cli2
antigravity2
windsurf2
claude-code2