Appearance
Collection, pricing, and alerts backend plan
Date: 2026-05-18. Status: product/backend architecture plan before pricing-provider implementation.
SKN must become more than a scanner. The scanner identifies the object; the collection, pricing, and alert engines turn that object into a private portfolio that helps the user decide when to buy, hold, sell, or list.
This plan deliberately separates the backend foundation from provider-specific pricing formulas. The exact weighting model should wait for the dedicated pricing documentation. The architecture below defines where that model will run and what data it needs.
Product Thesis
The core user loop is:
text
scan or search item
-> confirm identity, condition, language, grade, quantity
-> add to private collection
-> attach acquisition cost and proof/photos
-> compute valuation and confidence
-> track movements over time
-> notify user on meaningful pump/drop/opportunityThe app should feel closer to a portfolio terminal than to a static binder. But it must not pretend to be a financial market if the underlying data is weak. Every valuation and alert needs freshness, confidence, and source explanation.
Scope Boundary
In Scope
- private user collections;
- raw cards, slabs, and sealed products as separate item classes;
- acquisition cost and portfolio gain/loss;
- market observations from normalized providers;
- weighted price estimates with confidence;
- valuation snapshots per holding and portfolio;
- alert rules and alert events for drops, pumps, and watched items;
- provider adapters behind normalized contracts;
- audit trail for sensitive user collection changes.
Out of Scope for Now
- marketplace checkout, payment, payout, escrow, disputes;
- public collection sharing by default;
- automatic buy/sell execution;
- unreviewed scraping as a production dependency;
- public redistribution of images or market data that providers do not allow;
- promising real-time alerts when provider quotas only support polling.
Backend Domains
1. User and Collection Domain
The Collection Engine answers: "What does this user own?"
Core tables:
| Table | Purpose |
|---|---|
users | Account identity. Can start simple, but holdings must be user-scoped from day one. |
collection_items | One owned holding line: item reference, type, quantity, condition, language, grade, sealed state. |
collection_item_photos | User-owned proof photos, display photos, and scan captures. |
collection_item_acquisitions | Purchase price, purchase date, seller/source, shipping, fees, notes. |
collection_item_events | Audit trail: created, edited, quantity changed, sold, deleted, restored. |
collection_item_valuation_snapshots | Latest and historical value for a holding. |
collection_exports | Explicit export jobs for user-owned data. |
Privacy requirements:
- collection data is private by default;
- every query is scoped by user ID;
- location/storage labels are never public by default;
- acquisition cost is private and must not leak into marketplace listings later;
- EXIF should be stripped from uploaded photos;
- deletes should be soft-delete first, with restore/audit.
Remote backend should be canonical for a professional app. Mobile can cache locally later, but the backend should own identity, audit, valuation, alerts, and provider refresh state.
2. Item Identity Domain
The Pricing and Collection Engines must not store raw provider shapes directly. They reference normalized SKN identities.
Core identities:
| Item class | Identity fields |
|---|---|
raw_card | TCG, set, collector number, language, variant, finish, condition. |
graded_card / slab | inner card identity, grader, grade, cert number, label metadata. |
sealed_product | product type, set/series, language, region, SKU/barcode when available. |
The Identity Engine remains responsible for resolving a scan into one of these classes. The Collection Engine stores the user's owned instance of that identity.
3. Pricing Observation Domain
The Pricing Engine answers: "What market evidence exists?"
Core tables:
| Table | Purpose |
|---|---|
price_sources | Provider registry: Cardmarket, eBay, TCGPlayer, PriceCharting, provider APIs, manual imports. |
price_provider_items | Mapping between SKN item identity and provider SKU/listing/category IDs. |
price_observations | Raw normalized market observation: sold comp, active listing, asking price, graded comp, sealed comp. |
price_observation_fees | Shipping, buyer fee, seller fee, taxes when known. |
price_observation_flags | Outlier, low-confidence, wrong-language, damaged, incomplete, suspicious. |
price_import_runs | Ingestion job tracking, provider freshness, errors, quotas. |
Every observation should keep:
- source and source URL or external ID;
- sold vs active listing;
- price and currency;
- shipping/fees treatment;
- sale/listing timestamp;
- region;
- language;
- condition;
- item class;
- grader and grade when relevant;
- raw payload pointer or checksum for audit;
- terms/licensing status for cache/display.
Provider-specific adapters can be written later, but the observation schema should exist before many providers are added.
4. Pricing Estimate Domain
The Pricing Engine answers: "What is the current estimated value?"
Core tables:
| Table | Purpose |
|---|---|
price_estimates | Current estimate for one SKN item identity and market context. |
price_estimate_components | Breakdown by source/signal: Cardmarket, eBay sold, active listings, graded source, manual override. |
price_estimate_history | Time series of estimates for charting and alert comparisons. |
price_estimate_explanations | Human-readable explanation payload for the app. |
Estimate output should include:
- estimated low / mid / high;
- currency;
- confidence score;
- liquidity score;
- sample size;
- freshness;
- strongest sources;
- warnings: stale data, low comps, language mismatch, grade mismatch, outlier-heavy data.
Do not start with a "magic average". The first version can be simple but must be explainable:
- sold comps weigh more than active listings;
- recent observations weigh more than old observations;
- EU/FR signals weigh more for FR/EU users than US-only data;
- exact language/condition/grade matches weigh more than mismatches;
- active listings can inform ask pressure but should not dominate sold value;
- low sample size reduces confidence even when prices look high.
5. Valuation Domain
The Valuation Engine applies price estimates to the user's actual holdings.
It answers:
- total portfolio value;
- value by item class: raw, slab, sealed;
- unrealized gain/loss vs acquisition cost;
- top movers;
- stale valuation count;
- low-confidence valuation count.
Core tables:
| Table | Purpose |
|---|---|
portfolio_snapshots | Aggregated portfolio value per user over time. |
collection_item_valuation_snapshots | Holding-level estimated value over time. |
portfolio_breakdowns | Split by item class, TCG, set, condition, grade, language. |
This layer should not recompute provider data live on every mobile screen. It should read precomputed estimates and snapshots, then show freshness.
6. Alert Domain
The Alert Engine answers: "What changed enough to matter?"
Alert types:
| Alert | Trigger |
|---|---|
| Price drop | Watched item or wishlist item drops below threshold. |
| Price pump | Owned item rises above movement threshold. |
| Portfolio mover | A holding becomes a top daily/weekly mover. |
| Spread opportunity | Sold comps and active listings create a possible buy/sell spread. |
| Stale data | Important holding has no fresh market data. |
| Missing pricing | User owns an item that has no estimate yet. |
Core tables:
| Table | Purpose |
|---|---|
alert_rules | User-defined or system-suggested rules. |
alert_rule_targets | Item, holding, search query, set, product class, or portfolio segment. |
alert_events | Concrete fired alerts with payload and source estimate IDs. |
alert_deliveries | Push/email/in-app delivery state and retry info. |
alert_deduplication_keys | Cooldowns to avoid spam on noisy price feeds. |
Near-real-time should mean "fresh enough for marketplace behavior", not fake tick-by-tick trading. Provider permissions and quotas decide whether this is polling, webhook, batch refresh, or a hybrid. The backend should expose freshness honestly.
Price Refresh Cadence
Pricing freshness must be tiered. Pokémon has too many cards, variants, slabs, and sealed products to refresh everything continuously.
Current source constraints:
- Cardmarket's public price guide is documented as updated once daily, while the product catalogue updates when new releases are added. This makes Cardmarket a strong daily baseline, not a tick-by-tick source. Source: Cardmarket announcement, 2024-06-05.
- eBay Browse APIs can search active listings and support discovery/refinement use cases, but sold-history access is constrained. eBay documents many Buy APIs as limited release and notes that Marketplace Insights is restricted and not open to new users at this time. eBay's Feed API can provide hourly changed-item snapshots, but it is also limited release. Sources: eBay Buy APIs overview, Buy API support by marketplace, Feed API resources.
Recommended refresh tiers:
| Tier | Cadence | Scope | Purpose |
|---|---|---|---|
| Full market baseline | Daily | Cardmarket price guide, provider daily feeds, broad catalogue | Recompute stable estimates and long-tail prices. |
| User portfolio | Every 1 to 6 hours | Items owned by users, high-value holdings, recently scanned items | Keep the portfolio feeling alive without refreshing the entire market. |
| Watchlist / active alerts | Every 30 to 60 minutes when provider budget allows | Watched items, wishlist targets, items with active alert rules | Catch user-relevant movement faster than the daily baseline. |
| Hot queue | Every 5 to 30 minutes when legally and technically allowed | Items that recently moved sharply or have many users watching them | Support near-real-time "pump/drop" behavior without pretending every item is live. |
| Long tail | Daily or slower | Items with no owners, no watches, low liquidity | Avoid wasting provider quota. |
The app must display freshness next to estimates and alerts. A useful alert with "updated 22 minutes ago" is credible; a stale alert without freshness creates false confidence.
Alert Evaluation Model
Alerts should fire from estimate changes, not from raw provider observations alone.
Evaluation flow:
text
new observations ingested
-> estimate recalculated
-> estimate history updated
-> alert evaluator compares against baselines
-> confidence and liquidity gates applied
-> dedupe/cooldown checked
-> in-app/push alert createdComparison baselines:
- last reliable estimate;
- 7-day average;
- 30-day average;
- user's acquisition price when the item is owned;
- user-defined threshold for watchlist or wishlist items.
Default alert thresholds:
| Alert | Suggested default |
|---|---|
| Price drop | -10% to -15% versus 7-day average, or below a user threshold. |
| Price pump | +15% to +25% versus 7-day average, with enough liquidity. |
| Portfolio mover | Holding enters the user's top daily/weekly movers. |
| Spread opportunity | Active listing price materially below recent sold/estimated value. |
Quality gates before notifying:
- minimum observation count or strong trusted source;
- source freshness inside the alert tier's SLA;
- estimate confidence above the alert rule minimum;
- movement above both percentage and absolute-value noise floors;
- condition/language/grade match is acceptable;
- suspicious outliers do not dominate the estimate;
- no duplicate alert inside the cooldown window.
Example alert payload:
json
{
"type": "price_pump",
"title": "Charizard 4/102 FR is up 18%",
"itemId": "raw_card:pokemon:base1:4:fr",
"estimate": { "mid": 182.0, "currency": "EUR", "confidence": 0.76 },
"movement": { "window": "7d", "percent": 18.0, "absolute": 27.8 },
"primarySignals": ["ebay_sold", "cardmarket_daily"],
"freshness": "2h",
"action": "review_sell_or_list"
}Product wording should help decisions without implying financial advice. Prefer "review sell/list opportunity" over "sell now".
Provider and MCP Strategy
Provider integrations should be runtime backend adapters, not mobile dependencies.
Candidate provider families:
- Cardmarket for EU market context;
- eBay sold comps and active listings;
- TCGPlayer or provider APIs for US context;
- PriceCharting or graded providers for slabs;
- later Vinted / Leboncoin as French peer-to-peer signals if terms allow.
MCP connectors are useful for internal tooling, operations, backfills, and provider exploration. For product runtime, SKN should still store normalized observations in its backend database and serve the app through SKN APIs. The mobile app should never depend directly on provider-specific APIs or MCP tool shapes.
Before production use, every provider needs a terms review:
- cache permission;
- display permission;
- refresh quota;
- commercial use;
- attribution;
- rate limits;
- redistribution constraints;
- whether derived estimates can be stored and shown.
API Surface
Initial backend endpoints:
| Endpoint | Purpose |
|---|---|
POST /collection/items | Add holding from scan or manual search. |
GET /collection/items | List user holdings with filters. |
PATCH /collection/items/:id | Update condition, quantity, notes, storage, acquisition. |
DELETE /collection/items/:id | Soft-delete holding. |
GET /collection/portfolio | Portfolio totals and breakdowns. |
GET /pricing/items/:identityId/estimate | Current normalized estimate and explanation. |
GET /pricing/items/:identityId/observations | Debug/provider evidence when user is allowed to see it. |
POST /alerts/rules | Create alert rule. |
GET /alerts/events | In-app alert feed. |
PATCH /alerts/events/:id | Mark read, dismissed, acted-on. |
Scan integration:
text
scan result
-> confirmation screen
-> add to collection
-> collection item created
-> valuation snapshot attached if estimate exists
-> optional alert rules suggestedImplementation Roadmap
Phase 0 — Architecture Lock
Deliverables:
- schema ADR for user holdings, price observations, estimates, and alerts;
- provider terms checklist;
- item identity contract shared by collection/pricing/scan;
- decision on auth model for private holdings.
Phase 1 — Private Collection Foundation
Deliverables:
- user-scoped collection tables;
- add/edit/delete/list holdings;
- acquisition cost and date;
- item class support for
raw_card,sealed_product, and futuregraded_card; - audit trail;
- local tests around user isolation and soft-delete.
Phase 2 — Add From Scan
Deliverables:
- backend endpoint to create a holding from scan result;
- mobile confirmation flow;
- manual fallback when scan result is wrong or ambiguous;
- capture condition, language, quantity, acquisition price, notes.
Phase 3 — Pricing Schema With Mock Provider
Deliverables:
price_sources,price_observations,price_estimates;- synthetic/mock provider ingestion for tests;
- first explainable estimator using deterministic rules;
- no live provider dependency yet.
This phase proves the data model before Cardmarket/eBay complexity.
Phase 4 — Portfolio Valuation
Deliverables:
- valuation snapshots per holding;
- portfolio totals;
- gain/loss vs acquisition;
- breakdown by raw/slab/sealed;
- stale/low-confidence labels.
Phase 5 — Alerts With Simulated Market Events
Deliverables:
- alert rules;
- pump/drop detection over estimate history;
- dedupe and cooldown;
- in-app alert feed;
- tests using synthetic price movements.
This validates the product loop before real provider quotas are involved.
Phase 6 — First Real Provider Adapter
Deliverables:
- one provider adapter behind
PriceObservationSource; - import-run observability;
- terms-compliant cache/display behavior;
- provider mapping table;
- source-specific confidence flags.
Choose the first provider only after the dedicated pricing documentation and terms review. Cardmarket/eBay are strong candidates, but the first production adapter should be the source with the best legal, quota, and data-quality fit.
Phase 7 — Near-Real-Time Alerts
Deliverables:
- scheduled refresh tiers by item importance;
- high-priority watchlist refresh;
- event generation after estimate updates;
- push notification integration;
- alert explanations that show source, movement, confidence, and freshness.
First Development Slice Recommendation
Start with Phase 1: private collection foundation.
Why:
- it is required for every later valuation and alert feature;
- it is testable without provider risk;
- it creates immediate product surface after scan;
- it keeps user assets private from the beginning;
- it avoids building pricing logic before there is a place to attach valuations.
The first backend PR should not integrate Cardmarket or eBay yet. It should create user-scoped holdings and enough API shape for "scan -> confirm -> add to collection".
Open Decisions
- What auth model is acceptable for the first private collection backend?
- Should collection photos be stored on the VPS filesystem first, object storage later, or object storage from day one?
- Should the first pricing provider target EU/Cardmarket relevance or broader eBay sold comps?
- What is the minimum alert latency that feels valuable: minutes, hourly, daily?
- What wording avoids implying financial advice while still giving strong buy/sell decision support?
- Which item class should get pricing first: raw cards, sealed products, or slabs?