> ## Documentation Index
> Fetch the complete documentation index at: https://docs.windrose.market/llms.txt
> Use this file to discover all available pages before exploring further.

# Indexer

> Run the Ponder indexer that serves launches, trades, candles, holders, currencies, vaults and rates over GraphQL.

The indexer is a [Ponder](https://ponder.sh) app in `packages/indexer`. One instance follows one chain, selected by `CHAIN_ID`, and serves the GraphQL API described in the [API reference](/api) on port 42069. Every chain the web app offers needs its own instance.

## What it indexes

| Source                                                         | Events                                                                                   | Tables written                                  |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `LaunchFactory`                                                | `LaunchCreated`, `LaunchMetadata` (plus the `setup` seed)                                | `launch`, `curve`, `currency`                   |
| `BondingCurve` (factory pattern on `LaunchCreated.curve`)      | `Buy`, `Sell`, `ReadyToGraduate`, `Graduated`, `CreatorFeesClaimed`, `ProtocolFeesSwept` | `trade`, `candle`, `launch`, `pool`, `feeClaim` |
| `LaunchToken` (factory pattern on `LaunchCreated.token`)       | `Transfer`                                                                               | `holder`, `launch.holderCount`                  |
| `FxVault` (every `currencies[*].vault` of the deployment file) | `Minted`, `Redeemed`, `Deposited`, `Withdrawn`, `Donated`                                | `vaultEvent`, `vaultSnapshot`                   |
| `KeeperFxOracle`                                               | `RatePosted`                                                                             | `rate`, `currency.lastRate`                     |
| `CurrencyRegistry`                                             | `CurrencySet`                                                                            | `currency`                                      |
| `PoolManager` (Uniswap v4)                                     | `Swap`, only for pools of graduated launches                                             | `trade`, `candle`, `launch.lastPrice*`          |

The `PoolManager` source is dropped entirely when `INDEX_POOL_SWAPS=false` (see below).

## Setup

<Steps>
  <Step title="Install and configure">
    ```bash theme={"system"}
    pnpm install                                        # repository root
    cp packages/indexer/.env.example packages/indexer/.env.local
    ```

    Ponder reads `.env.local`. Set `CHAIN_ID`, the RPC for that chain and the start block (the deployment block; see "Addresses and start block").
  </Step>

  <Step title="Generate the ABIs">
    ```bash theme={"system"}
    pnpm --filter @launchpad/indexer codegen
    ```

    This re-exports the JSON ABIs from `@launchpad/abis` as `as const` TypeScript in `abis/index.ts` and writes `ponder-env.d.ts`. `abis/index.ts` is also regenerated by `postinstall`, so run `pnpm install` or `pnpm codegen` again after `pnpm abis` (repository root) changes the ABIs.
  </Step>

  <Step title="Run">
    ```bash theme={"system"}
    pnpm --filter @launchpad/indexer dev                # or `pnpm indexer` from the repository root
    ```

    GraphiQL opens at `http://localhost:42069`.
  </Step>
</Steps>

### Commands

| Command          | What it does                                                                                                |
| ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `pnpm dev`       | Ponder dev server with hot reload. Uses PGlite in `.ponder/pglite-<CHAIN_ID>` unless `DATABASE_URL` is set. |
| `pnpm start`     | Production indexing plus API. Use with `DATABASE_URL` (Postgres). Requires `DATABASE_SCHEMA` or `--schema`. |
| `pnpm serve`     | API only, against an already indexed Postgres database. Requires `DATABASE_SCHEMA`.                         |
| `pnpm codegen`   | Regenerates `abis/index.ts` and `ponder-env.d.ts`.                                                          |
| `pnpm typecheck` | `tsc --noEmit`.                                                                                             |

## Addresses and start block

Addresses come from `packages/abis/deployments/<CHAIN_ID>.json`, the file written by `contracts/script/Deploy.s.sol` (its shape is the `Deployment` type in `@launchpad/abis`). It is loaded lazily, so the indexer typechecks and boots without it; without a file or overrides every address is zero, a warning is logged and nothing is indexed. A file whose `chainId` differs from `CHAIN_ID` is rejected at startup.

Resolution order, highest priority first:

1. Environment overrides: `FACTORY_ADDRESS`, `REGISTRY_ADDRESS`, `KEEPER_ORACLE_ADDRESS`, `POOL_MANAGER_ADDRESS`, `VAULT_ADDRESSES` (comma separated), and `START_BLOCK_<chainId>` then `START_BLOCK` (a block number or `latest`).
2. The deployment file (or the one named by `DEPLOYMENTS_FILE`), including its `startBlock` or `deployBlock` field when present.
3. Zero addresses and block 0.

<Warning>
  Set the start block. `scripts/deploy-mainnet.sh` records the deploy block as `startBlock` in the deployment file, but the testnet script does not, and syncing from block 0 on a chain with 0.5 s blocks takes days. Use the block of the deployment transaction. The suffixed form `START_BLOCK_<chainId>` lets one `.env.local` serve an indexer per chain.
</Warning>

When no deployment file and no `VAULT_ADDRESSES` are available, vaults are discovered from the registry's `SyntheticCreated` event instead.

## Environment

| Variable                                                                               | Default                                                               | Notes                                                                                                                                                                                                                                     |
| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CHAIN_ID` (or `LAUNCHPAD_CHAIN_ID`)                                                   | `46630`                                                               | Registry entry in `packages/abis/chains.json`: selects the deployment file, the default RPCs, the `eth_getLogs` range and which suffixed variables are read. Unknown ids fail at startup with the list of registered chains.              |
| `PONDER_RPC_URL_<chainId>`                                                             | the registry's `rpcs` (alternates), else its `rpc`                    | HTTP RPC. Several URLs can be given comma separated; Ponder load-balances across them.                                                                                                                                                    |
| `PONDER_WS_URL_<chainId>`                                                              | the registry `ws`                                                     | Websocket for realtime blocks. An empty string disables it and polls over HTTP.                                                                                                                                                           |
| `START_BLOCK_<chainId>`, then `START_BLOCK`                                            | file `startBlock`, else `0`                                           | Block number or `latest`.                                                                                                                                                                                                                 |
| `FACTORY_ADDRESS`, `REGISTRY_ADDRESS`, `KEEPER_ORACLE_ADDRESS`, `POOL_MANAGER_ADDRESS` | from the deployment file                                              | Override or replace the file.                                                                                                                                                                                                             |
| `VAULT_ADDRESSES`                                                                      | `currencies[*].vault`                                                 | Comma separated `FxVault` addresses.                                                                                                                                                                                                      |
| `DEPLOYMENTS_FILE`                                                                     | `packages/abis/deployments/<chainId>.json`                            | Alternative path to a deployment JSON.                                                                                                                                                                                                    |
| `DATABASE_URL`                                                                         | unset (PGlite)                                                        | Postgres connection string.                                                                                                                                                                                                               |
| `DATABASE_SCHEMA`                                                                      | `public` in `dev`                                                     | Schema for the indexed tables. Required by `start` and `serve` (or pass `--schema`). One per chain.                                                                                                                                       |
| `PGLITE_DIR`                                                                           | `.ponder/pglite-<chainId>`                                            | PGlite directory when `DATABASE_URL` is unset.                                                                                                                                                                                            |
| `PORT`                                                                                 | `42069`                                                               | API port. One per instance.                                                                                                                                                                                                               |
| `ETH_GETLOGS_BLOCK_RANGE`                                                              | the registry `ethGetLogsBlockRange` (Arc 50, Robinhood 1000), else 50 | Blocks per `eth_getLogs` request.                                                                                                                                                                                                         |
| `MAX_REQUESTS_PER_SECOND`                                                              | `8`                                                                   | RPC pacing. Public endpoints answer 429 above roughly 10 requests per second.                                                                                                                                                             |
| `POLLING_INTERVAL_MS`                                                                  | `4000`                                                                | Block polling interval when the websocket is off.                                                                                                                                                                                         |
| `INDEX_POOL_SWAPS`                                                                     | `true`                                                                | `false` drops the `PoolManager` source. Required on chains that use the shared official Uniswap v4, where every swap on the chain would otherwise be delivered to the indexer. The token page then reads the pool price from `StateView`. |
| `VAULT_CHUNK`                                                                          | the registry `maxLogAddresses` (Arc 20, Robinhood 60), else 20        | Vault addresses per log filter; the vault list is split into three fixed chunks (`FxVaultA`, `FxVaultB`, `FxVaultC`) because public RPCs cap the addresses per `eth_getLogs`.                                                             |
| `PONDER_LOG_LEVEL`                                                                     | Ponder's default                                                      | `info` on the production box.                                                                                                                                                                                                             |

## RPC requirements

The indexer needs an endpoint that answers topic-filtered `eth_getLogs` at a steady rate and, because the handlers read contract state at the event's block (`vault.status()` for snapshots, for instance), archive state. Most public endpoints fail one of the two.

| Chain                   | Endpoint for the indexer                                                                                                                                                    | Block range            | Why the others do not work                                                                                                                                                                                                                      |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Arc testnet 5042002     | The mirrors, comma separated: `https://rpc.blockdaemon.testnet.arc.network,https://arc-testnet.drpc.org,https://5042002.rpc.thirdweb.com`, with `MAX_REQUESTS_PER_SECOND=6` | 50                     | `rpc.testnet.arc.io` rate-limits topic-filtered `eth_getLogs` almost immediately and the indexer never catches up. The mirrors cap a query at about 2000 logs, about 1000 blocks and 20 addresses. Alchemy and QuickNode offer keyed endpoints. |
| Robinhood testnet 46630 | `https://robinhood-testnet.drpc.org`                                                                                                                                        | 100 (dRPC's free plan) | The official RPC and publicnode are full nodes without historical state (reads at the event block fail with "historical state missing"). thirdweb rejects server IPs without a client id. Alchemy's free tier caps `eth_getLogs` at 10 blocks.  |
| Robinhood Chain 4663    | `https://robinhood.drpc.org`, the only public archive endpoint                                                                                                              | 100                    | The official RPC caps `eth_getLogs` at 10,000 logs and answers 429 after a burst. Use a paid endpoint for anything serious and keep `MAX_REQUESTS_PER_SECOND` low on the public one.                                                            |

<Note>
  The `.env.example` still lists `PONDER_RPC_URL_5042002=https://rpc.testnet.arc.io` as a starting point. Replace it with the mirrors before indexing anything on Arc testnet.
</Note>

## Database

Without `DATABASE_URL`, Ponder keeps an embedded PGlite database under `packages/indexer/.ponder/pglite-<CHAIN_ID>`, so two instances for two chains never share a directory. With Postgres (a `docker run postgres:16` is fine), give each instance its own schema: `DATABASE_SCHEMA` or `ponder start --schema <name>`. Backfill sits around 600 to 700 MB of RAM per instance; the production pm2 entries restart an indexer above 1200 MB.

## Endpoints

| Path                                             | Purpose                                               |
| ------------------------------------------------ | ----------------------------------------------------- |
| `POST /graphql` (GraphiQL at `/` and `/graphql`) | The GraphQL API. See the [API reference](/api).       |
| `/sql/*`                                         | SQL over HTTP for `@ponder/client`.                   |
| `/health`                                        | Process is up.                                        |
| `/ready`                                         | Historical sync finished.                             |
| `/status`                                        | Indexed block per chain; compare with the chain head. |
| `/metrics`                                       | Prometheus metrics.                                   |

The API answers CORS (`access-control-allow-origin: *`), so the browser can query it from another origin without proxy rules.

## One instance per chain

The web app talks to `NEXT_PUBLIC_INDEXER_URL_<chainId>` for each chain in its switcher, so run one indexer per chain from the same package with a different `CHAIN_ID` and `PORT`. Locally:

```bash theme={"system"}
CHAIN_ID=46630   PORT=42069 pnpm --filter @launchpad/indexer dev     # Robinhood testnet
CHAIN_ID=5042002 PORT=42070 pnpm --filter @launchpad/indexer dev     # Arc testnet
```

The production box runs three through `deploy/ecosystem.config.cjs`:

| Chain                   | pm2 name                      | Port  | Schema          | Public URL                                                   | Notes                                                                                                               |
| ----------------------- | ----------------------------- | ----- | --------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| Robinhood Chain 4663    | `windrose-indexer`            | 42069 | `windrose`      | `https://api.windrose.market`                                | `INDEX_POOL_SWAPS=false`, `ETH_GETLOGS_BLOCK_RANGE=100`, `START_BLOCK_4663` from the deployment file's `startBlock` |
| Robinhood testnet 46630 | `windrose-indexer-rh-testnet` | 42071 | `launchpad`     | `https://api.windrose.market/testnet/` (an nginx `location`) | `ETH_GETLOGS_BLOCK_RANGE=100`                                                                                       |
| Arc testnet 5042002     | `windrose-indexer-arc`        | 42070 | `launchpad_arc` | `https://api-arc.windrose.market`                            | `START_BLOCK_5042002=62459763`                                                                                      |

The three share one `.env.local`: the RPC and start-block variables carry the chain id, and `deploy/ecosystem.config.cjs` sets `CHAIN_ID`, `PORT` and the per-chain overrides on top. pm2 runs `node_modules/ponder/dist/esm/bin/ponder.js start --schema <schema>` directly rather than the `.bin` shim, which pnpm writes as a shell script. A chain whose indexer is down still works in the web app from on-chain reads, with "indexer offline" in place of charts, trades and holders.
