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

# Build a Canopy Event Indexer and Market Data Service

Canopy emits a complete, self-contained set of on-chain events for every state transition — offer creation, partial fills, cancellations, series creation, claim issuance (via fills), and redemptions. Because all state changes are observable through these events, you can build a reliable, fully-reconstructible market data service by ingesting them from the deployment block forward without making repeated contract storage reads. This guide covers the architecture of a Canopy indexer, the key events you need to handle, sample ingestion code, reorg handling, and patterns for serving order book data to frontends and analytics dashboards.

## Architecture Overview

A production-grade Canopy indexer consists of three loosely coupled layers:

1. **Ingestion layer** — polls the RPC node using `eth_getLogs` (or subscribes via WebSocket) to fetch Canopy events in chronological block order and writes raw event records to a local store.
2. **State layer** — applies each event as a delta to a structured offer and series table, maintaining derived fields like `status`, `assetFilled`, and `assetRemaining` so downstream consumers never need to re-derive state themselves.
3. **API layer** — exposes a REST or GraphQL interface that queries the state layer and returns pre-computed order book snapshots, series metadata, and fill history to frontends and analytics tools.

For small markets or development environments, the ingestion and state layers can live entirely in memory. For production, use a persistent store keyed by `offerId` and `seriesId`.

## Key Events to Index

Handle each of the following events to maintain complete and accurate state:

| Event            | Action                                                                                                   |
| ---------------- | -------------------------------------------------------------------------------------------------------- |
| `OfferCreated`   | Insert offer record with `escrowedAssetAmount`; set `assetFilled = 0`, `status = OPEN`                   |
| `OfferFilled`    | Increment `assetFilled` by `assetUnits`; set `status = FILLED` when `assetFilled == escrowedAssetAmount` |
| `OfferCancelled` | Set `status = CANCELLED`; store `assetDebited` and `assetReceived`                                       |
| `SeriesCreated`  | Insert series record with `claimToken` address, `asset`, and `unlockTime`                                |
| `Redeemed`       | Decrement `series.backing` by `amount`; record `assetReceived`                                           |

<Note>
  All event parameters needed for full state reconstruction are included in the event payloads themselves. You do not need to call `offers()` or `series()` contract reads during initial sync — the events are self-contained, which significantly speeds up historical backfill.
</Note>

## Sample Indexer Loop

The following TypeScript snippet shows a block-range ingestion function that processes `OfferCreated` events and upserts them into a database. Extend the same pattern to cover the remaining event types listed above.

```typescript theme={null}
const DEPLOY_BLOCK = 1000000; // replace with actual deployment block

async function indexEvents(fromBlock: number, toBlock: number) {
  const created = await venue.queryFilter(
    venue.filters.OfferCreated(), fromBlock, toBlock
  );
  for (const e of created) {
    await db.upsertOffer({
      offerId: e.args.offerId,
      seller: e.args.seller,
      asset: e.args.asset,
      quote: e.args.quote,
      escrowedAssetAmount: e.args.escrowedAssetAmount,
      assetFilled: 0n,
      pricePerUnit: e.args.pricePerUnit,
      expiry: e.args.expiry,
      unlockTime: e.args.unlockTime,
      allowedBuyer: e.args.allowedBuyer,
      status: "OPEN",
    });
  }
  // repeat for OfferFilled, OfferCancelled, SeriesCreated, Redeemed
}
```

To run a continuous sync, call `indexEvents` in a loop — advancing `fromBlock` by your chosen chunk size on each iteration — until you reach the chain head, then switch to polling or subscribing for new blocks.

<Tip>
  For public RPC endpoints that enforce response size limits, keep your block range window to 2,000–10,000 blocks per call. For a dedicated node or a node provider with higher limits, you can use larger windows to speed up historical backfill.
</Tip>

## Handling Reorgs

Chain reorganizations can invalidate events you have already processed. To handle them gracefully:

* Store the `blockNumber` and `transactionHash` alongside every event record when you ingest it.
* On each new polling cycle, compare the latest stored block hash against the current canonical chain. If they diverge, you have detected a reorg.
* Roll back all event records with `blockNumber` greater than the last stable (finalized) block, then re-index from that point forward.

For Robinhood Chain (Arbitrum Orbit stack), check the finality guarantees of the network to determine a safe confirmation depth below which reorgs are negligible.

## Serving Market Depth

Once your state layer is populated, computing an order book for a trading pair is a straightforward query:

1. Filter all offers where `asset == targetAsset` AND `quote == targetQuote` AND `status == OPEN` AND `expiry > now`.
2. Compute `assetRemaining = escrowedAssetAmount - assetFilled` for each matching offer.
3. Sort by `pricePerUnit` — ascending for asks, descending for bids — to produce depth ladders.
4. Optionally aggregate offers at the same price level into a single depth entry for a traditional order book display.

Expose this computation as a cached endpoint (refreshed on each new block) so frontends can request the current book without re-running the aggregation themselves.

<Tip>
  You can run a lightweight in-memory indexer for small markets or during development — no database required. For production deployments handling multiple trading pairs or high fill frequency, migrate to a persistent store such as PostgreSQL or SQLite keyed by `offerId`, and add indexes on `(asset, quote, status)` for fast pair lookups.
</Tip>
