---
name: ponder
description: "Integrate Ponder into a Scaffold-ETH 2 project for blockchain event indexing. Use when the user wants to: index contract events, add a blockchain backend, set up GraphQL for onchain data, use Ponder with SE-2, or build an indexer for their dApp."
---Ponder Integration for Scaffold-ETH 2
Prerequisites
Check if ./packages/nextjs/scaffold.config.ts exists directly in the current working directory (do not search subdirectories). If it doesn't exist, this is not a Scaffold-ETH 2 project. Follow the instructions at https://docs.scaffoldeth.io/SKILL.md to scaffold it first. If it exists, continue directly with building.
Overview
Ponder is an open-source framework for blockchain application backends. It indexes smart contract events and serves the data via a GraphQL API. This skill covers integrating Ponder into a Scaffold-ETH 2 (SE-2) project.
For anything not covered here, refer to the Ponder docs or search the web. This skill provides the SE-2-specific integration knowledge, not a complete Ponder reference.
Ponder gets added as a new workspace at packages/ponder/. The key integration point is that Ponder reads deployedContracts and scaffold.config from the nextjs package, so it automatically knows about all deployed contracts without duplicating ABIs or addresses.
Look at the actual project structure and contracts before setting things up. Adapt to what's there rather than following this skill rigidly.
Dependencies & Scripts
Ponder package (packages/ponder/)
The packages/ponder/package.json should follow SE-2's workspace naming convention (@se-2/ponder). Check npm or the Ponder docs for the latest versions before installing:
{
"name": "@se-2/ponder",
"private": true,
"type": "module",
"scripts": {
"dev": "ponder dev",
"start": "ponder start",
"db": "ponder db",
"codegen": "ponder codegen",
"serve": "ponder serve",
"lint": "eslint .",
"typecheck": "tsc"
},
"dependencies": {
"ponder": "latest",
"hono": "^4.5.0",
"viem": "^2.0.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"eslint": "^8.54.0",
"eslint-config-ponder": "latest",
"typescript": "^5.0.4"
},
"engines": {
"node": ">=18.18"
}
}NextJS package additions
For querying Ponder's GraphQL API from the frontend, add to packages/nextjs/:
{
"graphql": "^16.9.0",
"graphql-request": "^7.1.0"
}Root package.json scripts
{
"ponder:dev": "yarn workspace @se-2/ponder dev",
"ponder:start": "yarn workspace @se-2/ponder start",
"ponder:codegen": "yarn workspace @se-2/ponder codegen",
"ponder:serve": "yarn workspace @se-2/ponder serve",
"ponder:lint": "yarn workspace @se-2/ponder lint",
"ponder:typecheck": "yarn workspace @se-2/ponder typecheck"
}Environment variables
A .env.example in packages/ponder/ for reference:
PONDER_RPC_URL_{chainId}=
DATABASE_SCHEMA=my_schema
DATABASE_URL=The frontend uses NEXT_PUBLIC_PONDER_URL to know where the Ponder API lives (defaults to http://localhost:42069 in dev).
ponder.config.ts โ Bridging SE-2 and Ponder
This is the critical integration piece. The config below is a reference implementation that dynamically reads SE-2's deployed contracts and scaffold config so Ponder automatically knows what to index. Adapt it based on the project's actual setup:
import { createConfig } from "ponder";
import deployedContracts from "../nextjs/contracts/deployedContracts";
import scaffoldConfig from "../nextjs/scaffold.config";
const targetNetwork = scaffoldConfig.targetNetworks[0];
const deployedContractsForNetwork = deployedContracts[targetNetwork.id];
if (!deployedContractsForNetwork) {
throw new Error(
`No deployed contracts found for network ID ${targetNetwork.id}`,
);
}
const chains = {
[targetNetwork.name]: {
id: targetNetwork.id,
rpc:
process.env[`PONDER_RPC_URL_${targetNetwork.id}`] ||
"http://127.0.0.1:8545",
},
};
const contractNames = Object.keys(deployedContractsForNetwork);
const contracts = Object.fromEntries(
contractNames.map((contractName) => {
return [
contractName,
{
chain: targetNetwork.name as string,
abi: deployedContractsForNetwork[contractName].abi,
address: deployedContractsForNetwork[contractName].address,
startBlock:
deployedContractsForNetwork[contractName].deployedOnBlock || 0,
},
];
}),
);
export default createConfig({
chains: chains,
contracts: contracts,
});Schema, Handlers, and API
Schema (ponder.schema.ts)
Use onchainTable to define tables. Adapt to the project's actual contract events:
import { onchainTable } from "ponder";
export const greeting = onchainTable("greeting", (t) => ({
id: t.text().primaryKey(),
text: t.text().notNull(),
setterId: t.hex().notNull(),
premium: t.boolean().notNull(),
value: t.bigint().notNull(),
timestamp: t.integer().notNull(),
}));For the full schema API (column types, indexes, enums), see Ponder schema docs.
Event handlers (src/)
Use Ponder's virtual module imports. Handler name format is "ContractName:EventName":
import { ponder } from "ponder:registry";
import { greeting } from "ponder:schema";
ponder.on("YourContract:GreetingChange", async ({ event, context }) => {
await context.db.insert(greeting).values({
id: event.id,
text: event.args.newGreeting,
setterId: event.args.greetingSetter,
premium: event.args.premium,
value: event.args.value,
timestamp: Number(event.block.timestamp),
});
});GraphQL API (src/api/index.ts)
Ponder serves data via Hono. Minimal setup:
import { db } from "ponder:api";
import schema from "ponder:schema";
import { Hono } from "hono";
import { graphql } from "ponder";
const app = new Hono();
app.use("/graphql", graphql({ db, schema }));
export default app;Required boilerplate
ponder-env.d.ts: type declarations for Ponder's virtual modules (ponder:registry,ponder:schema,ponder:api, etc.). Without this, TypeScript won't resolve the virtual imports.tsconfig.json: strict TS config withmoduleResolution: "bundler",module: "ESNext",target: "ES2022".gitignore: includenode_modules,.ponder,/generated/
Frontend
Use graphql-request and @tanstack/react-query (both available in SE-2) to query the Ponder API. Ponder auto-generates GraphQL queries from your schema โ each onchainTable gets a pluralized query with items, orderBy, and orderDirection support:
import { gql, request } from "graphql-request";
import { useQuery } from "@tanstack/react-query";
const PONDER_URL = process.env.NEXT_PUBLIC_PONDER_URL || "http://localhost:42069";
const { data } = useQuery({
queryKey: ["ponder-greetings"],
queryFn: () => request(`${PONDER_URL}/graphql`, gql`{
greetings(orderBy: "timestamp", orderDirection: "desc") {
items { id text setterId premium value timestamp }
}
}`),
});Development & Production
yarn ponder:devstarts the dev server with hot reload. GraphiQL explorer available athttp://localhost:42069for testing queries interactively.- For production, set
PONDER_RPC_URL_{chainId}with a production RPC, optionally configureDATABASE_URLfor Postgres (defaults to PGlite in dev), and pointNEXT_PUBLIC_PONDER_URLto the deployed Ponder URL. See Ponder deployment docs.