Goal
Surface a composite Opportunity Score and Fair Value Range per card per PSA Grade — a data-backed answer to "is this listing a deal?" — so buying decisions are driven by signal rather than intuition.
Design
Fair Value Range
A (low, mid, high) triplet per Product per PSA Grade, computed independently per grade. Null for grades with insufficient sold history (no fabrication).
Base Range (from #84):
mid = median of eBay sold comps within the Sold Comp Window
low = mid − 1 stddev
high = mid + 1 stddev
Sold Comp Window (adaptive):
- 30 days if sufficient comps exist, expands to 90 days otherwise
- Exact thresholds require data calibration — not hardcoded
Opportunity Score
A numeric score (0–100) per Product per PSA Grade combining all signals. Higher = stronger opportunity.
Structure — hierarchical:
- Price dislocation vs Fair Value Range is the gate (required for any non-zero score)
- The 7 supporting signals determine confidence and amplify/dampen the score
| Signal |
Source |
Role |
| eBay sold prices |
#84 |
Base Range (required — primary signal) |
| Relative Pop Velocity |
#86 |
Supply pressure relative to same-era cards; high velocity compresses score |
| Card Popularity Score |
#99 |
Composite: Pokémon popularity (#89) + multi-Pokémon bonus (#97) + premium symbols (#98) |
| 52-week high/low |
#92 |
Buying near 52-week low amplifies opportunity |
| Listing Depth |
#93 |
Shallow depth discounts the Market Price floor |
| PSA grade price spread |
#94 |
Cross-grade coherence; validates range relative to adjacent grades |
| Listing Staleness |
#95 |
Stale listings discounted — not real market willingness to transact |
| Grade Liquidity Share |
#96 |
Illiquid grades discounted — a deal you can't exit isn't an opportunity |
Model iteration:
Signal weights are not hardcoded. The infrastructure stores each signal's value per sync with full history, enabling retrospective analysis (score at T vs price at T+90d) once sufficient data accumulates. Weights are calibrated empirically.
Schema
Two separate tables, full history (rows accumulate, never overwritten):
FairValueRange
productId String
grade Int (1–10)
low Float?
mid Float?
high Float?
sampleCount Int
windowDays Int (30 or 90 — actual window used)
computedAt DateTime
OpportunityScore
productId String
grade Int
score Float (0–100)
priceVsRange Float (% below low — the raw dislocation)
popVelocitySignal Float?
popularitySignal Float?
weekHighLowSignal Float?
listingDepthSignal Float?
gradeSpreadSignal Float?
stalenessSignal Float?
liquiditySignal Float?
computedAt DateTime
Computation
A dedicated FairValueSyncService runs nightly (or triggered after #84's eBay Sync). Pure computation over stored data — no external API calls. Reads eBay sold history and signal data already in the database, writes to the two tables above.
UI surfaces
- Main table column — highest-scoring PSA Grade's Opportunity Score, sortable
- Side panel — full per-grade breakdown: Fair Value Range + Opportunity Score + per-signal values
- Opportunities page — dedicated page showing top buying opportunities across all tracked Products for the day; at most N cards scoring above a minimum floor (both tunable); ranked by score descending; empty page on a slow day is intentional
Each opportunity card shows:
- Card image
- Card name + Product Set
- Highest-scoring PSA Grade + score
- Current Market Price vs Fair Value Range (e.g.
€170 vs €220–280)
- Dominant signal driving the score
Roadmap
Phase 1 — Data foundation 🔴
| Issue |
What |
| #84 |
eBay sold listings — gates everything |
Phase 2 — Fair value core 🔴
Ships together once #84 data exists. #92–#95 reuse existing data or scraping infrastructure and are fast to build.
| Issue |
What |
| #91 (this issue) |
Schema, FairValueSyncService, Base Range computation, all UI surfaces |
| #92 |
52-week high / low — derivable from existing price history |
| #93 |
CardMarket listing depth — same scraping pass as current source |
| #94 |
PSA grade price spread — derived from existing PSA Grade Prices |
| #95 |
CardMarket listing staleness — needs HTML verification first |
Phase 3 — Signal enrichment 🟡
Ship after Phase 2, once #84 data has accumulated enough to calibrate thresholds.
| Issue |
What |
| #96 |
Grade Liquidity Share — per-grade eBay sold volume distribution |
| #86 |
Relative Pop Velocity — requires PSA pop snapshot history (architectural change) |
Post-MVP — Popularity signal 🟢
Long dependency chain with manual data-entry work. Adds meaningful demand-side signal once in place.
| Issue |
What |
| #87 |
Pokémon entity (Pokédex seed) |
| #88 |
Pokémon ↔ card associations |
| #101 |
Research: external Pokémon popularity data sources |
| #89 |
Pokémon popularity score |
| #97 |
Multi-Pokémon card signal |
| #98 |
Premium card symbol signal |
| #99 |
Card popularity score (aggregator) |
| #100 |
Filter and browse products by Pokémon |
Blocked by
Value
Turns Gather from a price tracker into a buying-decision tool. The Opportunities page makes the signal actionable without requiring manual browsing.
Goal
Surface a composite Opportunity Score and Fair Value Range per card per PSA Grade — a data-backed answer to "is this listing a deal?" — so buying decisions are driven by signal rather than intuition.
Design
Fair Value Range
A
(low, mid, high)triplet per Product per PSA Grade, computed independently per grade. Null for grades with insufficient sold history (no fabrication).Base Range (from #84):
mid= median of eBay sold comps within the Sold Comp Windowlow= mid − 1 stddevhigh= mid + 1 stddevSold Comp Window (adaptive):
Opportunity Score
A numeric score (0–100) per Product per PSA Grade combining all signals. Higher = stronger opportunity.
Structure — hierarchical:
Model iteration:
Signal weights are not hardcoded. The infrastructure stores each signal's value per sync with full history, enabling retrospective analysis (score at T vs price at T+90d) once sufficient data accumulates. Weights are calibrated empirically.
Schema
Two separate tables, full history (rows accumulate, never overwritten):
Computation
A dedicated
FairValueSyncServiceruns nightly (or triggered after #84's eBay Sync). Pure computation over stored data — no external API calls. Reads eBay sold history and signal data already in the database, writes to the two tables above.UI surfaces
Each opportunity card shows:
€170 vs €220–280)Roadmap
Phase 1 — Data foundation 🔴
Phase 2 — Fair value core 🔴
Ships together once #84 data exists. #92–#95 reuse existing data or scraping infrastructure and are fast to build.
Phase 3 — Signal enrichment 🟡
Ship after Phase 2, once #84 data has accumulated enough to calibrate thresholds.
Post-MVP — Popularity signal 🟢
Long dependency chain with manual data-entry work. Adds meaningful demand-side signal once in place.
Blocked by
Value
Turns Gather from a price tracker into a buying-decision tool. The Opportunities page makes the signal actionable without requiring manual browsing.