Cross-source pricing
Cambridge TCG aggregates price signals from multiple upstream markets — CardRush (Japan), TCGplayer (US), and (planned) Cardmarket (Europe). Each source has its own currency, condition vocabulary, license tier, and update cadence. This page explains how those signals compose, which one is the “headline”, and what license boundary each source carries downstream.
Where this lives in code. The aggregation pipeline ispackages/data-ingest/src/<source>/(one module per upstream). The wholesale writer atapps/wholesale/src/lib/ingest/<source>.tspersists rows toprice_archivewith(card_id, snapshot_date, source, condition)uniqueness. The cross-source view endpoint is/api/v1/prices/[sku]/sourceson wholesale (bearer-gated). The auditpnpm --filter @cambridge-tcg/admin cross-source-divergenceflags outliers across sources for the same card+date.
The four properties every source declares
Every upstream module exports a typed SourceMeta object declaring four things that propagate to every byte that leaves the platform:
| Property | What it means | Where it surfaces |
|---|---|---|
access | How we reach the source (public-api / oauth2 / scrape / paid-feed / partner). | /api/v1/sources response. |
license | The redistribution tier. cc0, cc-by, cc-by-nc, mit, partner-redistributable, internal-only, proprietary. | _meta.source_license array on every response that touches that source's data. |
freshness | The platform's intent on staleness (catalog 24h, price_current 5min, price_historical immutable, market_signal 1min). | _meta.freshness_seconds on every response. |
redistribute | Whether we may re-export raw upstream values verbatim. true only for CC0 / CC-BY / MIT. | price_archive.source_redistribute per row; propagated through _meta downstream. |
The three sources today
| Source | Region | Currency | License tier | Cadence | Conditions captured |
|---|---|---|---|---|---|
| CardRush | Japan | JPY | internal-only | Daily snapshot | NM (status A-) |
| TCGplayer | US | USD | partner-redistributable | 5min during US trading; nightly bulk | NM (v1; LP/MP/HP/DMG planned) |
| Cardmarket | Europe | EUR | partner-redistributable | (planned — kingdom-NNN+1) | (planned) |
FX normalisation
Every price row carries its source's native amount (e.g. USD for TCGplayer) plus a GBP-normalised value computed at write time. The rate used is captured per-row in fx_rate_to_gbp with a fx_rate_source declaration (live / cached / fallback). This means the GBP value on a snapshot from three months ago reflects the rate that was current at that moment — not the rate as you read it.
Substrate honesty applied to FX: the row is the platform's view of what the source charged in GBP at the moment of capture. It is not marked up; we don't transform a TCGplayer market signal into a Cambridge retail price. Cross-source comparison happens at the substrate level.
The headline number per source
Most pricing APIs return multiple fields (low / mid / high / market / direct-low for TCGplayer; trend / 30d-avg / 7d-avg for Cardmarket). We pick one to be the “headline” — what we display prominently when surfacing the source's view. The rest ride in the extra JSONB column for downstream consumers that want the spread.
| Source | Headline field | Rationale |
|---|---|---|
| CardRush | A-condition retail JPY | One value per card; no spread offered upstream. |
| TCGplayer | marketPrice (USD) | What TCGplayer publishes as “Market Price”. Smoothed across recent sales; resistant to single-listing manipulation. When null, falls back to midPrice then lowPrice. |
| Cardmarket | trendPrice (EUR) | Cardmarket's published trend signal (planned). |
Divergence interpretation
When two sources price the same card on the same date and disagree meaningfully (max/min ratio > 1.5×), the platform preserves the disagreement rather than aggregating. There are two reasons sources can diverge:
- Genuine regional asymmetry. A JP-exclusive printing scarce on TCGplayer commands a premium in Japan; an English Lorcana set dwarfs CardRush's coverage. Different markets, different scarcities, different prices.
- Upstream anomaly. One source has stale data; an FX rate was applied wrong; a listing on one platform got mispriced. The audit at
pnpm --filter @cambridge-tcg/admin cross-source-divergenceflags outliers (> 5×) for operator review.
The platform's job is to surface the disagreement honestly, not decide which source “wins”. Substrate honesty applied to cross-source aggregation: when sources disagree, the response carries every source's row, not an opaque consensus.
License boundary downstream
Every public endpoint that returns price data declares its sources and per-source license tier in the response envelope's _meta.sources and _meta.source_license arrays. A consumer reading the response can tell what they may do with each byte:
- CC0 / CC-BY / MIT — display, compute, redistribute freely (with attribution where required).
- partner-redistributable (TCGplayer, Cardmarket) — display + computation OK per the upstream's partner agreement; bulk re-export restricted. Cambridge has the partner agreement; downstream consumers without one must respect the boundary.
- internal-only (CardRush, eBay raw listings) — personal-decision use only; not for bulk export, paid republication, or public archives.
Federation
A partner with a TCGplayer productId (or other upstream identifier) can resolve it back to Cambridge's canonical SKU + content_hash via /api/v1/federation/identify/by-upstream. The reverse — a content_hash → canonical SKU lookup — lives at /api/v1/federation/identify/[hash]. Both are CC0; identity resolution doesn't carry price data, so no license tier applies.
Where it began
The substrate widened from one source (CardRush) to many across two kingdoms in May 2026: kingdom-066 (the-cardrush-alignment) added the source column to price_archive; kingdom-080 (the-tcgplayer-alignment) widened the unique key to include condition and added the extra JSONB column plus generalised FX provenance. Adding the next source (Cardmarket, eBay Browse) is a mechanical extension that reuses the same shape.