> For the complete documentation index, see [llms.txt](https://docs.trover.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.trover.tech/engineering/verify-2026-09-01-chain-first-nft.md).

# Verify — chain-first NFT ingestion + per-sale scatter (2026-09-01)

## This round SHIPS A MIGRATION

`packages/db/migrations/20260831120000_nft_token_metadata` — creates `NftTokenMetadata`. It is additive (one new table, one index, one unique constraint); nothing existing is altered or dropped.

Services changed: **api**, **worker**, **web**. Web goes out on its own via Vercel when this lands on `main`. The backend is manual:

```bash
cd ~/trover
git pull --ff-only origin main
docker compose -f docker-compose.yml -f docker-compose.production.yml run --rm migrate
docker compose -f docker-compose.yml -f docker-compose.production.yml build api worker
docker compose -f docker-compose.yml -f docker-compose.production.yml up -d --force-recreate api worker
```

Run the `migrate` service before `up -d` — the api reads `NftTokenMetadata` on `GET /collections/:slug/sales` and will 500 on every request until the table exists.

> Still not deployed from the previous round: `a3690be` (payment-decimals fix, **no migration**) and its notes `52a8bfb`. Pulling `main` now picks both up in the same build, so the checks in `docs/VERIFY-2026-08-31-price-decimals.md` apply to this deploy too.

## What changed

**Backend**

* Both on-chain indexers are now **chain-wide and head-only**. Mints scan `Transfer` with `topics[1] = 0x0` and no `address` filter; sales scan Seaport `OrderFulfilled` with the tracked-contract check moved to *after* the NFT item is decoded. Window starts at 1,000 blocks and halves on the RPC's `logs matched by query exceeds limit of 10000`, with a wedge guard that steps the cursor by `minLogBlockRange` after 5 consecutive failures at one block.
* An unknown contract in either scan **auto-registers** its `NftCollectionMarket` row on sight, provisionally slugged by contract address, then renamed by a low-priority `resolveNftCollectionMetadata` job. The `discoverNewNftCollections` poll is deleted — there is nothing left to discover once every mint arrives unsolicited.
* Neither scanner is gated on `OPENSEA_ENABLED` any more, and neither depends on a watch lease. Leases now govern only the OpenSea orderbook sync.
* New `trover:nft-market:<chain>:@discover` channel, published from both `publish()` and the outbox drain, served by `GET /v1/nft-market/discover/stream`.
* New `GET /v1/nft-market/collections/:slug/sales` (`?chain=&range=&limit=`) returning per-sale points plus the stat strip.
* `NftTokenMetadata` is populated opportunistically from payloads we already store; ranks are computed against `NftTraitCount` on the existing 15-minute `refreshTraitCounts` debounce. No new provider calls.

**Frontend**

* `NftSaleScatter`: a canvas overlay over an invisible line series (lightweight- charts 4.2.3 has no scatter series and cannot position markers by price). Hovering a dot shows the token image, `#tokenId`, rank, price and traits.
* Chart toolbar gets `[ Dots ] [ Candles ]`, dots default, persisted at `trover:nft-chart-mode`.
* The stat strip goes from 5 tiles to 13. Every tile renders `—` for a missing value, never `0`.
* `formatAdaptiveNumber` in `apps/web/src/lib/format-price.ts` keeps 3 significant digits below 1, so a 0.000408 floor no longer prints as `0`.
* Discover uses the `@discover` socket with the 5s poll as reconnect fallback.

## Units — read this before judging any number on screen

Every `native` figure the API returns is an **ETH equivalent, converted through USD**, whatever token the collection actually settles in. `floor_price_usd / floor_price` is 2433–2453 across all eight collections sampled, including the USDG-settled ones — i.e. the ETH rate.

So `market.floor_price`, `market.volume_24h`, event `priceNative` and `mint.priceNative` are all ETH, and the terminal labels them ETH. The one exception is `stats.data.total.floor_price`, which *is* quoted in the settlement currency (`floor_price_symbol`) and is only used when that currency is ETH. The settlement currency is surfaced separately in the header (`settles in USDG · …`) rather than being applied as a unit to ETH numbers.

## Checks after deploy

1. **Price fix landed** — `proofmachines` candles drop from 203–358 ETH to sub-1: `curl -s 'https://api.trover.tech/v1/nft-market/collections/proofmachines/candles?chain=robinhood&interval=1m'` Cross-check one sale against `rh-scan.com`, and confirm an ETH-settled collection's candles did *not* move.
2. **Chain-wide ingestion** — pick a collection nobody has opened in the UI and confirm it appears in `/v1/nft-market/discover` with mints inside \~2 minutes. Compare its mint count to a direct `eth_getLogs` on that contract. Before this round, mint ingestion had been dead since `2026-08-31T13:35:01Z` while a direct `eth_getLogs` showed 126 ERC-721 mints across 12 contracts in 200 blocks — that gap is the thing being closed.
3. **No lease dependency** — let a collection's watch lease expire; its mints and sales must keep flowing.
4. **Push, not poll** — `websocat 'wss://api.trover.tech/v1/nft-market/discover/stream'` and confirm rows arrive with no polling.
5. **Scatter** — open a collection terminal, hover a dot, confirm the image, `#tokenId`, traits and rank match the same sale in the activity feed. `Metadata Fetched` sitting at a low percentage is expected, not a failure.
6. **Migration applied** — `GET /collections/<any>/sales` returns 200 rather than 500.

## Rollback

`docker compose … up -d --force-recreate api worker` against the previous tag. The migration is additive and safe to leave in place.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://docs.trover.tech/engineering/verify-2026-09-01-chain-first-nft.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
