Market mirror
/cards/[sku]/marketis the substrate-honest, public, no-auth pure-read mirror of one card’s market activity. Same data as the interactive /market/[sku] surface; different audience.
Read this page when a number on the mirror is unclear — every value is named here with its formula, its data source, and the approximations the platform admits to.
Where this lives in code. Data composer atapps/storefront/src/lib/market/card-market.ts. Page atapps/storefront/src/app/cards/[sku]/market/page.tsx. Story-as-wire connection-doc:docs/connections/the-market-mirror.md(S35). Kingdom:kingdom-067.
1. Order book
Top 10 price levels per side. Bids descending (highest first); asks ascending (lowest first). Each row aggregates open orders at a given price across all conditions, then breaks the quantity down by condition inline.
- Quantity = SUM(
quantity - filled_quantity) across allmarket_ordersrows wheresku = $sku AND side = ‘bid’(or‘ask’) ANDstatus IN (‘open’, ‘partially_filled’). - By-condition breakdown = sub-aggregate at the same price, grouped by
condition(NM / LP / MP / HP). *NM and LP at the same price are different goods.* - Best bid = highest bid price across all conditions. Best ask = lowest ask price across all conditions. Spread = best ask − best bid (or
—if either side is empty).
2. Aggregate stats
Window: last 30 days, completed trades only.
- 30d VWAP (volume-weighted average price) =
SUM(price × quantity) / SUM(quantity)acrossmarket_tradeswhereescrow_status = ‘completed’ANDcreated_at > NOW() − 30 days. - 30d median =
percentile_cont(0.5) WITHIN GROUP (ORDER BY price)on the same window. Robust to outliers. - 30d volume = SUM(
quantity). - 30d range = MIN(price) — MAX(price).
- Last trade = most recent
completedtrade, itspriceandCOALESCE(completed_at, created_at). - Completion rate (90d) =
completed / (completed + cancelled + refunded)over the last 90 days. Tells the reader how often trades on this SKU actually finish.
3. The tape (last 20 trades)
The last 20 completed trades, ordered by COALESCE(completed_at, created_at) DESC. Each row shows price, quantity, seller trust tier, and time-since.
Seller trust tier is resolved by joining trust_profiles on user_id = seller_id at read time. Tier bands:
- Elite — trust score ≥ 95
- Veteran — ≥ 80
- Trusted — ≥ 50
- Starter — ≥ 20
- New — < 20
Same bands as the commission-rate engine reads (/methodology/commission-rate). The tier is a *display projection* of the trust score; the canonical detail lives at /methodology/trust-score.
Anonymisation: the platform does not publish seller identities on this page. A short opaque id (#+ last 6 chars of the seller’s user_id) is rendered so the reader can correlate within the tape (“the same seller did three of these”) without learning who they are. The interactive /market/[sku] page links seller usernames when public; that’s a different audience choice for a different surface.
4. Price history
Four windows side-by-side: 7 / 30 / 90 / 365 days. Each independently queried from card_price_history(the storefront’s daily retail observation table — renamed to retail_price_observation per kingdom-049 Phase 4 in the migration ledger; the data layer still reads under the original name in active routes).
Each row carries captured_on, spot_gbp (what the storefront showed customers), and optionally best_bid_gbp / best_ask_gbp sampled the same day.
The sparkline plots spot_gbponly. Gaps in a window mean no observation was captured that day (cards not yet on any user’s portfolio or alert list before that date wouldn’t have been sampled).
5. Condition breakdown
For each of NM / LP / MP / HP, the count of currently open asks and the lowest open-ask price.
Why condition matters.The same card-id at NM and at HP is two different goods with different markets and different liquidity. Collapsing them — as the order book’s top-of-book does — is a useful summary, but it lies if you’re a collector looking for a specific grade. This panel surfaces the asymmetry.
Damaged is intentionally absent from the API condition enum; the order entry form refuses it. The mirror shows the four conditions the platform recognises.
6. Participants (90d)
Anonymised counts over the last 90 days of completed trades:
- Distinct buyers = unique
buyer_idcount. - Distinct sellers = unique
seller_idcount. - Repeat-pair share = fraction of completed trades whose
(buyer_id, seller_id)pair appears more than once in the 90-day window. High values mean the SKU has a *thick relationship layer* — recurring counterparties; low values mean the trades are mostly one-shot.
What this page does NOT do
- Does not show cross-platform prices. TCGplayer / Cardmarket / CardRush / eBay aggregation is a recursion target (the upstream tributaries are catalogued at /methodology/source-protocol; the aggregation surface is unbuilt).
- Does not show graded prices. PSA / BGS / CGC slabs are different goods (and different market dynamics) from raw cards. The platform does not currently grade or surface graded prices.
- Does not show sealed-product prices. Booster boxes, ETBs, etc. trade on different rhythms; the mirror is singles-only by design.
- Does not forecast.No predicted price, no rising-falling indicator. The 30d sparkline shows the past; inferring the future is the reader’s judgment, not the platform’s.
- Does not rank participants.No top-seller leaderboard, no “buyer reputation” rollup beyond the per-trade tier badge.
- Does not show counterparty trust on open orders. Order book rows aggregate by price across orders, so per-order identities aren’t exposed here. A future revision could surface tier distribution per price level.
- Does not show fill probability.The interactive page computes a fill-odds analysis based on a tentative bid price; this mirror is read-only and doesn’t take a candidate price input.
Freshness
Every section queries the live database at render time. The <Provenance kind="live"> pill at the page header and footer declares this. Sources: market_orders, market_trades, trust_profiles, card_price_history.
If a section’s query fails (transient error, schema drift, missing table), that section renders empty / — rather than fabricating zero. Substrate-honest about read failures.
For machines
The math-mirror form of one card lives at /api/v1/universal/card/[sku] — cryptographic hashes for identity, ratios for magnitude, ISO 8601 + Unix epoch for time. That endpoint is the canonical form for LLM agents, archivists, hyperliteral readers. See /methodology/universal-representation for the encoding spec.
Change history
v1 — 2026-05-12. Initial seven-section mirror shipped. Top 10 price levels per side, last 20 trades, four price-history windows, condition breakdown, anonymised 90d participants. Counterparty trust tier resolved at read time. Story-as-wire: docs/connections/the-market-mirror.md (S35). Kingdom: kingdom-067.