deckhearth/.convoys/pricing-online-local.md
Randall Stillwell d0468015eb
Some checks failed
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
Visual diff / Should run? (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
Visual diff / Screenshot diff (pull_request) Has been cancelled
feat(pricing): convoy for online prices and local stores
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>
2026-09-08 22:47:46 -05:00

13 KiB
Raw Blame History

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
version locked_at extension parent_system product_type pattern style stack generator generator_reason
1 2026-09-08 incremental liquid-glass consumer TCG collection + pricing utility authenticated app — card detail tabs + collection list + settings form Liquid Glass — warm ember accent on glass; honesty-first pricing metadata nextjs-pages-router | react-18 | tailwind | css-vars skipped Established Liquid Glass DS; incremental pricing surfaces only
default_session roles
auto
role-conductor role-architect role-ia-architect role-ux-reviewer role-ui-designer role-implementer role-reviewer role-security-auditor
composer-2.5-fast composer-2.5 composer-2.5-fast composer-2.5-fast composer-2.5-fast composer-2.5-fast cursor-grok-4.5-high gpt-5.6-terra-medium

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

  1. Conductor ✓
  2. IA Architect ✓
  3. UI Designer ✓ (incremental — skipped ui-ux-pro-max)
  4. UX Reviewer ✓
  5. Security Auditor ✓ (planning phase)
  6. Architect ✓ → Human gate 1 ← you are here
  7. Implementers (13 briefs, /multitask where safe)
  8. 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

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 TIMESTAMPTZ
  • card_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.