> 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-04-traders-and-rpc.md).

# Verify — trader leaderboard, profiles, and two RPC pool defects

## There IS a migration this round

`packages/db/migrations/20260904140000_trader_profiles_and_fill_pnl` — additive only: three nullable/defaulted columns on `trading_profiles`, two nullable columns on `trade_position_fills`, and two indexes. No table rewrite, no long lock. `trade_position_fills` is small next to `nft_market_events`, so the index build is quick.

```bash
cd ~/trover
git pull --ff-only origin main
docker compose -f docker-compose.yml -f docker-compose.production.yml build api worker migrate
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 up -d --force-recreate api worker
```

Build **before** migrate — `run --rm migrate` runs the migrations baked into its image, not the working tree.

## Two RPC defects found while the ownership walk was running

**1. Dwellir's free plan cannot serve `eth_getLogs` at all.**

```
403 "Your current plan does not support the eth_getLogs method."
```

This is a *capability* refusal, not a rate limit, so the `|rps=0.5` pacing from last round could never have helped it — pacing fixed the 429s on the methods the plan does support (`eth_blockNumber`, `eth_getTransaction*`), which is why it looked solved. It has been removed from `ROBINHOOD_LOG_RPC_URLS` on the server (backup taken; the var went from 2 endpoints to 1). It stays in `ROBINHOOD_NFT_ARCHIVE_RPC_URLS`, where it is used for `getTransaction` / `getTransactionReceipt` and works.

**The log pool is now a single endpoint with no fallback.** That is the direct argument for buying a log-capable provider — see the RPC note below.

**2. `exceedsLogLimit` missed block-range refusals.** Ink's public endpoint refuses with `"block range greater than 10000 max"` under **-32602** (invalid-params), not -32000. The detector only matched result-count phrasing, so on Ink the walk threw on its very first window instead of halving, and those collections advanced zero blocks. The pattern now covers both, and deliberately still does **not** match the Dwellir capability message, because halving cannot fix that.

## Trader leaderboard

`GET /v1/traders/leaderboard?window=24h&limit=50`

Ranked on **realized** PNL, not total. Realized is the only settled figure: an unrealized mark moves with the price feed, so ranking on it would reshuffle the board without anyone trading.

Windowed PNL needed a place to live. `trade_positions` carried only a running total, while the per-sell figure was computed in `trade-positions.ts` and thrown away into an increment — so "who made money in the last 24 hours" had no answer. It is now stored on the fill.

**Rows written before this migration have `realizedPnlUsd = NULL` and are excluded, not counted as zero.** Back-filling would mean replaying every position's basis history. Each row reports `attributedFills` so a partially-covered board says so rather than silently ranking on incomplete data. Expect the board to be sparse until sells accumulate after deploy.

```bash
curl -s 'https://api.trover.tech/v1/traders/leaderboard?window=24h&limit=5' | python3 -m json.tool
```

## Privacy

Every response goes through one `publicIdentity()` helper. The rule:

* **`walletAddress` is returned only when `profilePublic` is true.** Default is false, so doing nothing is the safe outcome.
* Everyone else gets `walletShort` — `0x143d…ff42` — enough to recognise a trader across sessions, not enough to look them up on an explorer.
* A trader with no handle still appears; they are on the board because they traded, not because they signed up for a name.

The client cannot widen this: it renders what the API sends, so a UI change cannot leak an address.

To confirm, pick any leaderboard row and check `walletAddress` is `null` while `walletShort` is set, until someone opts in.

## Handles

* `POST /v1/traders/me/username` — `{ "username": "alice" }`, auth required. Lowercased and matched against `^[a-z0-9_]{3,20}$`; a reserved list blocks impersonation (`trover`, `admin`, `support`, …). Uniqueness is settled by the database unique index, so two simultaneous claims cannot both succeed.
* `PATCH /v1/traders/me/visibility` — `{ "profilePublic": true }`.
* `GET /v1/traders?q=ali` — prefix search; only profiles with a handle.
* `GET /v1/traders/:username` — the public record.

**Stored lowercase on purpose.** Postgres unique indexes are case-sensitive, so without folding, `Alice` would be claimable alongside `alice`.

## Nav changes

* `nfts` and `league` removed from the **trover.tech** nav. Both routes still resolve — only the nav entries are gone — so existing links and indexed search results keep working.
* `traders` added to the **dex.trover.tech** nav.

## Airdrop countdown

`NEXT_PUBLIC_TRADER_AIRDROP_AT` (ISO instant) shows the banner and counts down. Unset hides it, which is the right default: an airdrop with no announced deadline should not display one.

**The countdown does not move any funds.** It ranks and displays. Distribution runs through the existing `AirdropCampaign` / `AirdropRecipient` admin path, which is deliberate — an automatic transfer triggered by a timer is not something to ship without you explicitly asking for it.

## What is NOT in this round

* The P\&L share card on sell.
* The account UI for claiming a handle and toggling visibility (the API exists; nothing calls it yet).
* Sitemap entries for `/traders` and profile pages.

***

## Addendum — P\&L card and SEO (same round, later commits)

### P\&L card on sell

`GET /v1/traders/me/sales?limit=1&collectionSlug=<slug>` returns recent sells with the profit each one booked, read from the fill rather than recomputed from the position's running total — the card is about one trade, not a lifetime.

The terminal fires it when a **sell** action reaches a confirmed terminal state, guarded by the action id so a re-render cannot reopen a dismissed card. The position ledger is written by the worker off the confirmed trade, so the fill can land a moment after the action reports confirmed; the card retries at 0s, 1.5s and 4s and then gives up. A missing card is a much smaller wound than a card quoting the previous trade.

Styled to match `ActionReceiptCard` deliberately — Trover mark, the asset's own art, then the numbers — because it is a receipt for a closed trade and should read like the other receipts.

**Sales with no attributable basis show proceeds, not a profit of zero.** Fills written before the migration, and assets acquired outside Trover, have no cost basis; the card says "cost basis unknown" rather than claiming a return. `realizedPct` is likewise null against a zero basis — a free mint sold at any price is not an infinite return, it is a return with no denominator.

To verify: sell an NFT you bought through Trover and confirm the card shows a realized figure; sell one you did not and confirm it shows proceeds with "not attributed".

### SEO

Collection terminals had **no `generateMetadata` at all** — every one inherited the root layout's title, so thousands of pages were indistinguishable to a crawler. Each now carries its own title, description, keywords, canonical, OpenGraph and Twitter card built from its live floor / owners / sales, plus `CollectionPage` + `Dataset` + `BreadcrumbList` JSON-LD rendered into the server HTML.

```bash
curl -s https://dex.trover.tech/nfts/robinhood/the-artificial-inus | grep -o '<title>[^<]*</title>'
curl -s https://dex.trover.tech/nfts/robinhood/the-artificial-inus | grep -c 'application/ld+json'
curl -s https://trover.tech/sitemap-nfts.xml | head -20
curl -s https://trover.tech/robots.txt
```

Expect a collection-specific `<title>`, one JSON-LD block, a sitemap listing collection URLs on the dex host, and robots.txt naming **both** sitemaps.

Then submit `https://trover.tech/sitemap-nfts.xml` in Google Search Console — nothing here makes Google crawl it on its own.


---

# 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-04-traders-and-rpc.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.
