|
| 1 | +# Indexer Schema |
| 2 | + |
| 3 | +The indexer writes a single unified event log plus a set of current-state |
| 4 | +tables. Everything is a Ponder *onchain* table — the USD price cache lives |
| 5 | +there too, because indexing handlers can only read and write onchain |
| 6 | +tables. For the high-level role of the service, see the |
| 7 | +[overview](/indexer). |
| 8 | + |
| 9 | +## Events |
| 10 | + |
| 11 | +All user-facing activity flows into one `events` table — one row per |
| 12 | +indexed log, with a unified shape so an activity feed can render any event |
| 13 | +type without joining. |
| 14 | + |
| 15 | +```text |
| 16 | +source ∈ { cryptopunks_v1, cryptopunks_v2, wrapped_punks, cryptopunks_721, |
| 17 | + v1_wrapper, punks_market, punks_auction, stash, vault } |
| 18 | +
|
| 19 | +type ∈ { assign, transfer, stashed, unstashed, vaulted, unvaulted, |
| 20 | + escrowed, listing, listing_cancelled, bid, bid_adjusted, |
| 21 | + bid_cancelled, sale, wrap, unwrap, escrow_credit, |
| 22 | + escrow_withdrawal, lot_created, lot_cancelled, lot_cleared, |
| 23 | + lot_updated, auction_started, auction_settled, offer_placed, |
| 24 | + offer_cancelled, offer_adjusted } |
| 25 | +``` |
| 26 | + |
| 27 | +| Column | Holds | |
| 28 | +| ------------------------------------------------------- | ---------------------------------------------------------------------- | |
| 29 | +| `id` | Primary key (deterministic per log) | |
| 30 | +| `source`, `source_event`, `type` | Origin contract, raw event name, and normalized activity type | |
| 31 | +| `punk_id` | The Punk involved, when applicable | |
| 32 | +| `actor`, `from`, `to`, `buyer`, `seller`, `bidder`, `settler` | The addresses involved, by role | |
| 33 | +| `wei_amount`, `listing_wei`, `bid_wei`, `settlement_wei` | The relevant ETH amounts | |
| 34 | +| `usd_value_cents` | Denormalized USD-cent value of `wei_amount` (see [USD pricing](#usd-pricing)) | |
| 35 | +| `only_sell_to` | Directed-listing / restricted-lot target, when set | |
| 36 | +| `bid_id`, `lot_id`, `auction_id`, `offer_id` | The `PunksMarket` / `PunksAuction` entity ids | |
| 37 | +| `offer_kind` | `collection` / `trait` / `selection` for offer rows | |
| 38 | +| `tx_hash`, `block_number`, `log_index` | Onchain location | |
| 39 | +| `timestamp`, `day_unix` | Event time and its UTC day (the JOIN key against `eth_usd_prices`) | |
| 40 | + |
| 41 | +Indexes on `timestamp`, `source`, `type`, `punk_id`, and the actor columns |
| 42 | +back the common feed queries (global, per-source, per-type, per-Punk, and |
| 43 | +per-account). |
| 44 | + |
| 45 | +## Current-state collections |
| 46 | + |
| 47 | +Per-Punk current state is split into two tables because both contracts use |
| 48 | +ids `0..9999` and carry independent market state: |
| 49 | + |
| 50 | +- `punks` — canonical CryptoPunks (`punk_id` PK, `owner`, `native_owner`, |
| 51 | + `native_standard`, `is_wrapped`, `wrapper`, `last_sale_wei`, …). |
| 52 | +- `v1_punks` — June 9th 2017 Punks, with the same shape. |
| 53 | + |
| 54 | +In both, `owner` is the public owner (the ERC-721 owner while wrapped, the |
| 55 | +native owner otherwise) and `native_owner` preserves the underlying holder. |
| 56 | + |
| 57 | +## Native market state |
| 58 | + |
| 59 | +The two native markets each get a listings table and a per-Punk bid table, |
| 60 | +updated in place as state changes: |
| 61 | + |
| 62 | +| Table | One row per | Holds | |
| 63 | +| ------------------------------ | ----------- | ---------------------------------------------- | |
| 64 | +| `listings` / `v1_listings` | Punk | `seller`, `min_value_wei`, `only_sell_to`, `active` | |
| 65 | +| `punk_bids` / `v1_punk_bids` | Punk | `bidder`, `value_wei`, `active` | |
| 66 | + |
| 67 | +The per-Punk bid tables hold a single outstanding bid each, mirroring the |
| 68 | +contract, which only allows one. |
| 69 | + |
| 70 | +## PunksMarket criteria bids |
| 71 | + |
| 72 | +`PunksMarket` collection bids are decomposed for SQL matching. The scalar |
| 73 | +row lives in `market_bids`; the predicate is exploded into side tables so a |
| 74 | +"which bids match this Punk?" query runs as a join rather than re-deriving |
| 75 | +criteria in the client: |
| 76 | + |
| 77 | +| Table | Holds | |
| 78 | +| -------------------------------------- | ---------------------------------------------------------------------------- | |
| 79 | +| `market_bids` | `bid_id` PK, `bidder`, `bid_wei`, `settlement_wei`, `active`, the trait/color masks and pixel/color-count ranges, `has_include_ids`, and the raw `criteria_json` / `include_ids_json` / `exclude_ids_json` payloads | |
| 80 | +| `bid_traits` | One row per `(bid_id, trait_id, kind)`, `kind ∈ { required, forbidden, anyOf }` | |
| 81 | +| `bid_colors` | One row per `(bid_id, color_id, kind)`, same `kind` set | |
| 82 | +| `bid_include_ids` / `bid_exclude_ids` | One row per `(bid_id, punk_id)` allow/deny entry | |
| 83 | + |
| 84 | +The matching query joins these against a static Punk dataset, seeded once |
| 85 | +from the SDK offline bundle: |
| 86 | + |
| 87 | +| Table | Holds | |
| 88 | +| -------------- | ------------------------------------------------ | |
| 89 | +| `punk_traits` | One row per `(punk_id, trait_id)` | |
| 90 | +| `punk_colors` | One row per `(punk_id, color_id)` | |
| 91 | +| `punk_visuals` | `punk_id` PK, `pixel_count`, `color_count` | |
| 92 | + |
| 93 | +The hosted matching predicate is exposed through the |
| 94 | +[`/bids/matching/*` routes](/indexer/api#bids). |
| 95 | + |
| 96 | +## Auction state |
| 97 | + |
| 98 | +`PunksAuction` lots, auctions, and offers each mirror their contract struct |
| 99 | +so handlers can recover the seller/offerer at cancel or settle time without |
| 100 | +re-reading from chain (the contract has often deleted the entity by then): |
| 101 | + |
| 102 | +| Table | Holds | |
| 103 | +| -------------------- | -------------------------------------------------------------------------------------- | |
| 104 | +| `auction_lots` | `lot_id` PK, `seller`, `reserve_wei`, `only_sell_to`, `item_count`, `active` | |
| 105 | +| `auction_lot_items` | One row per `(lot_id, item_index)`: `standard`, `punk_id`, `weight_bps` | |
| 106 | +| `auction_auctions` | `auction_id` PK, `lot_id`, `seller`, `latest_bidder`, `latest_bid_wei`, `end_timestamp`, `settled` | |
| 107 | +| `auction_offers` | `offer_id` PK, `offerer`, `amount_wei`, `slot_count`, `kind`, `specific_punk_id`, `active` | |
| 108 | + |
| 109 | +`auction_lots` and `auction_offers` are kept around after an entity is |
| 110 | +consumed or cancelled so the corresponding handlers can still resolve the |
| 111 | +party. |
| 112 | + |
| 113 | +## Accounts |
| 114 | + |
| 115 | +`accounts` is a per-EOA registry, populated whenever an address appears in |
| 116 | +an event. It pins each user's deterministic custody contracts — |
| 117 | +`vault` (`PunksVaultFactory.predictVault`) and `stash` |
| 118 | +(`StashFactory.stashAddressFor`) — plus the `user_proxy` from the legacy |
| 119 | +wrapper, and tracks whether each clone is `*_deployed` on-chain. The vault, |
| 120 | +stash, and proxy columns are individually indexed for reverse lookup |
| 121 | +(custody address → owner EOA), which powers profile-URL canonicalization. |
| 122 | + |
| 123 | +## USD pricing |
| 124 | + |
| 125 | +Every event row carries `usd_value_cents` — the USD-cent equivalent of |
| 126 | +`wei_amount` at the day's ETH/USD price, **denormalized onto the row at |
| 127 | +indexing time**. Consumers reading recent sales never need a JOIN or a |
| 128 | +follow-up RPC. |
| 129 | + |
| 130 | +The day-keyed cache lives in `eth_usd_prices` (`day_unix` PK, |
| 131 | +`eth_usd_cents` as Stripe-style integer cents, `source`, `block_number`), |
| 132 | +filled by two layered sources: |
| 133 | + |
| 134 | +1. **CSV historical baseline** — `data/eth_usd_prices.csv` ships daily |
| 135 | + closes from the June 9th launch through today, seeded once on startup and |
| 136 | + guarded by a row in `backfill_markers` so the seed is idempotent across |
| 137 | + restarts. |
| 138 | +2. **Chainlink live fill** — when a sale handler finds no cached row for the |
| 139 | + event's UTC day, it reads Chainlink's ETH/USD aggregator at the event's |
| 140 | + historical block, caches the result under `day_unix`, and stamps the row. |
| 141 | + Blocks before Chainlink existed (mid-2021) fall back to the CSV baseline; |
| 142 | + if neither covers the day, `usd_value_cents` stays null. |
0 commit comments