# indexer

basefun's API service does three jobs in one Node process:

* **Indexer** — tail-polls Base for events, materializes them into Postgres.
* **REST API** — serves the web front (`/api/tokens`, `/api/tokens/:addr/...`, `/api/trades/recent`, etc.).
* **Image proxy** — serves uploaded launch images from `Image` table at `/api/img/:id`.

Backed by Prisma + viem + Fastify.

## Indexer loop

```
boot:
  ensure schema (idempotent)
  backfill stats for any token whose marketCapUSDC == 0
  fetch IndexerCursor.lastBlock (default = configured START_BLOCK)

loop:
  refresh cursor (so manual resets via SQL are picked up)
  head = publicClient.getBlockNumber()
  if cursor >= head: sleep TAIL_POLL_MS (1s) and continue
  while cursor < head:
    to = min(cursor + 200, head)
    processRange(cursor + 1, to)
    cursor.lastBlock = to
```

200-block chunks keep `eth_getLogs` calls under most providers' rate limits. Catch-up logs every chunk; tail polling is silent.

## processRange(fromBlock, toBlock)

For each factory address (both V2 and V3):

```
1. Fetch TokenCreated logs from factory in [from, to]
2. For each log:
     ensureTokenRow(token, curve, factoryVersion, args)
     refreshTokenStats(token, curve)        // reads getReserveUSDC, spotPriceUSDC, etc.
     verifyAndPersist(token)                // fire-and-forget BaseScan verification
```

Then for **all known curves** (one batch):

```
3. Fetch Bought/Sold/Graduated logs across all curves in [from, to]
4. For Bought/Sold:
     upsert Trade row, keyed by (txHash, logIndex)
     refreshTokenStats(token, curve)
5. For Graduated:
     resolve Uni V2 pair address; persist if found
6. For graduated tokens whose pair hasn't been resolved yet, try again
```

Then for **graduated tokens with a known pair**:

```
7. Fetch Uni V2 Swap logs from each pair in [from, to]
8. Decode side (BUY if USDC in / SELL if USDC out), upsert Trade row
```

Then for **all known tokens**:

```
9. Fetch Transfer logs from each token in [from, to]
10. For each Transfer:
      INSERT INTO HolderTransferCursor (idempotency marker)
      atomic SQL: balance[from] -= value; balance[to] += value
```

## Why upsert by (txHash, logIndex)

If the indexer crashes mid-range, on restart it re-reads from `IndexerCursor.lastBlock` and may re-process some of the same logs. Upserting by the log's unique key (`(txHash, logIndex)` = `@@unique` in Prisma) makes the operation **idempotent**. No double-counting.

The Holder updates use a different idempotency strategy: insert into `HolderTransferCursor` **first** with the same `(txHash, logIndex)` PK; if that throws a duplicate-key error, skip. The atomic SQL `balance += delta` is only reached once per log.

## REST routes

| Route                                             | Use                                                                    |
| ------------------------------------------------- | ---------------------------------------------------------------------- |
| `GET /api/health`                                 | Returns `{ok, chain, factory, factoryV3, avntis}`                      |
| `GET /api/tokens?limit=&sort=&graduated=&search=` | Home list. `sort=mcap` uses live MC; `sort=new` orders by `createdAt`. |
| `GET /api/tokens/:addr`                           | Single token + live reserve/spot/MC reads from the curve               |
| `GET /api/tokens/:addr/trades?limit=`             | Trades tab                                                             |
| `GET /api/tokens/:addr/candles?interval=&series=` | OHLCV per interval, computed on the fly from `Trade` rows              |
| `GET /api/tokens/:addr/holders?limit=`            | Holders tab                                                            |
| `GET /api/tokens/:addr/earnings`                  | Creator earnings rollup                                                |
| `GET /api/positions/:wallet`                      | Wallet's basefun positions, valued at spot                             |
| `GET /api/markets`                                | The 99 Avantis markets used in `/create`                               |
| `GET /api/stats`                                  | `{tokens, graduated}` counts                                           |
| `GET /api/trades/recent?limit=`                   | Cross-token recent trades for the home sidebar                         |
| `POST /api/tokens/:addr/socials`                  | Edit socials (creator-wallet-signed)                                   |
| `POST /api/upload`                                | Image upload, returns `{id, url}`                                      |
| `GET /api/img/:id`                                | Image proxy                                                            |

## Boot script behavior

The web service runs `scripts/start-railway.cjs` on boot, which spawns:

1. `prisma db push --skip-generate` (idempotent schema sync).
2. Next.js standalone server on `WEB_PORT` (loopback).
3. Fastify API on `$PORT` (public), proxying any non-`/api/*` request to Next.

A single Railway service hosts both UI and API on the same hostname.

## Why no Postgres LISTEN/NOTIFY

The chart/trades panel uses `useWatchContractEvent` directly against the RPC's WebSocket. The DB is a cache for fast aggregations, not the source of truth for live updates. New trades hit the UI from two sides independently: WS event (instant) and indexer (\~1 block later, with derived fields like reserveAfter).


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://basefun.gitbook.io/basefun-docs/indexer.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
