A Ponder indexer for the OCR (Open Creator Rails) protocol.
This indexer tracks:
- AssetRegistry: deployment and configuration of
Assetcontracts. - Asset: subscription-gated assets and their subscribers.
It exposes convenient entities to answer questions like:
- Which assets exist and who owns them?
- Which assets is a user subscribed to?
- Which users are subscribed to a given asset?
- Node.js v18+
- pnpm v8+
- Foundry (for local development) — install via
curl -L https://foundry.paradigm.xyz | bash && foundryup
Clone with submodules (the open-creator-rails repo is included as a submodule for ABI generation):
git clone --recurse-submodules https://github.com/ChainSafe/ocr-indexer.git
cd ocr-indexer
pnpm setup # installs deps, builds contracts, syncs ABIsIf you already cloned without --recurse-submodules:
git submodule update --init --recursive
pnpm setuppnpm dev:localThis starts three things in sequence:
- Anvil — local EVM node
- Seed — deploys registry, test token, and sample assets/subscriptions
- Ponder — starts the indexer pointed at local Anvil (
PONDER_RPC_URL_31337)
The API will be available at http://localhost:42069.
To index against Sepolia instead, set PONDER_RPC_URL_11155111 in .env.local and run:
pnpm dev| Script | Description |
|---|---|
pnpm setup |
Install deps, build contracts, sync ABIs — run once after cloning |
pnpm dev |
Start Ponder in dev mode (live reload) |
pnpm dev:local |
Start Anvil + seed + Ponder for local development |
pnpm start |
Start Ponder in production mode |
pnpm contracts:build |
Run forge build inside the open-creator-rails submodule |
pnpm sync |
Extract ABIs from Foundry build output into config/ |
pnpm codegen |
Regenerate Ponder types from schema and config |
pnpm typecheck |
TypeScript type check |
pnpm lint |
ESLint |
ABIs live in config/ and are generated from the contracts in the open-creator-rails submodule. To update after contract changes:
git submodule update --remote open-creator-rails
pnpm contracts:build
pnpm syncThe abi-sync-check CI workflow enforces that config/AssetABI.ts and config/AssetRegistryABI.ts are always in sync with the submodule.
| Variable | Chain | Description |
|---|---|---|
PONDER_RPC_URL_31337 |
Local (Anvil) | Anvil RPC URL. Enables local chain indexing. |
PONDER_RPC_URL_11155111 |
Sepolia | Sepolia RPC URL. Enables Sepolia indexing. |
DATABASE_URL |
— | Postgres connection string. Dev mode uses PGlite (in-process) by default. |
Both RPC URLs can be set simultaneously to index multiple chains at once.
docker compose up --build # start the full stack
docker compose down # stop
docker compose down -v # stop and remove volumes (full reset)Requires PONDER_RPC_URL_11155111 to be set in your shell or in .env.
The stack runs five services:
| Service | Port | Description |
|---|---|---|
worker |
42070 | Ponder indexer — backfills and live-indexes chain data |
api |
42069 | Ponder API server — serves GraphQL/REST from the views schema |
postgres |
5432 | Shared database |
prometheus |
9090 | Scrapes Ponder metrics from worker and API |
grafana |
3000 | Dashboards for sync lag, API latency, and errors |
Grafana is available at http://localhost:3000 (default credentials: admin / admin).
The indexer exposes the following endpoints:
| Endpoint | Description |
|---|---|
GET / |
Auto-generated GraphQL playground (deprecated) |
POST /graphql |
Auto-generated GraphQL API (Ponder) (deprecated — use /v2/graphql) |
POST /v2/graphql |
Custom GraphQL API (recommended) |
GET /ready |
Health check — returns 200 when live |
The v2 endpoint at /v2/graphql is the recommended API. Open the GraphiQL playground at http://localhost:42069/v2/graphql after starting the indexer.
Key differences from the auto-generated endpoint:
- Case-insensitive address filters — pass addresses in any casing; the
Addressscalar normalises them automatically - Equality-only filters — simple
whereclauses with no_gt/_lt/_in/AND/OR operators - List queries only — no single-item-by-id queries
| Query | Description |
|---|---|
registryEntitys |
Registry contract state |
assetEntitys |
Asset contract state |
subscriptions |
Subscription state per asset–subscriber |
assetRegistry_AssetCreateds |
Asset creation events |
assetRegistry_OwnershipTransferreds |
Registry ownership transfers |
assetRegistry_RegistryFeeShareUpdateds |
Registry fee share updates |
assetRegistry_RegistryFeeClaimedBatchs |
Registry fee claim batches |
asset_SubscriptionAddeds |
New subscription events |
asset_SubscriptionExtendeds |
Subscription extension events |
asset_SubscriptionRevokeds |
Subscription revocation events |
asset_SubscriptionCancelleds |
Subscription cancellation events |
asset_SubscriptionPriceUpdateds |
Price update events |
asset_CreatorFeeClaimeds |
Creator fee claim events |
asset_OwnershipTransferreds |
Asset ownership transfer events |
_meta |
Indexer sync status per chain |
Fetch all assets by owner:
{
assetEntitys(where: { owner: "0xYourAddress" }) {
items {
id
assetId
address
chainId
}
}
}Check subscriptions for a payer:
{
subscriptions(where: { payer: "0x..." }) {
items {
assetId
subscriber
startTime
endTime
}
}
}Check indexer sync status:
{
_meta {
status
}
}Pagination:
{
subscriptions(limit: 20, offset: 0) {
items { ... }
pageInfo {
hasNextPage
hasPreviousPage
}
totalCount
}
}Notes:
- Address filter fields accept any casing —
"0xAbCd..."and"0xabcd..."both work- All
BigIntvalues are returned as strings to avoid JavaScript integer overflowblockTimestampis a Unix timestamp in secondssubscriberis abytes32identity hash, not an address
Defined in ponder.schema.ts.
One row per deployed Asset contract.
id: asset contract address (lowercased).assetId: bytes32 id in the registry.registryAddress: address of theAssetRegistrythat created it.owner: currentAssetowner (creator or transferee), always lowercased.
One row per (asset, subscriber) pair representing their current contiguous active state.
id:${assetAddress}_${subscriber}(both lowercased).assetId: foreign key toAssetEntity.id.user: subscriber address (lowercased).startTime: start of the unbroken subscription block (BigInt).endTime: final expiry timestamp (BigInt).nonce: latest subscription iteration counter.isActive: false if revoked.
When a user tops up an active subscription, the contract sets the new event's
startTimeequal to the oldendTime. The indexer stitches these together soSubscriptionalways reflects the continuous timeline, while theAsset_SubscriptionAddedlog table tracks each iteration individually via nonces.
Raw event history for debugging and analytics:
AssetRegistry_AssetCreatedAssetRegistry_OwnershipTransferredAssetRegistry_RegistryFeeShareUpdatedAssetRegistry_RegistryFeeClaimedBatchAsset_SubscriptionAddedAsset_SubscriptionExtendedAsset_SubscriptionPriceUpdatedAsset_SubscriptionRevokedAsset_SubscriptionCancelledAsset_CreatorFeeClaimedAsset_OwnershipTransferred