Ponder Blockchain Indexer Integration: Implementation Guide
Related ADR: Ponder Blockchain IndexerScope: Add Ponder indexer for NFT ownership tracking with Prisma integration
Overview
Add apps/ponder to the monorepo — a Ponder blockchain indexer that watches Transfer events from Diamond collection contracts on Arbitrum and writes ownership data into Prisma-managed PostgreSQL tables.
Phase Summary
| Phase | Scope | Status |
|---|---|---|
| 1 | Prisma Schema Migration (new models + deck field) | Pending |
| 2 | Ponder App Scaffolding (config, schema, ABIs) | Pending |
| 3 | Event Handlers + Prisma Sync Layer | Pending |
| 4 | Database Package Exports (ownership queries) | Pending |
| 5 | Turbo Integration + Environment Setup | Pending |
| 6 | Testing + Verification (Arbitrum One mainnet) | Pending |
| 7 | Monitor Ownership UI | Pending |
Phase 1: Prisma Schema Migration
1.1 Schema Changes
File: packages/database/prisma/schema.prisma
The existing deck model already IS the NFT token record — it has token_id, mint_recipient, mint_tx_hash, mint_status, and links to collection (which has contract_address). No separate ownership table is needed. We add fields to deck and a transfer history table.
Add to existing deck model:
// On-chain ownership tracking — populated by Ponder indexer
current_owner String? // Current on-chain owner wallet address
last_transfer_block BigInt? // Block number of most recent transfer
last_transfer_at DateTime? // Timestamp of most recent transfer
nft_transfers nft_transfer[]Add new nft_transfer model (transfer event history):
/// Transfer event history for NFT tokens, populated by the Ponder indexer.
/// Links directly to deck via deck_id FK — the deck IS the token.
model nft_transfer {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
deck_id String @db.Uuid
from_address String
to_address String
block_number BigInt
tx_hash String
log_index Int
timestamp DateTime
created_at DateTime @default(now())
deck deck @relation(fields: [deck_id], references: [id], onDelete: Cascade)
@@unique([tx_hash, log_index])
@@index([deck_id])
@@index([from_address])
@@index([to_address])
@@index([block_number])
}Add new ponder_indexer_state model (indexer health tracking):
/// Singleton table tracking the Ponder indexer's operational state.
model ponder_indexer_state {
id String @id @default("singleton")
status String @default("idle")
last_indexed_block BigInt?
chain_id Int?
started_at DateTime?
updated_at DateTime @updatedAt
}Why no nft_token_owner table? The deck model already represents the NFT token — it has token_id, collection_id (→ contract_address), mint_recipient, user_id, custodied_for. Adding current_owner to deck means ownership lives alongside the token record it describes. The nft_transfer table links to deck via deck_id FK, giving a clean deck.nft_transfers relation for transfer history.
1.3 Run Migration
cd packages/database
pnpm prisma migrate dev --name ponder_ownership_tracking
pnpm buildRisk: Migration affects the shared database. Follow the existing multi-app coordination workflow — apply to staging first, verify, then production. The migration only adds new tables and a nullable column, so it's non-breaking.
Phase 2: Ponder App Scaffolding
2.1 Directory Structure
apps/ponder/
package.json
tsconfig.json
ponder.config.ts
ponder.schema.ts
ponder-env.d.ts
.env.example
abis/
DiamondFactory.ts
ERC721Diamond.ts
src/
index.ts
handlers/
factory.ts
transfer.ts
lib/
prisma-sync.ts
reindex.ts
api/
index.ts2.2 Package Configuration
File: apps/ponder/package.json
{
"name": "@emp/ponder",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "ponder dev",
"start": "ponder start",
"build": "ponder build",
"serve": "ponder serve",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@emergexyz/db": "workspace:*",
"ponder": "^0.10.0",
"hono": "^4.5.0",
"viem": "^2.45.2"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
}
}File: apps/ponder/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"composite": true
},
"include": ["src/**/*", "ponder.config.ts", "ponder.schema.ts", "abis/**/*"],
"exclude": ["dist", "node_modules"]
}2.3 ABI Files
Extract only the event signatures Ponder needs. Self-contained — no dependency on packages/nft-contracts build.
File: apps/ponder/abis/DiamondFactory.ts
Source: packages/nft-contracts/contracts/DiamondFactory.sol:39-43
export const DiamondFactoryAbi = [
{
type: "event",
name: "CollectionCreated",
inputs: [
{ name: "diamond", type: "address", indexed: true },
{ name: "collectionOwner", type: "address", indexed: true },
{ name: "salt", type: "bytes32", indexed: false },
],
},
] as const;File: apps/ponder/abis/ERC721Diamond.ts
Sources:
Transfer: SolidStateERC721Base(standard ERC721 event)Minted:packages/nft-contracts/contracts/facets/MintFacet.sol:13
export const ERC721DiamondAbi = [
{
type: "event",
name: "Transfer",
inputs: [
{ name: "from", type: "address", indexed: true },
{ name: "to", type: "address", indexed: true },
{ name: "tokenId", type: "uint256", indexed: true },
],
},
{
type: "event",
name: "Minted",
inputs: [
{ name: "to", type: "address", indexed: true },
{ name: "tokenId", type: "uint256", indexed: true },
],
},
] as const;2.4 Ponder Config
File: apps/ponder/ponder.config.ts
import { createConfig, factory } from "ponder";
import { http, parseAbiItem } from "viem";
import { DiamondFactoryAbi } from "./abis/DiamondFactory";
import { ERC721DiamondAbi } from "./abis/ERC721Diamond";
// Factory addresses from packages/nft-contracts/deployments/*.json
const ARBITRUM_ONE_FACTORY = "0x4f885f1F940D5Bb33cC85741fd635e379B6581aB";
const COLLECTION_CREATED_EVENT = parseAbiItem(
"event CollectionCreated(address indexed diamond, address indexed collectionOwner, bytes32 salt)"
);
export default createConfig({
database: {
kind: "postgres",
connectionString: process.env.PONDER_DATABASE_URL,
},
networks: {
arbitrumOne: {
chainId: 42161,
transport: http(process.env.PONDER_RPC_URL_ARBITRUM_ONE),
},
},
contracts: {
DiamondFactory: {
abi: DiamondFactoryAbi,
network: {
arbitrumOne: {
address: ARBITRUM_ONE_FACTORY,
startBlock: 0, // TODO: Replace with actual deployment block
},
},
},
ERC721Diamond: {
abi: ERC721DiamondAbi,
network: {
arbitrumOne: {
address: factory({
address: ARBITRUM_ONE_FACTORY,
event: COLLECTION_CREATED_EVENT,
parameter: "diamond",
}),
startBlock: 0, // TODO: Replace with actual deployment block
},
},
},
},
});Note: startBlock must be looked up from the factory deployment transaction:
- Arbitrum One: Deployed 2026-02-18 (
deploy_tx_hashinpackages/nft-contracts/deployments/arbitrum-one.json)
Use Arbiscan to convert tx hash → block number.
2.5 Ponder Schema (Internal Tables)
File: apps/ponder/ponder.schema.ts
import { index, onchainTable, primaryKey } from "ponder";
// Factory-discovered collections (for Ponder's internal tracking)
export const collections = onchainTable("collections", (t) => ({
address: t.hex().primaryKey(),
owner: t.hex().notNull(),
salt: t.hex().notNull(),
chainId: t.integer().notNull(),
createdAtBlock: t.bigint().notNull(),
createdAtTimestamp: t.bigint().notNull(),
}));
// Transfer log (Ponder's internal copy — the source of truth for Prisma sync)
export const transfers = onchainTable(
"transfers",
(t) => ({
id: t.text().primaryKey(),
contractAddress: t.hex().notNull(),
tokenId: t.bigint().notNull(),
from: t.hex().notNull(),
to: t.hex().notNull(),
blockNumber: t.bigint().notNull(),
timestamp: t.bigint().notNull(),
transactionHash: t.hex().notNull(),
}),
(table) => ({
contractIdx: index().on(table.contractAddress),
tokenIdx: index().on(table.contractAddress, table.tokenId),
fromIdx: index().on(table.from),
toIdx: index().on(table.to),
})
);These are Ponder's internal bookkeeping tables. They are ephemeral — dropped and recreated on reindex. Apps never query these directly.
No tokenOwners table — the existing deck model in Prisma is the token record. Ponder updates deck.current_owner directly via the sync layer.
2.6 Environment File
File: apps/ponder/.env.example
# RPC Endpoint (Alchemy, Infura, or similar)
PONDER_RPC_URL_ARBITRUM_ONE=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY
# Ponder internal tables (separate schema in same database)
PONDER_DATABASE_URL=postgresql://user:pass@host:5432/dbname?schema=ponder
# Prisma tables (same database, public schema)
DATABASE_URL=postgresql://user:pass@host:5432/dbname
# Ponder HTTP port (default 42069)
PONDER_PORT=42069Phase 3: Event Handlers + Prisma Sync Layer
3.1 Handler Registration
File: apps/ponder/src/index.ts
import "./handlers/factory";
import "./handlers/transfer";3.2 Factory Handler
File: apps/ponder/src/handlers/factory.ts
Handles DiamondFactory:CollectionCreated:
- Insert into Ponder's
collectionstable - Call
syncCollectionCreated()to update existing Prismacollectionrecord
import { ponder } from "ponder:registry";
import { collections } from "ponder:schema";
import { syncCollectionCreated } from "../lib/prisma-sync";
ponder.on("DiamondFactory:CollectionCreated", async ({ event, context }) => {
const { diamond, collectionOwner, salt } = event.args;
await context.db.insert(collections).values({
address: diamond,
owner: collectionOwner,
salt,
chainId: context.network.chainId,
createdAtBlock: event.block.number,
createdAtTimestamp: event.block.timestamp,
});
await syncCollectionCreated({
contractAddress: diamond,
owner: collectionOwner,
chainId: context.network.chainId,
blockNumber: event.block.number,
timestamp: event.block.timestamp,
});
});3.3 Transfer Handler (Core)
File: apps/ponder/src/handlers/transfer.ts
Handles ERC721Diamond:Transfer:
- Detect mint (
from == 0x0) vs secondary transfer - Insert into Ponder's
transferstable - Call
syncTransfer()to updatedeck.current_ownerand insertnft_transfer
import { ponder } from "ponder:registry";
import { transfers } from "ponder:schema";
import { syncTransfer } from "../lib/prisma-sync";
import { zeroAddress } from "viem";
ponder.on("ERC721Diamond:Transfer", async ({ event, context }) => {
const { from, to, tokenId } = event.args;
const contractAddress = event.log.address;
const isMint = from === zeroAddress;
const transferId = `${event.transaction.hash}-${event.log.logIndex}`;
// Ponder internal: record transfer
await context.db.insert(transfers).values({
id: transferId,
contractAddress,
tokenId,
from,
to,
blockNumber: event.block.number,
timestamp: event.block.timestamp,
transactionHash: event.transaction.hash,
});
// Prisma sync: update deck.current_owner + insert nft_transfer
await syncTransfer({
contractAddress,
chainId: context.network.chainId,
tokenId: Number(tokenId),
from,
to,
blockNumber: event.block.number,
txHash: event.transaction.hash,
logIndex: event.log.logIndex,
timestamp: event.block.timestamp,
isMint,
});
});3.4 Prisma Sync Module
File: apps/ponder/src/lib/prisma-sync.ts
The bridge between Ponder and Prisma. Imports @emergexyz/db and writes directly to deck + nft_transfer.
Key functions:
syncTransfer():
- Find the
deckviacollection.contract_address+deck.token_id - Update
deck.current_owneranddeck.last_transfer_block/deck.last_transfer_at - Insert
nft_transferrecord linked to the deck viadeck_idFK - Update
ponder_indexer_state.last_indexed_block
syncCollectionCreated():
- Update existing
collectionrecord if it exists (setmint_statusto"deployed")
All writes use upsert or createMany with skipDuplicates for idempotency.
3.5 Reindex Cleanup
File: apps/ponder/src/lib/reindex.ts
Called from Ponder's setup event (or startup hook) before historical replay:
- Delete all
nft_transferrecords - Reset all
deck.current_owner,deck.last_transfer_block,deck.last_transfer_attonull - Set
ponder_indexer_state.statusto"reindexing"
import { prisma } from "@emergexyz/db";
export async function clearPrismaOwnershipData() {
await prisma.nft_transfer.deleteMany({});
await prisma.deck.updateMany({
where: { current_owner: { not: null } },
data: { current_owner: null, last_transfer_block: null, last_transfer_at: null },
});
await prisma.ponder_indexer_state.upsert({
where: { id: "singleton" },
create: { status: "reindexing", started_at: new Date() },
update: { status: "reindexing", started_at: new Date(), last_indexed_block: null },
});
}Phase 4: Database Package Exports
4.1 Ownership Query Helpers
File: packages/database/src/nft-ownership.ts (new)
import { prisma } from './client.js';
export class NftOwnershipOperations {
/** Get current owner of a specific token (via deck) */
static async getTokenOwner(collectionId: string, tokenId: number) {
return prisma.deck.findFirst({
where: { collection_id: collectionId, token_id: tokenId },
select: {
id: true,
token_id: true,
current_owner: true,
mint_recipient: true,
mint_status: true,
minted_at: true,
last_transfer_block: true,
last_transfer_at: true,
},
});
}
/** Get all tokens owned by a wallet address */
static async getTokensByOwner(ownerAddress: string) {
return prisma.deck.findMany({
where: { current_owner: ownerAddress },
include: { collection: { select: { id: true, name: true, contract_address: true } } },
orderBy: { minted_at: 'desc' },
});
}
/** Get all minted tokens in a collection with current owners */
static async getCollectionTokens(collectionId: string) {
return prisma.deck.findMany({
where: { collection_id: collectionId, token_id: { not: null } },
select: {
id: true,
token_id: true,
current_owner: true,
mint_recipient: true,
mint_status: true,
},
orderBy: { token_id: 'asc' },
});
}
/** Get transfer history for a specific deck/token */
static async getDeckTransfers(deckId: string) {
return prisma.nft_transfer.findMany({
where: { deck_id: deckId },
orderBy: { block_number: 'desc' },
});
}
/** Get all transfers for a wallet (sent or received) */
static async getWalletTransfers(address: string) {
return prisma.nft_transfer.findMany({
where: {
OR: [
{ from_address: address },
{ to_address: address },
],
},
include: { deck: { select: { id: true, token_id: true, collection_id: true } } },
orderBy: { block_number: 'desc' },
});
}
/** Get indexer operational state */
static async getIndexerState() {
return prisma.ponder_indexer_state.findUnique({
where: { id: 'singleton' },
});
}
}4.2 Update Package Exports
File: packages/database/src/index.ts
Add to exports:
// NFT Ownership operations (populated by Ponder indexer)
export { NftOwnershipOperations } from './nft-ownership.js';
// Add to type-only re-exports:
export type {
nft_transfer,
ponder_indexer_state
} from '@prisma/client';4.3 Usage Example
After these changes, any app can query ownership through the deck model:
import { NftOwnershipOperations, prisma } from '@emergexyz/db';
// High-level API
const owner = await NftOwnershipOperations.getTokenOwner(collectionId, 1);
const myTokens = await NftOwnershipOperations.getTokensByOwner(walletAddress);
const history = await NftOwnershipOperations.getDeckTransfers(deckId);
// Or direct Prisma queries — deck already has ownership
const deck = await prisma.deck.findFirst({
where: { collection_id: collectionId, token_id: 1 },
select: { id: true, current_owner: true, mint_recipient: true, mint_status: true },
});
// Get all decks with their transfer history
const decksWithHistory = await prisma.deck.findMany({
where: { collection_id: collectionId, token_id: { not: null } },
include: { nft_transfers: { orderBy: { block_number: 'desc' } } },
});Phase 5: Turbo Integration + Environment Setup
5.1 Turbo Pipeline
File: turbo.json
Add new tasks:
{
"ponder:dev": {
"cache": false,
"persistent": true
},
"ponder:start": {
"dependsOn": ["^build"]
}
}5.2 Root Scripts
File: Root package.json
Add scripts:
{
"ponder:dev": "turbo run ponder:dev --filter=@emp/ponder",
"ponder:start": "turbo run ponder:start --filter=@emp/ponder"
}5.3 Environment Variables
| Variable | Description | Source |
|---|---|---|
PONDER_RPC_URL_ARBITRUM_ONE | Arbitrum One RPC | Same provider as ARBITRUM_ONE_RPC_URL |
PONDER_DATABASE_URL | PostgreSQL with ?schema=ponder | Same Neon DB, separate schema |
DATABASE_URL | PostgreSQL for Prisma writes | Same as other apps |
PONDER_PORT | HTTP port (default 42069) | Override if port conflicts |
5.4 Custom API Endpoints
File: apps/ponder/src/api/index.ts
Hono-based supplementary API served by Ponder:
| Endpoint | Purpose |
|---|---|
GET /ownership/:contractAddress/:tokenId | Current owner of a token |
GET /ownership/wallet/:address | All tokens owned by a wallet |
GET /transfers/:contractAddress/:tokenId | Transfer history for a token |
GET /health | Indexer status + last indexed block |
These are supplementary to Prisma queries — useful for real-time data or external consumers.
Phase 6: Testing + Verification
Development Workflow
# Terminal 1: Start Ponder dev server (Arbitrum One mainnet)
cd apps/ponder
cp .env.example .env # Fill in RPC URL and DB URLs
pnpm dev
# Terminal 2: Monitor logs
# Ponder logs show discovered contracts and processed eventsVerification Checklist
- [ ] Migration:
pnpm prisma migrate devcreatesnft_token_owner,nft_transfer,ponder_indexer_statetables anddeck.current_ownercolumn - [ ] Build:
pnpm buildsucceeds across the full monorepo - [ ] Discovery: Ponder discovers existing Diamond collections via
CollectionCreatedfactory events - [ ] Mint indexing: Mint a token via emprops-api →
nft_token_ownerrecord created with correctowner_addressandminted_to - [ ] Deck update: After mint,
deck.current_owneris populated automatically - [ ] Transfer indexing: Transfer a token on-chain →
nft_token_owner.owner_addressupdated,nft_transferrecord created - [ ] Query helpers:
NftOwnershipOperations.getTokensByOwner()returns correct results - [ ] Reindex: Restart Ponder → Prisma tables cleared and rebuilt from chain state
- [ ] Indexer state:
ponder_indexer_stateshows correctstatusandlast_indexed_block - [ ] Ponder API:
GET /indexer/statusreturns indexer status (Ponder reserves/health,/ready,/status,/metrics)
Integration Test Scenarios
- Fresh start: Ponder starts, discovers all existing collections, indexes all historical events, Prisma tables match on-chain state
- New collection: Deploy a new collection while Ponder is running → automatically discovered and indexed
- Mint + transfer: Mint token → transfer to different address → verify
nft_token_ownerreflects final owner,nft_transferhas both events - Multi-collection: Multiple collections with independent token ID spaces → verify no cross-contamination
- Reindex: Change ponder.schema.ts → Ponder reindexes → verify Prisma tables rebuilt correctly
Phase 7: Monitor Ownership UI
Add an "Ownership" tab to the monitor's NFT page (apps/monitor/src/app/nft/page.tsx) that displays Ponder-indexed ownership data from Prisma.
7.1 API Routes
Three new Next.js API routes proxy ownership data from emprops-api:
GET /api/nft/ownership?collectionId=<uuid> — Token ownership for a collection
Returns all minted tokens in a collection with their current on-chain owners (from deck.current_owner), mint recipients, mint status, and last transfer timestamps.
GET /api/nft/ownership/transfers?deckId=<uuid> — Transfer history for a token
Returns the full transfer history for a specific deck/token from nft_transfer, ordered by block number descending. Each record includes from/to addresses, block number, tx hash, and timestamp.
GET /api/nft/ownership/indexer-status — Ponder indexer health
Returns the ponder_indexer_state singleton: status (idle/indexing/reindexing), last indexed block, chain ID, and timestamps.
7.2 Ownership Tab UI
The 6th tab on the NFT page with three sections:
Indexer Status Banner:
- Shows Ponder indexer operational state (idle/indexing/reindexing)
- Last indexed block number
- "Syncing" indicator during reindex
Collection Token Ownership Table:
- Collection selector (reuses existing dropdown pattern from Mint/Admin tabs)
- Table columns: Token ID, Current Owner, Minted To, Mint Status, Last Transfer
- Owner addresses with copy-to-clipboard buttons
- Click a row to expand and show transfer history inline
Transfer History Panel (expandable per token):
- Shows: From → To, Block #, Tx Hash, Timestamp
- Tx hash links to Arbiscan (chain-aware)
- Mint events highlighted (from = 0x0000...)
7.3 Files
| File | Change |
|---|---|
apps/monitor/src/app/nft/page.tsx | Add Ownership tab (6th tab) with OwnershipTab component |
apps/monitor/src/app/api/nft/ownership/route.ts | New: token ownership endpoint |
apps/monitor/src/app/api/nft/ownership/transfers/route.ts | New: transfer history endpoint |
apps/monitor/src/app/api/nft/ownership/indexer-status/route.ts | New: indexer status endpoint |
7.4 Verification
- Start Ponder against Arbitrum One, wait for it to index
- Open monitor → NFT → Ownership tab
- Select a collection → see tokens with current owners
- Mint a new token → verify it appears with owner after Ponder indexes
- Transfer a token on-chain → verify owner updates and transfer history shows
Key Gotchas
startBlockvalues: Must be resolved from factory deployment tx hashes. Using0works but is slow (scans from genesis). Look up actual block numbers via Arbiscan.Diamond proxy events:
Transferevents are emitted at the Diamond proxy address, not the facet address. This is correct behavior — Ponder indexes by contract address, and the Diamond proxy IS the contract address.Ponder
factory()start block: The factory'sstartBlockmust be ≤ the earliestCollectionCreatedevent. If set too late, early collections won't be discovered.BigInt handling: Ponder uses
bigintfor block numbers and token IDs. Prisma usesBigInt. The sync layer must handle conversion:Number(tokenId)for Prisma Int fields, passbigintdirectly for Prisma BigInt fields.Address checksumming: Ponder returns lowercase hex addresses. Prisma stores whatever you give it. Normalize to lowercase in the sync layer to avoid duplicate records with different casing.
Neon connection limits: Ponder's internal pool + Prisma pool both connect to Neon. Keep Ponder's pool small (default is fine) and ensure the total doesn't exceed Neon's plan limits.
Ponder version: Use
0.10.x(stableonchainTableandfactory()API). Avoid0.11+until its breaking changes are evaluated.Reindex side effects: When Ponder reindexes, it replays all events. The Prisma sync layer must be idempotent (use
upsert, notcreate). TheclearPrismaOwnershipData()function should run once at the start of reindex.
Files Modified
| File | Change |
|---|---|
packages/database/prisma/schema.prisma | Add nft_transfer + ponder_indexer_state models, add fields to deck |
packages/database/src/index.ts | Export new types + operations |
turbo.json | Add ponder pipeline tasks |
Root package.json | Add ponder scripts |
apps/docs/src/.vitepress/config.ts | Sidebar links for ADR + IMPL |
apps/monitor/src/app/nft/page.tsx | Add Ownership tab (Phase 7) |
Files Created
| File | Purpose |
|---|---|
apps/ponder/package.json | App definition |
apps/ponder/tsconfig.json | TypeScript config |
apps/ponder/ponder.config.ts | Network + contract configuration (Arbitrum One only) |
apps/ponder/ponder.schema.ts | Ponder internal tables |
apps/ponder/ponder-env.d.ts | Type environment |
apps/ponder/.env.example | Environment template |
apps/ponder/abis/DiamondFactory.ts | Factory event ABI |
apps/ponder/abis/ERC721Diamond.ts | ERC721 event ABIs |
apps/ponder/src/index.ts | Handler registration |
apps/ponder/src/handlers/factory.ts | CollectionCreated handler |
apps/ponder/src/handlers/transfer.ts | Transfer handler (core) |
apps/ponder/src/lib/prisma-sync.ts | Prisma write layer |
apps/ponder/src/lib/reindex.ts | Reindex cleanup |
apps/ponder/src/api/index.ts | Custom API endpoints |
packages/database/src/nft-ownership.ts | Ownership query helpers |
apps/monitor/src/app/api/nft/ownership/route.ts | Token ownership endpoint |
apps/monitor/src/app/api/nft/ownership/transfers/route.ts | Transfer history endpoint |
apps/monitor/src/app/api/nft/ownership/indexer-status/route.ts | Indexer status endpoint |
