Add pricing schema, free API integrations (MTG bulk, TCGdex, Foolio), valuation and sort-by-price, card detail pricing UI, and nearby store discovery via home zip and OSM geo clients. Co-authored-by: Cursor <cursoragent@cursor.com>
13 KiB
| name | classification | success_metric | skip | status | created | branch | design_direction | model_policy | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pricing-online-local | feature | Users see honest online prices (MTG/Pokémon/Lorcana) with sync timestamps, can sort collections by price with correct foil-aware totals, and authenticated users with a saved zip see nearby game stores — all on $0 APIs. | implementers-complete | 2026-09-08 | convoy/pricing-online-local |
|
|
Convoy: Pricing — Online + Local Near Me (Free APIs Only)
Branch: convoy/pricing-online-local
Constraint: $0 recurring — no Google Places, no paid pricing tiers, no scraping.
Goal: Wire honest online pricing + gentle local store discovery.
Why
Collection valuation, card detail, and purchase flows today show incomplete or wrong prices: Pokémon/Lorcana sync writes null prices, MTG totals use foil market_price for nonfoil rows, sort-by-price UI is inert, and Price History / Purchase tabs are stubs. Users need trustworthy online estimates and optional local store discovery without paid APIs.
Scope
In: Scryfall (MTG), TCGdex (Pokémon), Foilio (Lorcana) ingest; foil-aware valuation; sort-by-price; pricing + history APIs; Purchase / Price History tabs; home zip + nearby stores (Nominatim + Overpass).
Out: Paid aggregators, scraping, in-store inventory/prices, buylist data, affiliate links.
Roles invoked
- Conductor ✓
- IA Architect ✓
- UI Designer ✓ (incremental — skipped ui-ux-pro-max)
- UX Reviewer ✓
- Security Auditor ✓ (planning phase)
- Architect ✓ → Human gate 1 ← you are here
- Implementers (13 briefs,
/multitaskwhere safe) - Audit fan-out (reviewer + security + design-system + a11y)
Todos
- Planning roles (IA, UX, UI, Security, Architect)
- Human gate 1 — plan approval
- Phase 0 implementers (Briefs 1–7)
- Phase 1 implementers (Briefs 8–9)
- Phase 2 implementers (Briefs 10–13)
- Audit fan-out + merge
Free-only stack (recommended)
Online pricing by game
| Game | Source | Cost | Key? | Notes |
|---|---|---|---|---|
| MTG | Scryfall bulk + card API | Free | No | Already integrated. prices.usd / usd_foil. Polite rate limits (~10 req/s). |
| Pokemon | TCGdex REST | Free | No | Catalog + embedded pricing.tcgplayer (USD) + pricing.cardmarket (EUR). Hourly/daily updates. |
| Lorcana | Foilio feed GET /api/prices/feed |
Free | No | JSON, CORS, cached 1h. EUR/USD as reported; match on card_id / name+set. |
| Fallback links | TCGPlayer + eBay search URLs | Free | No | Already on card detail — keep for buy-outbound. |
Optional signup (still $0):
- TCG Price Lookup — 10,000 req/month free with API key (multi-game). Good for on-demand card detail if TCGdex/Foilio miss a card.
Do NOT use for this convoy:
- tcgapi.dev beyond 100/day (too tight for bulk sync)
- Google Places (paid after credits)
- Scraping TCGPlayer / Cardmarket pages
Local “near me” (free)
| Step | Source | Cost |
|---|---|---|
| Zip → lat/lng | Nominatim (OSM) | Free — 1 req/s max, cache forever per zip |
| Stores in radius | Overpass API shop=games |
Free — cache 7 days per (lat,lng,radius) |
Show: name, address, distance, website / phone tags, OpenStreetMap link. No scraped in-store prices.
Decisions (planning round)
| # | Question | Decision |
|---|---|---|
| A | Nearby stores auth | Auth required; zip from saved home_zip only — no anonymous geocode relay (Security sev-3) |
| B | Local stores placement | Subsection in Purchase tab, not a fourth tab |
| C | Zip input | Manual zip in Preferences; geolocation nice-to-have later |
| D | Online pricing visibility | Public (matches existing /api/cards/[id]) |
| E | Price History empty state | Show tab with honest empty copy until snapshots accumulate |
| F | Collection sort | Server-side sort=price on paginated APIs |
| G | Settings placement | Home zip under Preferences panel |
| H | Currency v1 | USD primary; respect preferred_currency where data exists |
Design direction
Incremental extension of Liquid Glass — no new token files. Prices use .gradient-text-gold / var(--accent-flame); staleness uses neutral tertiary pills (not error red); glass on section containers only (never per-card grid). See full typography/spacing/badge spec in planning transcript; repo tokens win on conflict.
Surfaces: Purchase tab price summary, Price History chart panel, staleness badge, collection sort control, settings zip field, nearby stores list.
IA
Affected routes
Pages: /card/[id] [modified], /settings [modified], /my-cards [modified], /collection/[identifier] [modified], /collections [impacted], /profile [impacted], /cards [impacted]
API: GET /api/cards/[id]/pricing [new], GET /api/cards/[id]/price-history [new], GET /api/stores/nearby [new], GET|PUT /api/user/settings [modified], GET /api/cards/owned [modified], bulk import jobs [impacted]
User flow
flowchart LR
A[Settings: save zip] --> B[Card Detail]
B --> C[Purchase tab]
C --> D[Online NM price]
C --> E[Buy links]
C --> F[Stores near you]
G[My Collection] --> H[Sort by Price]
Screen inventory
| Screen | Path | New/modified | Notes |
|---|---|---|---|
| Card Detail — Purchase | /card/[id] |
Modified | DB prices, staleness badge, outbound links, local stores subsection |
| Card Detail — Price History | /card/[id] |
Modified | Snapshot chart from sync-stored history |
| Settings — zip | /settings |
Modified | Home ZIP in Preferences |
| My Collection | /my-cards |
Modified | Sort by price + corrected estimated value |
| List Detail | /collection/[identifier] |
Modified | Wire existing sort dropdown |
UX
Reuse: CardDetailView, useCardDetail, CollectionPageView, useCollectionView, StatCard, GlassSurface, Input, Button, settings page patterns.
Key patterns: DB-first pricing (no optimistic price UI); staleness badge with text date (not color-only); server-side sort; zip saves with explicit Settings Save button; local stores below purchase links.
A11y handoff: Tab roles on card detail tabs; aria-live for pricing load; zip validation via Input error; 4.5:1 contrast on badge text; link purpose text on all outbound URLs.
Anti-patterns: No “live” pricing copy; no per-card glass; no client-only sort across pages; no auto GPS on load.
Security (planning)
| Check | Status | Notes |
|---|---|---|
| Auth boundary | ⚠️ | Pricing public OK; nearby stores auth required |
| Authorization (IDOR) | ⚠️ | Zip settings owner-scoped; no arbitrary zip query |
| Input / injection | ⚠️ | Overpass fixed-template only; validate zip + radius |
| Secrets exposure | ✅ | Core path keyless; server-only optional key |
| Dependencies | ⚠️ | Validate upstream JSON; native fetch preferred |
Top pre-merge fixes: Overpass QL injection (sev 4), geocode relay without auth (sev 3), bulk sync admin gate (sev 3).
Re-audit with file:line citations after implementer PR.
Architecture
Decomposition
| Brief # | Title | Phase | Depends on | Est. PR |
|---|---|---|---|---|
| 1 | Pricing schema migration | 0 | — | ~120 LOC |
| 2 | Pricing foundation lib | 0 | 1 | ~280 LOC |
| 3 | MTG bulk sync + snapshots | 0 | 2 | ~180 LOC |
| 4 | TCGdex Pokémon pricing | 0 | 2 | ~320 LOC |
| 5 | Foilio Lorcana pricing | 0 | 2 | ~280 LOC |
| 6 | Foil-aware valuation APIs | 0 | 2 | ~150 LOC |
| 7 | Sort by price wiring | 0 | 2, 6 | ~220 LOC |
| 8 | Card pricing + history APIs | 1 | 2 | ~350 LOC |
| 9 | Card detail pricing UI | 1 | 8 | ~380 LOC |
| 10 | Geo clients + DB cache | 2 | 1 | ~300 LOC |
| 11 | Stores nearby API | 2 | 10 | ~200 LOC |
| 12 | Settings home zip | 2 | 1 | ~120 LOC |
| 13 | Local stores card detail UI | 2 | 9, 11, 12 | ~200 LOC |
Brief files: .convoys/pricing-online-local/brief-*.md
Schema diff (migration 1783000000001_add-pricing-schema.js)
cards.prices_updated_at TIMESTAMPTZcard_price_history(daily snapshots, UNIQUE per card/day/source/type/currency)users.home_zip VARCHAR(10)geo_zip_cache,geo_stores_cache(DB-backed, 7d TTL on stores)
slice_dependencies (multitask-ready)
slice_dependencies:
- brief: 1
depends_on: []
files: [migrations/1783000000001_add-pricing-schema.js, docs/SCHEMA_MAP.md]
- brief: 2
depends_on: [1]
files: [lib/pricing/card-price.js, lib/pricing/price-history.js, lib/pricing/collection-valuation.js, lib/pricing/outbound-links.js]
- brief: 3
depends_on: [2]
files: [lib/card-import/bulk-sync.js]
- brief: 4
depends_on: [2]
files: [lib/pricing/tcgdex-client.js, lib/card-import/pokemon-bulk.js]
- brief: 5
depends_on: [2]
files: [lib/pricing/foilio-client.js, lib/card-import/lorcana-bulk.js]
- brief: 6
depends_on: [2]
files: [pages/api/user/stats.js, pages/api/collections/[identifier].js]
- brief: 7
depends_on: [2, 6]
files: [pages/api/cards/owned.js, pages/api/collections/[identifier]/cards.js, lib/use-collection-view.js, components/CollectionPageView.js, pages/my-cards.js]
- brief: 8
depends_on: [2]
files: [lib/pricing/pricing-service.js, pages/api/cards/[id]/pricing.js, pages/api/cards/[id]/price-history.js]
- brief: 9
depends_on: [8]
files: [components/card-detail/CardDetailPurchaseTab.js, components/card-detail/CardDetailPriceHistoryTab.js, components/CardDetailView.js, lib/use-card-detail.js]
- brief: 10
depends_on: [1]
files: [lib/pricing/nominatim.js, lib/pricing/overpass.js, lib/pricing/geo-cache.js]
- brief: 11
depends_on: [10]
files: [pages/api/stores/nearby.js, lib/rate-limit.js]
- brief: 12
depends_on: [1]
files: [pages/settings.js, pages/api/user/settings.js]
- brief: 13
depends_on: [9, 11, 12]
files: [components/card-detail/CardDetailLocalStores.js, components/CardDetailView.js, lib/use-card-detail.js]
Parallelization: After Brief 1 → Brief 2 → parallel Briefs 3, 4, 5, 6 → Brief 7 → Brief 8 → Brief 9 → parallel Briefs 10, 12 → Brief 11 → Brief 13.
Risks
| Risk | Mitigation |
|---|---|
| TCGdex ID mismatch | Match set_code + card_number; log unmatched |
| Foilio card_id drift | Fallback name+set match |
| Nominatim ToS | DB cache forever; 1 req/s; server-only |
| Overpass load | 7d cache; auth + 1/hour/user |
| Valuation on lists (no is_foil) | COALESCE(current_price, market_price) + tooltip |
Phase checklist
Phase 0
- Pokemon bulk → TCGdex prices
- Lorcana bulk → Foilio feed
- Fix MTG foil valuation bug
- Sort by Price wired
- Price snapshots on sync
Phase 1
GET /api/cards/[id]/pricing- Purchase tab + staleness badge
- Price History tab
Phase 2
- Settings home zip
GET /api/stores/nearby(auth-gated)- Local stores on card detail
Env vars
TCGDEX_BASE_URL=https://api.tcgdex.net/v2/en
FOILIO_PRICES_FEED_URL=https://foilio.net/api/prices/feed?game=lorcana
NOMINATIM_USER_AGENT=DeckHearth/1.0 (contact@stillwell.cloud)
OVERPASS_URL=https://overpass-api.de/api/interpreter
TCG_PRICE_LOOKUP_API_KEY= # optional
PRICING_LOCAL_ENABLED=true
Gentleness rules
| API | Rule |
|---|---|
| Scryfall bulk | One download per sync; User-Agent set |
| TCGdex | Batch by set; sleep 100ms between pages |
| Foilio | One feed fetch/hour |
| Nominatim | Max 1 req/s; cache zip→coords forever |
| Overpass | Max 1 query/user/hour; 7d cache |
Architecture complete. 13 briefs created. Estimated PRs: 13. Awaiting human gate 1 (plan approval) before implementers run.