ponder
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). Reference structure with minimum version requirements. 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"
}
}
Note:
ponderandeslint-config-ponderversions should match. Uselatestor check the releases for the current stable version.
NextJS package additions
These are needed in packages/nextjs/ for querying Ponder's GraphQL API from the frontend:
{
"graphql": "^16.9.0",
"graphql-request": "^7.1.0"
}
Root package.json scripts
Wire up workspace commands so they're accessible from the monorepo root:
{
"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:
# RPC URL for the target chain (replace {chainId} with actual chain ID, e.g. PONDER_RPC_URL_1 for mainnet)
PONDER_RPC_URL_{chainId}=
# Database schema name
DATABASE_SCHEMA=my_schema
# (Optional) Postgres database URL. If not provided, PGlite (embedded Postgres) will be used.
DATABASE_URL=
The frontend uses NEXT_PUBLIC_PONDER_URL to know where the Ponder API lives (defaults to http://localhost:42069 in dev).
Ponder Package Configuration
ponder.config.ts - bridging SE-2 and Ponder
The config needs to read SE-2's deployed contracts and scaffold config so Ponder is aware of what to index. Here's a reference implementation that dynamically builds the Ponder config from SE-2's data. Adapt it based on the project's actual setup (e.g., if multiple networks are needed, or if contracts should be filtered):
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 definition
The schema in ponder.schema.ts should reflect the project's actual contract events. Look at what events the deployed contracts emit and design tables to capture that data. Each onchainTable defines a table that Ponder populates during indexing.
Solidity-to-Ponder type reference:
| Solidity | Ponder | TS type |
|---|---|---|
address |
t.hex() |
`0x${string}` |
uint256 / int256 |
t.bigint() |
bigint |
string |
t.text() |
string |
bool |
t.boolean() |
boolean |
bytes / bytes32 |
t.hex() |
`0x${string}` |
uint8 / uint32 etc. |
t.integer() |
number |
Additional column types: t.real() (floats), t.timestamp() (Date), t.json() (arbitrary JSON). Columns support modifiers: .primaryKey(), .notNull(), .default(value), .array(). See schema docs for the full API including composite primary keys, indexes, and enums.
Syntax example (for a greeting event, your schema will differ based on the actual contracts):
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(),
}));
Event handlers
Handlers go in packages/ponder/src/ and define what happens when contract events are detected. Look at the project's contracts to decide which events matter and what data to extract. The handler name format is "ContractName:EventName", where ContractName matches the key in deployedContracts.
Syntax example:
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
Ponder serves data via a Hono-based API. This is mostly boilerplate. A minimal packages/ponder/src/api/index.ts:
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;
Custom API routes can be added to this Hono app if GraphQL alone isn't sufficient. See Ponder API docs.
Boilerplate files
These are standard Ponder project files, nothing SE-2-specific, just needed for Ponder to work:
ponder-env.d.ts: type declarations for Ponder's virtual modules (ponder:registry,ponder:schema,ponder:api, etc.)tsconfig.json: standard strict TS config withmoduleResolution: "bundler",module: "ESNext",target: "ES2022".gitignore: should includenode_modules,.ponder,/generated/
Frontend
The frontend needs a page to display Ponder-indexed data. Use graphql-request and @tanstack/react-query (both available in SE-2) to query the Ponder API. The GraphQL query shape depends on what you defined in ponder.schema.ts. Ponder auto-generates queries from your schema, with each onchainTable getting a pluralized query with items, orderBy, and orderDirection support.
Fetch pattern for reference:
const fetchData = async () => {
const query = gql`
query {
greetings(orderBy: "timestamp", orderDirection: "desc") {
items {
id
text
setterId
premium
value
timestamp
}
}
}
`;
return request(
`${process.env.NEXT_PUBLIC_PONDER_URL || "http://localhost:42069"}/graphql`,
query,
);
};
// In component:
const { data } = useQuery({ queryKey: ["ponder-data"], queryFn: fetchData });
Development & Production
yarn ponder:devstarts the dev server with hot reload. GraphiQL explorer available athttp://localhost:42069for testing queries interactively.- For production deployment, see Ponder deployment docs. Key things: set
PONDER_RPC_URL_{chainId}with a production RPC, optionally configureDATABASE_URLfor Postgres (defaults to PGlite in dev), and point the frontend'sNEXT_PUBLIC_PONDER_URLto the deployed Ponder URL.