home / skills / ladderchaos / tora-skills / ponder-gen

ponder-gen skill

/ponder-gen

This skill generates and maintains Ponder indexer handlers from ABIs and schemas, accelerating setup and updates for new contracts and events.

npx playbooks add skill ladderchaos/tora-skills --skill ponder-gen

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.9 KB
---
name: ponder-gen
description: 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:

```bash
cp packages/contracts-core/out/TickBookPoolManager.sol/TickBookPoolManager.json \
   packages/indexer/abis/
```

### Step 2: Configure Contract in ponder.config.ts

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

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

```typescript
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:

```typescript
// 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

```bash
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:

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

```typescript
// Convert to string for storage
marketId: marketId.toString(),

// Keep as bigint for numeric ops
shares: shares, // bigint column
```

### Composite IDs

```typescript
// Create unique IDs from multiple fields
const id = `${marketId}-${trader}-${outcome}`;
const txId = `${event.transaction.hash}-${event.log.logIndex}`;
```

### Upsert Pattern

```typescript
await context.db
  .insert(schema.Position)
  .values(newPosition)
  .onConflictDoUpdate({
    shares: (existing) => existing.shares + newShares,
  });
```

## Debugging

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

Overview

This skill generates Ponder indexer handlers from contract ABIs and schema definitions to accelerate event indexing setup and maintenance. It creates typed, idiomatic handlers for inserting, updating, upserting, and deleting records in the Ponder database based on emitted contract events. Use it to bootstrap new contract support or update the indexer after ABI/schema changes.

How this skill works

The generator reads contract ABIs and your ponder.schema definitions and emits handler stubs for each configured event. Each handler maps event.args to schema fields, constructs composite IDs where needed, and applies appropriate DB operations (insert, update, upsert, delete). It also wires network and contract metadata from ponder.config to ensure type-safe event typing and correct startBlock behavior.

When to use it

  • Adding indexing for a new contract or topic
  • Creating handlers for new events after ABI changes
  • Updating indexer logic when schema evolves
  • Debugging or resyncing when indexed data is inconsistent
  • Optimizing handler patterns for performance and bulk operations

Best practices

  • Set startBlock to the contract deployment block to avoid unnecessary historical scans
  • Use composite IDs (txHash-logIndex, marketId-trader-outcome) for deterministic uniqueness
  • Prefer batch inserts for high-throughput events to reduce DB overhead
  • Keep numeric types: store bigints in bigint columns and convert only when needed for string keys
  • Use onConflict upserts for incremental aggregates (positions, balances)

Example use cases

  • Generate handlers for MarketCreated, MarketSettled, RangeOrderPlaced, SpotOrderPlaced, OrderFilled, OrderCancelled
  • Add a new contract ABI and emit scaffolded handlers wired to the existing schema
  • Update handlers after ABI changes so event.args remain correctly typed and mapped
  • Bulk-migrate or reindex by regenerating handlers and resetting .ponder for schema changes
  • Create midpoint price calculation or alternate price storage in generated Order handler

FAQ

Will handlers be fully typed from the ABI?

Yes. Handlers use Ponder’s type inference so event.args, event.block, and event.transaction are strongly typed based on the ABI.

How do I handle schema changes without corrupting data?

For schema changes, update ponder.schema, delete .ponder to reset generated metadata, then resync. Use migrations or export/import procedures for production data retention.