Connectors: TCG data wave (tcgcsv, Digimon, SWU, Grand Archive, Sorcery, MTGGoldfish, pokemonprice; agent K); registry 61 connectors; more deterministic identifiers
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
42 changed files +7,711 −356
added
connectors/api/_lib/capture-k.ts
+89 −0
@@ -0,0 +1,89 @@ | ||
| 1 | +/** | |
| 2 | + * Live capture + smoke for the wave-3 TCG connectors. Runs each connector in probe mode through the real | |
| 3 | + * router/crawl context, saves small fixtures and prints sample normalized records. | |
| 4 | + * Usage: pnpm tsx connectors/api/_lib/capture-k.ts [ids…] (default: all seven) | |
| 5 | + */ | |
| 6 | +import { createCrawlContext, createRouter, type RareIndexConnector, type RawRecordInput } from '@rareindex/connectors'; | |
| 7 | +import { localMeta } from './local-meta.js'; | |
| 8 | +import { saveFixture } from '@rareindex/connectors/testing'; | |
| 9 | +import { childLogger } from '@rareindex/shared'; | |
| 10 | + | |
| 11 | +interface Plan { | |
| 12 | + limit: number; | |
| 13 | + seeds?: string[]; | |
| 14 | + /** pick which raw records become fixtures: returns a fixture name or null */ | |
| 15 | + pick: (raw: RawRecordInput, i: number) => string | null; | |
| 16 | + kinds: string[]; | |
| 17 | + requiredFields: string[]; | |
| 18 | +} | |
| 19 | + | |
| 20 | +const PLANS: Record<string, Plan> = { | |
| 21 | + tcgcsv: { | |
| 22 | + limit: 40, | |
| 23 | + seeds: ['3'], | |
| 24 | + pick: (raw, i) => { | |
| 25 | + const p = raw.payload as { product: { name: string }; prices: unknown[] }; | |
| 26 | + if (i === 0) return 'pokemon-first-product'; | |
| 27 | + if (p.prices.length > 1 && /charizard|blastoise|venusaur/i.test(p.product.name)) return 'pokemon-multi-subtype'; | |
| 28 | + return null; | |
| 29 | + }, | |
| 30 | + kinds: ['catalog_item', 'price_observation'], | |
| 31 | + requiredFields: ['attributes.identifiers.tcgplayer_id', 'attributes.set'], | |
| 32 | + }, | |
| 33 | + digimoncard: { limit: 12, seeds: ['BT5'], pick: (_r, i) => (i === 0 ? 'bt5-first' : i === 5 ? 'bt5-sixth' : null), kinds: ['catalog_item'], requiredFields: ['attributes.identifiers.digimoncard_id', 'attributes.number'] }, | |
| 34 | + 'swu-db': { limit: 8, seeds: ['SOR'], pick: (_r, i) => (i === 0 ? 'sor-first' : i === 3 ? 'sor-fourth' : null), kinds: ['catalog_item', 'price_observation'], requiredFields: ['attributes.setCode', 'attributes.number'] }, | |
| 35 | + 'grand-archive': { limit: 6, pick: (_r, i) => (i === 0 ? 'page1-first' : i === 2 ? 'page1-third' : null), kinds: ['catalog_item'], requiredFields: ['attributes.identifiers.gatcg_edition_id'] }, | |
| 36 | + 'sorcery-tcg': { limit: 6, pick: (_r, i) => (i === 0 ? 'first' : i === 5 ? 'sixth' : null), kinds: ['catalog_item'], requiredFields: ['attributes.identifiers.sorcery_printing_id', 'attributes.set'] }, | |
| 37 | + mtggoldfish: { | |
| 38 | + limit: 400, | |
| 39 | + seeds: ['Limited+Edition+Alpha'], | |
| 40 | + pick: (raw) => { | |
| 41 | + const p = raw.payload as { kind: string; card: { display_name: string; foil?: boolean } }; | |
| 42 | + if (p.kind === 'card' && p.card.display_name === 'Black Lotus' && !p.card.foil) return 'lea-black-lotus'; | |
| 43 | + if (p.kind === 'history' && p.card.display_name === 'Black Lotus') return 'lea-black-lotus-history'; | |
| 44 | + if (p.kind === 'card' && p.card.display_name === 'Air Elemental') return 'lea-air-elemental'; | |
| 45 | + return null; | |
| 46 | + }, | |
| 47 | + kinds: ['catalog_item', 'price_observation'], | |
| 48 | + requiredFields: ['attributes.setCode', 'attributes.number'], | |
| 49 | + }, | |
| 50 | + pokemonprice: { limit: 3, seeds: ['base-set-1st-edition'], pick: (raw, i) => (i === 0 ? 'base-set-1st-edition-first' : i === 2 ? 'base-set-1st-edition-third' : null), kinds: ['catalog_item', 'price_observation'], requiredFields: ['attributes.set', 'attributes.number'] }, | |
| 51 | +}; | |
| 52 | + | |
| 53 | +const ids = process.argv.slice(2).length ? process.argv.slice(2) : Object.keys(PLANS); | |
| 54 | +const router = createRouter({ firecrawlApiKey: process.env.FIRECRAWL_API_KEY, scrapflyApiKey: process.env.SCRAPFLY_API_KEY }); | |
| 55 | +for (const id of ids) { | |
| 56 | + const plan = PLANS[id]; | |
| 57 | + if (!plan) { | |
| 58 | + console.log(`[${id}] no plan`); | |
| 59 | + continue; | |
| 60 | + } | |
| 61 | + const started = Date.now(); | |
| 62 | + const mod = (await import(`../${id}/index.ts`)) as { default: (m: ReturnType<typeof localMeta>) => RareIndexConnector }; | |
| 63 | + const metaJson = (await import(`../${id}/meta.json`, { with: { type: 'json' } })) as { default: unknown }; | |
| 64 | + const connector = mod.default(localMeta(metaJson.default)); | |
| 65 | + const ctx = createCrawlContext({ router, meta: connector.meta, options: { mode: 'probe', limit: plan.limit, ...(plan.seeds ? { seeds: plan.seeds } : {}) }, log: childLogger({ connector: id, level: 'warn' }) }); | |
| 66 | + let raws = 0; | |
| 67 | + let normalized = 0; | |
| 68 | + let printed = 0; | |
| 69 | + let saved = 0; | |
| 70 | + for await (const raw of connector.crawl(ctx)) { | |
| 71 | + const rawLike = { ...raw, externalId: raw.externalId ?? null, fetchedAt: raw.fetchedAt ?? new Date() }; | |
| 72 | + const out = await connector.normalize(rawLike); | |
| 73 | + normalized += out.length; | |
| 74 | + const name = plan.pick(raw, raws); | |
| 75 | + raws++; | |
| 76 | + if (name) { | |
| 77 | + saveFixture(id, name, { raw: rawLike, expect: { minCount: 1, kinds: plan.kinds, requiredFields: plan.requiredFields }, note: `Live capture ${new Date().toISOString().slice(0, 10)} from ${raw.url}` }); | |
| 78 | + saved++; | |
| 79 | + } | |
| 80 | + if (printed < 3) { | |
| 81 | + for (const r of out.slice(0, 3)) { | |
| 82 | + const summary = r.kind === 'price_observation' ? `${r.priceKind} ${r.price} ${r.currency} @ ${r.observationDate.toISOString().slice(0, 10)}${r.grade.grader ? ` [${r.grade.grader} ${r.grade.grade ?? ''}]` : ''}` : r.kind === 'catalog_item' ? `variant=${r.attributes.variant ?? '-'} set=${r.attributes.set ?? '-'} #${r.attributes.number ?? '-'} ids=${JSON.stringify(r.attributes.identifiers)}` : ''; | |
| 83 | + console.log(` [${id}] ${r.kind}: ${'rawTitle' in r ? r.rawTitle : ''} → ${summary}`); | |
| 84 | + } | |
| 85 | + printed++; | |
| 86 | + } | |
| 87 | + } | |
| 88 | + console.log(`[${id}] raw=${raws} normalized=${normalized} fixtures=${saved} engineStats=${JSON.stringify(ctx.engineStats)} anomalies=${ctx.anomalies.slice(0, 3).join(' | ')} ms=${Date.now() - started}`); | |
| 89 | +} | |
added
connectors/api/_lib/local-meta.ts
+6 −0
@@ -0,0 +1,6 @@ | ||
| 1 | +import { ConnectorMetaSchema, type ConnectorMeta } from '@rareindex/connectors'; | |
| 2 | + | |
| 3 | +/** Parse a connector's own meta.json (lets tests/smoke run before the registry is rebuilt). */ | |
| 4 | +export function localMeta(meta: unknown): ConnectorMeta { | |
| 5 | + return ConnectorMetaSchema.parse(meta); | |
| 6 | +} | |
added
connectors/api/_lib/tcg-shared.ts
+68 −0
@@ -0,0 +1,68 @@ | ||
| 1 | +/** | |
| 2 | + * Helpers shared by the TCG catalog/price connectors added in wave 3 (tcgcsv, digimoncard, swu-db, | |
| 3 | + * grand-archive, sorcery-tcg, mtggoldfish, pokemonprice). Kept inside connectors/api (not the framework). | |
| 4 | + */ | |
| 5 | + | |
| 6 | +export const UA_HEADERS = { 'user-agent': 'RareIndexBot/0.1 (+https://www.rareindex.io/about/data)', accept: 'application/json, text/html;q=0.9, */*;q=0.8' }; | |
| 7 | + | |
| 8 | +/** UTC midnight of a Date (observation dates are day-precise). */ | |
| 9 | +export function dayOf(d: Date): Date { | |
| 10 | + return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); | |
| 11 | +} | |
| 12 | + | |
| 13 | +/** "2026-09-06T20:05:30+0000" | "2026-09-06" → UTC midnight Date, else null. */ | |
| 14 | +export function isoDay(s: string | null | undefined): Date | null { | |
| 15 | + if (!s) return null; | |
| 16 | + const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/); | |
| 17 | + if (!m) return null; | |
| 18 | + const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))); | |
| 19 | + return Number.isNaN(d.getTime()) ? null : d; | |
| 20 | +} | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * TCGplayer "subTypeName" / finish label → RareIndex variant vocabulary shared with the API catalogs | |
| 24 | + * (pokemontcg: 'Holo', 'Reverse Holo', '1st Edition', '1st Edition Holo'; scryfall: 'Foil', 'Etched Foil'). | |
| 25 | + * Returns null for the base printing. | |
| 26 | + */ | |
| 27 | +export function variantFromSubType(subType: string | null | undefined): string | null { | |
| 28 | + if (!subType) return null; | |
| 29 | + const s = subType.trim().toLowerCase(); | |
| 30 | + if (!s || s === 'normal' || s === 'regular' || s === 'standard' || s === 'non-foil' || s === 'nonfoil' || s === 'unlimited') return null; | |
| 31 | + if (s === 'holofoil' || s === 'holo') return 'Holo'; | |
| 32 | + if (s === 'reverse holofoil' || s === 'reverse holo') return 'Reverse Holo'; | |
| 33 | + if (s === '1st edition holofoil' || s === '1st edition holo') return '1st Edition Holo'; | |
| 34 | + if (s === '1st edition normal' || s === '1st edition') return '1st Edition'; | |
| 35 | + if (s === 'unlimited holofoil') return 'Holo'; | |
| 36 | + if (s === 'foil') return 'Foil'; | |
| 37 | + if (s === 'etched foil' || s === 'foil etched') return 'Etched Foil'; | |
| 38 | + // Title-case anything else ("Hyperspace Foil", "Showcase", "Textured Foil") | |
| 39 | + return subType.trim().replace(/\s+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); | |
| 40 | +} | |
| 41 | + | |
| 42 | +/** "PSA9" | "PSA 10" | "BGS9.5" | "CGC 9.8" | "Raw" → grader/grade. */ | |
| 43 | +export function parseCompactGrade(label: string | null | undefined): { grader: string | null; grade: string | null } { | |
| 44 | + if (!label) return { grader: null, grade: null }; | |
| 45 | + const s = label.trim(); | |
| 46 | + if (/^(raw|ungraded)$/i.test(s)) return { grader: 'raw', grade: null }; | |
| 47 | + const m = s.match(/^(PSA|BGS|CGC|SGC|TAG|ACE|CBCS)\s*(\d{1,2}(?:\.\d)?)$/i); | |
| 48 | + if (!m) return { grader: null, grade: null }; | |
| 49 | + return { grader: m[1]!.toLowerCase(), grade: m[2]! }; | |
| 50 | +} | |
| 51 | + | |
| 52 | +/** "101/102" → { number: "101", total: 102 }; "SV049" → { number: "SV049", total: null } */ | |
| 53 | +export function splitNumber(s: string | null | undefined): { number: string | null; total: number | null } { | |
| 54 | + if (!s) return { number: null, total: null }; | |
| 55 | + const t = s.trim(); | |
| 56 | + const m = t.match(/^([A-Za-z0-9-]+)\s*\/\s*([A-Za-z0-9-]+)$/); | |
| 57 | + if (m) { | |
| 58 | + const total = Number(m[2]); | |
| 59 | + return { number: m[1]!, total: Number.isFinite(total) ? total : null }; | |
| 60 | + } | |
| 61 | + return { number: t || null, total: null }; | |
| 62 | +} | |
| 63 | + | |
| 64 | +/** Upscale a TCGplayer CDN thumbnail (…_200w.jpg → …_400w.jpg). */ | |
| 65 | +export function tcgplayerImage(url: string | null | undefined): string[] { | |
| 66 | + if (!url) return []; | |
| 67 | + return [url.replace(/_200w\.jpg$/, '_400w.jpg')]; | |
| 68 | +} | |
added
connectors/api/digimoncard/index.test.ts
+29 −0
@@ -0,0 +1,29 @@ | ||
| 1 | +import { describe, expect, it } from 'vitest'; | |
| 2 | +import meta from './meta.json' with { type: 'json' }; | |
| 3 | +import { localMeta } from '../_lib/local-meta.js'; | |
| 4 | +import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; | |
| 5 | +import createConnector, { primarySet, setPrefix } from './index.js'; | |
| 6 | + | |
| 7 | +const connector = createConnector(localMeta(meta)); | |
| 8 | + | |
| 9 | +describe('digimoncard', () => { | |
| 10 | + runFixtureSuite(connector, it, expect); | |
| 11 | + | |
| 12 | + it('derives the primary set from the card number prefix', () => { | |
| 13 | + expect(setPrefix('BT5-103')).toEqual({ prefix: 'BT5', code: 'BT-05' }); | |
| 14 | + expect(setPrefix('ST1-03').code).toBe('ST-1'); | |
| 15 | + expect(primarySet('BT5-103', ['Promo', 'BT-05: Battle of Omni'])).toBe('BT-05: Battle of Omni'); | |
| 16 | + expect(primarySet('ST1-03', ['ST-1: Starter Deck Gaia Red', 'Trial Deck'])).toBe('ST-1: Starter Deck Gaia Red'); | |
| 17 | + }); | |
| 18 | + | |
| 19 | + it('emits digimon_tcg catalog items with tcgplayer ids when present', async () => { | |
| 20 | + const [name] = listFixtures('digimoncard'); | |
| 21 | + const out = await connector.normalize(loadFixture('digimoncard', name!).raw); | |
| 22 | + expect(out).toHaveLength(1); | |
| 23 | + const c = out[0]!; | |
| 24 | + if (c.kind !== 'catalog_item') throw new Error('expected catalog item'); | |
| 25 | + expect(c.attributes.categorySlug).toBe('digimon_tcg'); | |
| 26 | + expect(c.attributes.identifiers.digimoncard_id).toMatch(/^[A-Z]+\d*-\d+$/); | |
| 27 | + expect(c.imageUrls[0]).toContain('images.digimoncard.io'); | |
| 28 | + }); | |
| 29 | +}); | |
added
connectors/api/digimoncard/index.ts
+147 −0
@@ -0,0 +1,147 @@ | ||
| 1 | +import { z } from 'zod'; | |
| 2 | +import { BaseConnector, type ConnectorMeta, type CrawlContext, type RawRecordInput, type RawRecordLike } from '@rareindex/connectors'; | |
| 3 | +import { type NormalizedRecord } from '@rareindex/shared'; | |
| 4 | +import { attrs, catalogItem, makeTitle, withRetries } from '../_lib/shared.js'; | |
| 5 | +import { UA_HEADERS } from '../_lib/tcg-shared.js'; | |
| 6 | + | |
| 7 | +/** DigimonCard.io — full Digimon Card Game catalog in one public JSON call (no prices). */ | |
| 8 | +const API = 'https://digimoncard.io/api-public'; | |
| 9 | +const PARSER_VERSION = '1.0.0'; | |
| 10 | + | |
| 11 | +const CardSchema = z | |
| 12 | + .object({ | |
| 13 | + name: z.string(), | |
| 14 | + type: z.string().nullable().optional(), | |
| 15 | + id: z.string(), | |
| 16 | + color: z.string().nullable().optional(), | |
| 17 | + color2: z.string().nullable().optional(), | |
| 18 | + rarity: z.string().nullable().optional(), | |
| 19 | + stage: z.string().nullable().optional(), | |
| 20 | + attribute: z.string().nullable().optional(), | |
| 21 | + level: z.number().nullable().optional(), | |
| 22 | + dp: z.number().nullable().optional(), | |
| 23 | + artist: z.string().nullable().optional(), | |
| 24 | + series: z.string().nullable().optional(), | |
| 25 | + pretty_url: z.string().nullable().optional(), | |
| 26 | + date_added: z.string().nullable().optional(), | |
| 27 | + tcgplayer_name: z.string().nullable().optional(), | |
| 28 | + tcgplayer_id: z.number().nullable().optional(), | |
| 29 | + set_name: z.array(z.string()).nullable().optional(), | |
| 30 | + }) | |
| 31 | + .loose(); | |
| 32 | +export type DigimonCard = z.infer<typeof CardSchema>; | |
| 33 | + | |
| 34 | +export function trimCard(raw: Record<string, unknown>): DigimonCard { | |
| 35 | + const keep = ['name', 'type', 'id', 'color', 'color2', 'rarity', 'stage', 'attribute', 'level', 'dp', 'artist', 'series', 'pretty_url', 'date_added', 'tcgplayer_name', 'tcgplayer_id', 'set_name']; | |
| 36 | + const out: Record<string, unknown> = {}; | |
| 37 | + for (const k of keep) if (raw[k] !== undefined) out[k] = raw[k]; | |
| 38 | + return CardSchema.parse(out); | |
| 39 | +} | |
| 40 | + | |
| 41 | +/** "BT5-103" → { prefix: "BT5", code: "BT-05" }; "ST1-03" → { prefix "ST1", code "ST-1" }; "P-021" → { prefix "P", code "P" } */ | |
| 42 | +export function setPrefix(id: string): { prefix: string; code: string } { | |
| 43 | + const m = id.match(/^([A-Za-z]+)(\d*)-/); | |
| 44 | + if (!m) return { prefix: id, code: id }; | |
| 45 | + const letters = m[1]!.toUpperCase(); | |
| 46 | + const digits = m[2] ?? ''; | |
| 47 | + const code = digits ? `${letters}-${letters === 'BT' || letters === 'EX' ? digits.padStart(2, '0') : digits}` : letters; | |
| 48 | + return { prefix: `${letters}${digits}`, code }; | |
| 49 | +} | |
| 50 | + | |
| 51 | +/** Pick the primary set name for a card from its set list using the number prefix. */ | |
| 52 | +export function primarySet(id: string, sets: string[] | null | undefined): string | null { | |
| 53 | + if (!sets?.length) return null; | |
| 54 | + const { code, prefix } = setPrefix(id); | |
| 55 | + const letters = prefix.replace(/\d+$/, ''); | |
| 56 | + const digits = prefix.slice(letters.length); | |
| 57 | + const candidates = [code, `${letters}-${digits}`, `${letters}-${digits.padStart(2, '0')}`, `${letters}${digits}`]; | |
| 58 | + for (const s of sets) { | |
| 59 | + const head = s.split(':')[0]!.trim().toUpperCase(); | |
| 60 | + if (candidates.includes(head)) return s; | |
| 61 | + } | |
| 62 | + return sets[0] ?? null; | |
| 63 | +} | |
| 64 | + | |
| 65 | +export class DigimonCardConnector extends BaseConnector { | |
| 66 | + readonly version = '1.0.0'; | |
| 67 | + readonly parserVersion = PARSER_VERSION; | |
| 68 | + protected override minIntervalMs = 1000; | |
| 69 | + override readonly urlPatterns = [/digimoncard\.io\/card\/[a-z0-9-]+/i]; | |
| 70 | + | |
| 71 | + async *crawl(ctx: CrawlContext): AsyncIterable<RawRecordInput> { | |
| 72 | + await this.throttle(); | |
| 73 | + const res = await withRetries(() => ctx.fetch(`${API}/search?series=${encodeURIComponent('Digimon Card Game')}`, { engines: ['api'], headers: UA_HEADERS, timeoutMs: 120_000 }), (r) => r.success && Array.isArray(r.json), 3, 3000); | |
| 74 | + if (!res.success || !Array.isArray(res.json)) throw new Error(`digimoncard: catalog unavailable (${res.error ?? res.httpStatus})`); | |
| 75 | + const seen = new Map<string, number>(); | |
| 76 | + let count = 0; | |
| 77 | + for (const raw of res.json as Record<string, unknown>[]) { | |
| 78 | + let card: DigimonCard; | |
| 79 | + try { | |
| 80 | + card = trimCard(raw); | |
| 81 | + } catch (err) { | |
| 82 | + ctx.anomaly('parse_failure_card', err instanceof Error ? err.message : String(err)); | |
| 83 | + continue; | |
| 84 | + } | |
| 85 | + if (ctx.options.seeds?.length && !ctx.options.seeds.includes(setPrefix(card.id).prefix)) continue; | |
| 86 | + const n = (seen.get(card.id) ?? 0) + 1; | |
| 87 | + seen.set(card.id, n); | |
| 88 | + if (this.reached(ctx, count)) return; | |
| 89 | + count++; | |
| 90 | + yield { url: `https://digimoncard.io/card/${card.pretty_url ?? card.id.toLowerCase()}`, externalId: n > 1 ? `${card.id}#${n}` : card.id, kind: 'catalog_item', engine: 'api', httpStatus: res.httpStatus, payload: { card, alt: n }, fetchedAt: res.fetchedAt }; | |
| 91 | + } | |
| 92 | + await ctx.setCursor({ updatedAt: new Date().toISOString(), cards: count }); | |
| 93 | + } | |
| 94 | + | |
| 95 | + async lookup(url: string, ctx: CrawlContext): Promise<RawRecordInput[]> { | |
| 96 | + const m = url.match(/digimoncard\.io\/card\/([a-z0-9-]+)/i); | |
| 97 | + if (!m) return []; | |
| 98 | + const id = m[1]!.split('-').slice(-2).join('-').toUpperCase(); // pretty_url ends with the card number | |
| 99 | + const res = await ctx.fetch(`${API}/search?card=${encodeURIComponent(id)}`, { engines: ['api'], headers: UA_HEADERS }); | |
| 100 | + if (!res.success || !Array.isArray(res.json) || !res.json.length) return []; | |
| 101 | + const card = trimCard(res.json[0] as Record<string, unknown>); | |
| 102 | + return [{ url, externalId: card.id, kind: 'catalog_item', engine: 'api', httpStatus: res.httpStatus, payload: { card, alt: 1 }, fetchedAt: res.fetchedAt }]; | |
| 103 | + } | |
| 104 | + | |
| 105 | + async normalize(raw: RawRecordLike): Promise<NormalizedRecord[]> { | |
| 106 | + const { card, alt } = z.object({ card: CardSchema, alt: z.number().default(1) }).parse(raw.payload); | |
| 107 | + const set = primarySet(card.id, card.set_name); | |
| 108 | + const { code } = setPrefix(card.id); | |
| 109 | + const identifiers: Record<string, string> = { digimoncard_id: card.id }; | |
| 110 | + if (card.tcgplayer_id) identifiers.tcgplayer_id = String(card.tcgplayer_id); | |
| 111 | + const variant = alt > 1 ? `Alternate Art ${alt - 1}` : null; | |
| 112 | + const a = attrs({ | |
| 113 | + categorySlug: 'digimon_tcg', | |
| 114 | + franchise: 'Digimon', | |
| 115 | + brand: 'Bandai', | |
| 116 | + set, | |
| 117 | + setCode: code, | |
| 118 | + name: card.name, | |
| 119 | + number: card.id, | |
| 120 | + year: null, | |
| 121 | + variant, | |
| 122 | + language: 'English', | |
| 123 | + rarity: card.rarity ?? null, | |
| 124 | + identifiers, | |
| 125 | + metadata: { type: card.type, color: card.color, color2: card.color2, stage: card.stage, attribute: card.attribute, level: card.level, dp: card.dp, artist: card.artist, sets: card.set_name ?? [] }, | |
| 126 | + }); | |
| 127 | + const rawTitle = makeTitle({ name: card.name, set, number: card.id, variant }); | |
| 128 | + return [ | |
| 129 | + catalogItem({ | |
| 130 | + kind: 'catalog_item', | |
| 131 | + connectorId: this.meta.id, | |
| 132 | + sourceId: this.meta.sourceId, | |
| 133 | + sourceUrl: raw.url, | |
| 134 | + externalId: raw.externalId ?? card.id, | |
| 135 | + rawTitle, | |
| 136 | + imageUrls: [`https://images.digimoncard.io/images/cards/${card.id}.jpg`], | |
| 137 | + attributes: a, | |
| 138 | + observedAt: raw.fetchedAt, | |
| 139 | + confidence: 0.85, | |
| 140 | + parserVersion: PARSER_VERSION, | |
| 141 | + releaseDate: null, | |
| 142 | + }), | |
| 143 | + ]; | |
| 144 | + } | |
| 145 | +} | |
| 146 | + | |
| 147 | +export default (meta: ConnectorMeta) => new DigimonCardConnector(meta); | |
added
connectors/api/digimoncard/meta.json
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +{ | |
| 2 | + "id": "digimoncard", | |
| 3 | + "displayName": "DigimonCard.io (Digimon Card Game)", | |
| 4 | + "sourceId": "digimoncard", | |
| 5 | + "sourceName": "DigimonCard.io", | |
| 6 | + "sourceType": "catalog", | |
| 7 | + "sourceUrl": "https://digimoncard.io", | |
| 8 | + "module": "api/digimoncard", | |
| 9 | + "enginePriority": ["api"], | |
| 10 | + "categories": ["digimon_tcg"], | |
| 11 | + "regions": ["US", "JP"], | |
| 12 | + "languages": ["en"], | |
| 13 | + "currency": [], | |
| 14 | + "supportsListings": false, | |
| 15 | + "supportsSold": false, | |
| 16 | + "supportsAuctions": false, | |
| 17 | + "supportsImages": true, | |
| 18 | + "supportsCatalog": true, | |
| 19 | + "supportsPopulation": false, | |
| 20 | + "supportsLookup": true, | |
| 21 | + "refreshFrequencyMinutes": 10080, | |
| 22 | + "priority": "low", | |
| 23 | + "trustScore": 0.7, | |
| 24 | + "attributionRequired": true, | |
| 25 | + "termsUrl": "https://digimoncard.io/api-public/", | |
| 26 | + "accessNotes": "Free public JSON API (https://digimoncard.io/api-public/search?series=Digimon%20Card%20Game returns every card in one call; the documented '.php' paths 301 to the extensionless ones). Catalog only — no prices; TCGplayer product ids are included and used as the deterministic identifier (tcgplayer_id) so TCGCSV prices attach to the same assets. Images from images.digimoncard.io/images/cards/<id>.jpg. Primary set derived from the card number prefix (BT5 → 'BT-05' set entry).", | |
| 27 | + "enabled": true, | |
| 28 | + "schemaVersion": "1.0", | |
| 29 | + "config": {} | |
| 30 | +} | |
added
connectors/api/grand-archive/index.test.ts
+25 −0
@@ -0,0 +1,25 @@ | ||
| 1 | +import { describe, expect, it } from 'vitest'; | |
| 2 | +import meta from './meta.json' with { type: 'json' }; | |
| 3 | +import { localMeta } from '../_lib/local-meta.js'; | |
| 4 | +import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; | |
| 5 | +import createConnector from './index.js'; | |
| 6 | + | |
| 7 | +const connector = createConnector(localMeta(meta)); | |
| 8 | + | |
| 9 | +describe('grand-archive', () => { | |
| 10 | + runFixtureSuite(connector, it, expect); | |
| 11 | + | |
| 12 | + it('emits one catalog item per edition/finish with publisher populations', async () => { | |
| 13 | + const [name] = listFixtures('grand-archive'); | |
| 14 | + const out = await connector.normalize(loadFixture('grand-archive', name!).raw); | |
| 15 | + expect(out.length).toBeGreaterThanOrEqual(1); | |
| 16 | + for (const c of out) { | |
| 17 | + if (c.kind !== 'catalog_item') throw new Error('expected catalog item'); | |
| 18 | + expect(c.attributes.categorySlug).toBe('other_tcg'); | |
| 19 | + expect(c.attributes.franchise).toBe('Grand Archive'); | |
| 20 | + expect(c.attributes.identifiers.gatcg_edition_id).toBeTruthy(); | |
| 21 | + // production quantity only when the publisher states an exact count | |
| 22 | + if (c.attributes.productionQuantity !== null) expect(c.attributes.metadata.population_exact).toBe(true); | |
| 23 | + } | |
| 24 | + }); | |
| 25 | +}); | |
added
connectors/api/grand-archive/index.ts
+123 −0
@@ -0,0 +1,123 @@ | ||
| 1 | +import { z } from 'zod'; | |
| 2 | +import { BaseConnector, type ConnectorMeta, type CrawlContext, type RawRecordInput, type RawRecordLike } from '@rareindex/connectors'; | |
| 3 | +import { type NormalizedRecord } from '@rareindex/shared'; | |
| 4 | +import { attrs, catalogItem, makeTitle, withRetries } from '../_lib/shared.js'; | |
| 5 | +import { UA_HEADERS } from '../_lib/tcg-shared.js'; | |
| 6 | + | |
| 7 | +/** Grand Archive official index — editions with publisher-declared circulations (print runs). */ | |
| 8 | +const API = 'https://api.gatcg.com'; | |
| 9 | +const PARSER_VERSION = '1.0.0'; | |
| 10 | + | |
| 11 | +const CirculationSchema = z.object({ uuid: z.string().optional(), kind: z.string().nullable().optional(), foil: z.boolean().optional(), population: z.number().nullable().optional(), population_operator: z.string().nullable().optional(), printing: z.boolean().optional() }); | |
| 12 | +const EditionSchema = z | |
| 13 | + .object({ | |
| 14 | + uuid: z.string(), | |
| 15 | + slug: z.string().nullable().optional(), | |
| 16 | + collector_number: z.string().nullable().optional(), | |
| 17 | + rarity: z.number().nullable().optional(), | |
| 18 | + illustrator: z.string().nullable().optional(), | |
| 19 | + image: z.string().nullable().optional(), | |
| 20 | + configuration: z.string().nullable().optional(), | |
| 21 | + set: z.object({ id: z.string().optional(), name: z.string(), prefix: z.string().nullable().optional(), release_date: z.string().nullable().optional(), language: z.string().nullable().optional() }), | |
| 22 | + circulations: z.array(CirculationSchema).default([]), | |
| 23 | + }) | |
| 24 | + .loose(); | |
| 25 | +const CardSchema = z | |
| 26 | + .object({ | |
| 27 | + uuid: z.string(), | |
| 28 | + name: z.string(), | |
| 29 | + slug: z.string().nullable().optional(), | |
| 30 | + element: z.string().nullable().optional(), | |
| 31 | + types: z.array(z.string()).optional(), | |
| 32 | + classes: z.array(z.string()).optional(), | |
| 33 | + editions: z.array(EditionSchema).default([]), | |
| 34 | + }) | |
| 35 | + .loose(); | |
| 36 | +export type GaCard = z.infer<typeof CardSchema>; | |
| 37 | + | |
| 38 | +const RARITY: Record<number, string> = { 1: 'Common', 2: 'Uncommon', 3: 'Rare', 4: 'Super Rare', 5: 'Ultra Rare', 6: 'Promotional', 7: 'Collector Super Rare', 8: 'Collector Ultra Rare', 9: 'Collector Promo' }; | |
| 39 | + | |
| 40 | +export function trimCard(raw: Record<string, unknown>): GaCard { | |
| 41 | + const editions = Array.isArray(raw.editions) ? (raw.editions as Record<string, unknown>[]).map((e) => ({ | |
| 42 | + uuid: e.uuid, slug: e.slug, collector_number: e.collector_number, rarity: e.rarity, illustrator: e.illustrator, image: e.image, configuration: e.configuration, | |
| 43 | + set: e.set && typeof e.set === 'object' ? { id: (e.set as Record<string, unknown>).id, name: (e.set as Record<string, unknown>).name, prefix: (e.set as Record<string, unknown>).prefix, release_date: (e.set as Record<string, unknown>).release_date, language: (e.set as Record<string, unknown>).language } : undefined, | |
| 44 | + circulations: Array.isArray(e.circulations) ? (e.circulations as Record<string, unknown>[]).map((c) => ({ uuid: c.uuid, kind: c.kind, foil: c.foil, population: c.population, population_operator: c.population_operator, printing: c.printing })) : [], | |
| 45 | + })) : []; | |
| 46 | + return CardSchema.parse({ uuid: raw.uuid, name: raw.name, slug: raw.slug, element: raw.element, types: raw.types, classes: raw.classes, editions }); | |
| 47 | +} | |
| 48 | + | |
| 49 | +export class GrandArchiveConnector extends BaseConnector { | |
| 50 | + readonly version = '1.0.0'; | |
| 51 | + readonly parserVersion = PARSER_VERSION; | |
| 52 | + protected override minIntervalMs = 600; | |
| 53 | + | |
| 54 | + async *crawl(ctx: CrawlContext): AsyncIterable<RawRecordInput> { | |
| 55 | + const pageSize = Number(this.meta.config.pageSize ?? 50); | |
| 56 | + let page = Number(ctx.options.cursor?.page ?? 1); | |
| 57 | + let count = 0; | |
| 58 | + for (; page < 400; page++) { | |
| 59 | + if (ctx.signal?.aborted) return; | |
| 60 | + await this.throttle(); | |
| 61 | + const res = await withRetries(() => ctx.fetch(`${API}/cards/search?page=${page}&page_size=${pageSize}`, { engines: ['api'], headers: UA_HEADERS }), (r) => r.success && r.json !== null, 3, 2000); | |
| 62 | + const body = res.json as { data?: unknown[]; has_more?: boolean } | null; | |
| 63 | + if (!res.success || !Array.isArray(body?.data)) { | |
| 64 | + ctx.anomaly('page_fetch_failed', `page ${page}: ${res.error ?? res.httpStatus}`); | |
| 65 | + break; | |
| 66 | + } | |
| 67 | + for (const raw of body.data) { | |
| 68 | + let card: GaCard; | |
| 69 | + try { | |
| 70 | + card = trimCard(raw as Record<string, unknown>); | |
| 71 | + } catch (err) { | |
| 72 | + ctx.anomaly('parse_failure_card', `page ${page}: ${err instanceof Error ? err.message : String(err)}`); | |
| 73 | + continue; | |
| 74 | + } | |
| 75 | + if (this.reached(ctx, count)) return; | |
| 76 | + count++; | |
| 77 | + yield { url: `https://index.gatcg.com/card/${card.slug ?? card.uuid}`, externalId: card.uuid, kind: 'catalog_item', engine: 'api', httpStatus: res.httpStatus, payload: { card }, fetchedAt: res.fetchedAt }; | |
| 78 | + } | |
| 79 | + await ctx.setCursor({ page: page + 1, updatedAt: new Date().toISOString() }); | |
| 80 | + if (!body.has_more) break; | |
| 81 | + } | |
| 82 | + await ctx.setCursor({ page: 1, updatedAt: new Date().toISOString() }); | |
| 83 | + } | |
| 84 | + | |
| 85 | + async normalize(raw: RawRecordLike): Promise<NormalizedRecord[]> { | |
| 86 | + const { card } = z.object({ card: CardSchema }).parse(raw.payload); | |
| 87 | + const out: NormalizedRecord[] = []; | |
| 88 | + for (const ed of card.editions) { | |
| 89 | + const y = ed.set.release_date ? Number(ed.set.release_date.slice(0, 4)) : NaN; | |
| 90 | + const year = Number.isFinite(y) && y >= 1990 ? y : null; // epoch placeholders (1970) are not release years | |
| 91 | + const images = ed.image ? [`${API}${ed.image.startsWith('/') ? '' : '/'}${ed.image}`] : []; | |
| 92 | + const circulations = ed.circulations.length ? ed.circulations : [{ kind: 'NONFOIL', foil: false, population: null, population_operator: null }]; | |
| 93 | + const seen = new Set<string>(); | |
| 94 | + for (const c of circulations) { | |
| 95 | + const variant = c.foil || c.kind === 'FOIL' ? 'Foil' : null; | |
| 96 | + const key = variant ?? ''; | |
| 97 | + if (seen.has(key)) continue; | |
| 98 | + seen.add(key); | |
| 99 | + const exact = c.population_operator === '=' || c.population_operator === 'EXACT'; | |
| 100 | + const a = attrs({ | |
| 101 | + categorySlug: 'other_tcg', | |
| 102 | + franchise: 'Grand Archive', | |
| 103 | + brand: 'Weebs of the Shore', | |
| 104 | + set: ed.set.name, | |
| 105 | + setCode: ed.set.prefix ?? null, | |
| 106 | + name: card.name, | |
| 107 | + number: ed.collector_number ?? null, | |
| 108 | + year, | |
| 109 | + variant, | |
| 110 | + language: ed.set.language === 'EN' || !ed.set.language ? 'English' : ed.set.language, | |
| 111 | + rarity: ed.rarity ? (RARITY[ed.rarity] ?? String(ed.rarity)) : null, | |
| 112 | + productionQuantity: exact && c.population ? c.population : null, | |
| 113 | + identifiers: { gatcg_edition_id: ed.uuid, gatcg_card_id: card.uuid, ...(ed.slug ? { gatcg_slug: ed.slug } : {}) }, | |
| 114 | + metadata: { element: card.element, types: card.types ?? [], classes: card.classes ?? [], illustrator: ed.illustrator, configuration: ed.configuration, population: c.population ?? null, population_operator: c.population_operator ?? null, population_exact: exact }, | |
| 115 | + }); | |
| 116 | + out.push(catalogItem({ kind: 'catalog_item', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: `${ed.uuid}${variant ? `:${variant}` : ''}`, rawTitle: makeTitle({ name: card.name, set: ed.set.name, number: ed.collector_number ?? null, year, variant }), imageUrls: images, attributes: a, observedAt: raw.fetchedAt, confidence: 0.9, parserVersion: PARSER_VERSION, releaseDate: year && ed.set.release_date ? new Date(ed.set.release_date) : null })); | |
| 117 | + } | |
| 118 | + } | |
| 119 | + return out; | |
| 120 | + } | |
| 121 | +} | |
| 122 | + | |
| 123 | +export default (meta: ConnectorMeta) => new GrandArchiveConnector(meta); | |
added
connectors/api/grand-archive/meta.json
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +{ | |
| 2 | + "id": "grand-archive", | |
| 3 | + "displayName": "Grand Archive Index (official card API)", | |
| 4 | + "sourceId": "grand-archive", | |
| 5 | + "sourceName": "Grand Archive TCG Index", | |
| 6 | + "sourceType": "manufacturer", | |
| 7 | + "sourceUrl": "https://index.gatcg.com", | |
| 8 | + "module": "api/grand-archive", | |
| 9 | + "enginePriority": ["api"], | |
| 10 | + "categories": ["other_tcg"], | |
| 11 | + "regions": ["US"], | |
| 12 | + "languages": ["en"], | |
| 13 | + "currency": [], | |
| 14 | + "supportsListings": false, | |
| 15 | + "supportsSold": false, | |
| 16 | + "supportsAuctions": false, | |
| 17 | + "supportsImages": true, | |
| 18 | + "supportsCatalog": true, | |
| 19 | + "supportsPopulation": true, | |
| 20 | + "supportsLookup": false, | |
| 21 | + "refreshFrequencyMinutes": 10080, | |
| 22 | + "priority": "low", | |
| 23 | + "trustScore": 0.85, | |
| 24 | + "attributionRequired": true, | |
| 25 | + "termsUrl": "https://index.gatcg.com", | |
| 26 | + "accessNotes": "Official Grand Archive card index API (https://api.gatcg.com/cards/search, paginated, no key). Emits one catalog_item per edition × circulation (non-foil / foil) with the publisher's declared print run in `productionQuantity` when the operator is exact ('=') and in metadata when approximate ('≈') — a rare case of manufacturer-published production counts. No prices (TCGCSV supplies Grand Archive prices via tcgplayer_id when available).", | |
| 27 | + "enabled": true, | |
| 28 | + "schemaVersion": "1.0", | |
| 29 | + "config": { "pageSize": 50 } | |
| 30 | +} | |
added
connectors/api/mtggoldfish/index.test.ts
+39 −0
@@ -0,0 +1,39 @@ | ||
| 1 | +import { describe, expect, it } from 'vitest'; | |
| 2 | +import meta from './meta.json' with { type: 'json' }; | |
| 3 | +import { localMeta } from '../_lib/local-meta.js'; | |
| 4 | +import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; | |
| 5 | +import createConnector, { parseHistory, parseSetPage } from './index.js'; | |
| 6 | + | |
| 7 | +const connector = createConnector(localMeta(meta)); | |
| 8 | + | |
| 9 | +describe('mtggoldfish', () => { | |
| 10 | + runFixtureSuite(connector, it, expect); | |
| 11 | + | |
| 12 | + it('parses the embedded set JSON and history series', () => { | |
| 13 | + const html = `<title>Limited Edition Alpha (LEA) Price Guide | MTGGoldfish</title><div data-x="{"cards":[{"name":"Black Lotus [LEA]","set":"LEA","display_name":"Black Lotus","rarity":"Rare","foil":false,"card_num":232,"finish":"regular","card_uuid":"u1","prices":{"paper":{"current_price":"12345.5"},"online":{"current_price":null}},"links":{"default":"/price/limited-edition-alpha/232/black-lotus"},"source_image_variants":{"672x938":"https://img/x.webp"}}],"view_type":"grid"}"></div>`; | |
| 14 | + const { setName, cards } = parseSetPage(html); | |
| 15 | + expect(setName).toBe('Limited Edition Alpha'); | |
| 16 | + expect(cards).toHaveLength(1); | |
| 17 | + expect(cards[0]).toMatchObject({ set: 'LEA', display_name: 'Black Lotus', card_num: 232, paper: 12345.5, card_uuid: 'u1' }); | |
| 18 | + expect(parseHistory('var d = "";\nd += "2010-11-02, 3102.99\\n";\nd += "2010-11-03, 3110\\n";')).toEqual([['2010-11-02', 3102.99], ['2010-11-03', 3110]]); | |
| 19 | + }); | |
| 20 | + | |
| 21 | + it('emits magic catalog items keyed like Scryfall (set code + collector number) with USD observations', async () => { | |
| 22 | + const names = listFixtures('mtggoldfish'); | |
| 23 | + const card = names.find((n) => !n.includes('history')) ?? names[0]!; | |
| 24 | + const out = await connector.normalize(loadFixture('mtggoldfish', card).raw); | |
| 25 | + const c = out.find((r) => r.kind === 'catalog_item'); | |
| 26 | + if (!c || c.kind !== 'catalog_item') throw new Error('expected catalog item'); | |
| 27 | + expect(c.attributes.categorySlug).toBe('magic_the_gathering'); | |
| 28 | + expect(c.attributes.setCode).toMatch(/^[A-Z0-9]{2,6}$/); | |
| 29 | + expect(c.attributes.number).toBeTruthy(); | |
| 30 | + for (const o of out) if (o.kind === 'price_observation') expect(o.currency).toBe('USD'); | |
| 31 | + const history = names.find((n) => n.includes('history')); | |
| 32 | + if (history) { | |
| 33 | + const h = await connector.normalize(loadFixture('mtggoldfish', history).raw); | |
| 34 | + expect(h.length).toBeGreaterThan(10); | |
| 35 | + const dates = h.map((r) => (r.kind === 'price_observation' ? r.observationDate.toISOString().slice(0, 10) : '')); | |
| 36 | + expect(new Set(dates).size).toBe(dates.length); | |
| 37 | + } | |
| 38 | + }); | |
| 39 | +}); | |
added
connectors/api/mtggoldfish/index.ts
+214 −0
@@ -0,0 +1,214 @@ | ||
| 1 | +import { z } from 'zod'; | |
| 2 | +import { BaseConnector, html as htmlUtil, type ConnectorMeta, type CrawlContext, type RawRecordInput, type RawRecordLike } from '@rareindex/connectors'; | |
| 3 | +import { type NormalizedRecord } from '@rareindex/shared'; | |
| 4 | +import { attrs, catalogItem, makeTitle, num, priceObservation, withRetries } from '../_lib/shared.js'; | |
| 5 | +import { dayOf, isoDay } from '../_lib/tcg-shared.js'; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * MTGGoldfish — set pages embed a JSON card list with current paper/online prices; the public | |
| 9 | + * price-history component returns a daily paper price series (2010 →) per printing. | |
| 10 | + */ | |
| 11 | +const BASE = 'https://www.mtggoldfish.com'; | |
| 12 | +const PARSER_VERSION = '1.0.0'; | |
| 13 | +const HEADERS = { 'user-agent': 'RareIndexBot/0.1 (+https://www.rareindex.io/about/data)', accept: 'text/html,application/xhtml+xml' }; | |
| 14 | + | |
| 15 | +const CardSchema = z.object({ | |
| 16 | + name: z.string(), // "Black Lotus [LEA]" — MTGGoldfish card id | |
| 17 | + set: z.string(), | |
| 18 | + display_name: z.string(), | |
| 19 | + rarity: z.string().nullable().optional(), | |
| 20 | + foil: z.boolean().optional(), | |
| 21 | + card_num: z.union([z.number(), z.string()]).nullable().optional(), | |
| 22 | + finish: z.string().nullable().optional(), | |
| 23 | + card_uuid: z.string().nullable().optional(), | |
| 24 | + paper: z.number().nullable(), | |
| 25 | + online: z.number().nullable(), | |
| 26 | + link: z.string().nullable(), | |
| 27 | + image: z.string().nullable(), | |
| 28 | +}); | |
| 29 | +type Card = z.infer<typeof CardSchema>; | |
| 30 | + | |
| 31 | +const SetPayloadSchema = z.object({ kind: z.literal('card'), setName: z.string().nullable(), setSlug: z.string(), card: CardSchema }); | |
| 32 | +const HistoryPayloadSchema = z.object({ kind: z.literal('history'), setName: z.string().nullable(), card: CardSchema, series: z.array(z.tuple([z.string(), z.number()])) }); | |
| 33 | + | |
| 34 | +/** Parse the escaped JSON card list embedded in a set page. */ | |
| 35 | +export function parseSetPage(html: string): { setName: string | null; cards: Card[] } { | |
| 36 | + const idx = html.indexOf('"cards":['); | |
| 37 | + if (idx < 0) return { setName: titleOf(html), cards: [] }; | |
| 38 | + const start = html.lastIndexOf('="', idx) + 2; | |
| 39 | + const end = html.indexOf('"', start); | |
| 40 | + const raw = html.slice(start, end); | |
| 41 | + const decoded = raw.replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); | |
| 42 | + let json: { cards?: Array<Record<string, unknown>> }; | |
| 43 | + try { | |
| 44 | + json = JSON.parse(decoded) as { cards?: Array<Record<string, unknown>> }; | |
| 45 | + } catch { | |
| 46 | + return { setName: titleOf(html), cards: [] }; | |
| 47 | + } | |
| 48 | + const cards: Card[] = []; | |
| 49 | + for (const c of json.cards ?? []) { | |
| 50 | + const prices = (c.prices ?? {}) as Record<string, { current_price?: string | null } | null>; | |
| 51 | + const images = (c.source_image_variants ?? {}) as Record<string, string>; | |
| 52 | + const links = (c.links ?? {}) as Record<string, string>; | |
| 53 | + const parsed = CardSchema.safeParse({ | |
| 54 | + name: c.name, | |
| 55 | + set: c.set, | |
| 56 | + display_name: c.display_name, | |
| 57 | + rarity: c.rarity, | |
| 58 | + foil: c.foil, | |
| 59 | + card_num: c.card_num, | |
| 60 | + finish: c.finish, | |
| 61 | + card_uuid: c.card_uuid, | |
| 62 | + paper: num(prices.paper?.current_price), | |
| 63 | + online: num(prices.online?.current_price), | |
| 64 | + link: links.default ?? null, | |
| 65 | + image: images['672x938'] ?? images['265x370'] ?? null, | |
| 66 | + }); | |
| 67 | + if (parsed.success) cards.push(parsed.data); | |
| 68 | + } | |
| 69 | + return { setName: titleOf(html), cards }; | |
| 70 | +} | |
| 71 | + | |
| 72 | +function titleOf(html: string): string | null { | |
| 73 | + const m = html.match(/<title>([^<]+)<\/title>/); | |
| 74 | + if (!m) return null; | |
| 75 | + return m[1]!.replace(/\s*[|·-]\s*MTGGoldfish.*$/i, '').replace(/\s*Price(s| Guide).*$/i, '').replace(/\s*\([A-Z0-9]{2,6}\)\s*$/, '').trim() || null; | |
| 76 | +} | |
| 77 | + | |
| 78 | +/** "d += '2010-11-02, 3102.99\n'" lines → [[date, price], …] */ | |
| 79 | +export function parseHistory(js: string): Array<[string, number]> { | |
| 80 | + const out: Array<[string, number]> = []; | |
| 81 | + for (const m of js.matchAll(/(\d{4}-\d{2}-\d{2}),\s*([0-9.]+)/g)) { | |
| 82 | + const price = Number(m[2]); | |
| 83 | + if (Number.isFinite(price) && price > 0) out.push([m[1]!, price]); | |
| 84 | + } | |
| 85 | + return out; | |
| 86 | +} | |
| 87 | + | |
| 88 | +export class MtgGoldfishConnector extends BaseConnector { | |
| 89 | + readonly version = '1.0.0'; | |
| 90 | + readonly parserVersion = PARSER_VERSION; | |
| 91 | + protected override minIntervalMs = 1500; | |
| 92 | + override readonly urlPatterns = [/mtggoldfish\.com\/price\/[^/]+\/\d+\//i, /mtggoldfish\.com\/sets\/[^/?#]+/i]; | |
| 93 | + | |
| 94 | + private async page(ctx: CrawlContext, url: string) { | |
| 95 | + await this.throttle(); | |
| 96 | + return withRetries(() => ctx.fetch(url, { engines: ['api', 'firecrawl'], headers: HEADERS, responseType: 'text', minQuality: 0.3 }), (r) => r.success && Boolean(r.html || r.markdown), 3, 2500); | |
| 97 | + } | |
| 98 | + | |
| 99 | + private async discoverSets(ctx: CrawlContext): Promise<string[]> { | |
| 100 | + const seeds = (ctx.options.seeds?.length ? ctx.options.seeds : (this.meta.config.seeds as string[] | undefined)) ?? []; | |
| 101 | + if (seeds.length) return seeds; | |
| 102 | + const res = await this.page(ctx, `${BASE}/sets`); | |
| 103 | + const text = res.html ?? ''; | |
| 104 | + const slugs = [...new Set([...text.matchAll(/href="\/sets\/([^"#?]+)"/g)].map((m) => m[1]!))]; | |
| 105 | + return slugs; | |
| 106 | + } | |
| 107 | + | |
| 108 | + async *crawl(ctx: CrawlContext): AsyncIterable<RawRecordInput> { | |
| 109 | + const sets = await this.discoverSets(ctx); | |
| 110 | + const maxSets = ctx.options.mode === 'backfill' ? Infinity : Number(this.meta.config.maxSetsPerRun ?? 60); | |
| 111 | + const hist = (this.meta.config.history ?? {}) as { enabled?: boolean; minPrice?: number; maxPerRun?: number; days?: number }; | |
| 112 | + let setIdx = Number(ctx.options.cursor?.setIdx ?? 0); | |
| 113 | + if (setIdx >= sets.length) setIdx = 0; | |
| 114 | + let done = 0; | |
| 115 | + let count = 0; | |
| 116 | + let historyFetched = 0; | |
| 117 | + for (; setIdx < sets.length && done < maxSets; setIdx++, done++) { | |
| 118 | + const slug = sets[setIdx]!; | |
| 119 | + const res = await this.page(ctx, `${BASE}/sets/${slug}`); | |
| 120 | + if (!res.success || !res.html) { | |
| 121 | + ctx.anomaly('page_fetch_failed', `${slug}: ${res.error ?? res.httpStatus}`); | |
| 122 | + continue; | |
| 123 | + } | |
| 124 | + const { setName, cards } = parseSetPage(res.html); | |
| 125 | + if (!cards.length) ctx.anomaly('empty_page', `set ${slug}`); | |
| 126 | + for (const card of cards) { | |
| 127 | + if (this.reached(ctx, count)) return; | |
| 128 | + count++; | |
| 129 | + const url = card.link ? `${BASE}${card.link}` : `${BASE}/sets/${slug}`; | |
| 130 | + yield { url, externalId: card.card_uuid ?? card.name, kind: 'catalog_item', engine: res.engine, httpStatus: res.httpStatus, payload: { kind: 'card', setName, setSlug: slug, card }, fetchedAt: res.fetchedAt }; | |
| 131 | + if (hist.enabled !== false && card.paper !== null && card.paper >= Number(hist.minPrice ?? 100) && historyFetched < Number(hist.maxPerRun ?? 150) && !card.foil) { | |
| 132 | + historyFetched++; | |
| 133 | + const q = new URLSearchParams({ card_id: card.name, selector: '#tab-paper', type: 'paper', price_type: '' }); | |
| 134 | + const h = await this.page(ctx, `${BASE}/price_history_component?${q.toString()}`); | |
| 135 | + if (h.success && h.html) { | |
| 136 | + const cutoff = Date.now() - Number(hist.days ?? 1095) * 86_400_000; | |
| 137 | + const series = parseHistory(h.html).filter(([d]) => new Date(`${d}T00:00:00Z`).getTime() >= cutoff); | |
| 138 | + if (series.length) yield { url, externalId: `${card.card_uuid ?? card.name}:history`, kind: 'price_observation', engine: h.engine, httpStatus: h.httpStatus, payload: { kind: 'history', setName, card, series }, fetchedAt: h.fetchedAt }; | |
| 139 | + } else ctx.anomaly('page_fetch_failed', `history ${card.name}: ${h.error ?? h.httpStatus}`); | |
| 140 | + } | |
| 141 | + } | |
| 142 | + await ctx.setCursor({ setIdx: setIdx + 1, updatedAt: new Date().toISOString() }); | |
| 143 | + } | |
| 144 | + if (setIdx >= sets.length) await ctx.setCursor({ setIdx: 0, updatedAt: new Date().toISOString() }); | |
| 145 | + } | |
| 146 | + | |
| 147 | + async lookup(url: string, ctx: CrawlContext): Promise<RawRecordInput[]> { | |
| 148 | + const m = url.match(/mtggoldfish\.com\/price\/([^/]+)\/(\d+)\//i); | |
| 149 | + if (!m) return []; | |
| 150 | + // Card pages do not embed the JSON list; resolve through the set page. | |
| 151 | + const res = await this.page(ctx, `${BASE}/sets/${m[1]}`); | |
| 152 | + if (!res.success || !res.html) return []; | |
| 153 | + const { setName, cards } = parseSetPage(res.html); | |
| 154 | + const card = cards.find((c) => String(c.card_num) === m[2] && !c.foil) ?? cards.find((c) => String(c.card_num) === m[2]); | |
| 155 | + if (!card) return []; | |
| 156 | + return [{ url, externalId: card.card_uuid ?? card.name, kind: 'catalog_item', engine: res.engine, httpStatus: res.httpStatus, payload: { kind: 'card', setName, setSlug: m[1]!, card }, fetchedAt: res.fetchedAt }]; | |
| 157 | + } | |
| 158 | + | |
| 159 | + private attrsFor(card: Card, setName: string | null) { | |
| 160 | + const variant = card.foil || /foil/i.test(card.finish ?? '') ? (/etched/i.test(card.finish ?? '') ? 'Etched Foil' : 'Foil') : null; | |
| 161 | + return { | |
| 162 | + variant, | |
| 163 | + a: attrs({ | |
| 164 | + categorySlug: 'magic_the_gathering', | |
| 165 | + franchise: 'Magic: The Gathering', | |
| 166 | + brand: 'Wizards of the Coast', | |
| 167 | + set: setName ?? card.set, | |
| 168 | + setCode: card.set.toUpperCase(), | |
| 169 | + name: card.display_name, | |
| 170 | + number: card.card_num !== null && card.card_num !== undefined ? String(card.card_num) : null, | |
| 171 | + year: null, | |
| 172 | + variant, | |
| 173 | + language: 'English', | |
| 174 | + rarity: card.rarity ?? null, | |
| 175 | + identifiers: { mtggoldfish_card_id: card.name, ...(card.card_uuid ? { mtggoldfish_uuid: card.card_uuid } : {}) }, | |
| 176 | + metadata: { finish: card.finish ?? null, online_price: card.online }, | |
| 177 | + }), | |
| 178 | + }; | |
| 179 | + } | |
| 180 | + | |
| 181 | + async normalize(raw: RawRecordLike): Promise<NormalizedRecord[]> { | |
| 182 | + const payload = raw.payload as { kind?: string }; | |
| 183 | + if (payload.kind === 'history') { | |
| 184 | + const { card, setName, series } = HistoryPayloadSchema.parse(raw.payload); | |
| 185 | + const { a, variant } = this.attrsFor(card, setName); | |
| 186 | + const rawTitle = makeTitle({ name: card.display_name, set: setName ?? card.set, number: card.card_num !== null && card.card_num !== undefined ? String(card.card_num) : null, variant }); | |
| 187 | + const out: NormalizedRecord[] = []; | |
| 188 | + const seen = new Set<string>(); | |
| 189 | + for (const [date, price] of series) { | |
| 190 | + if (seen.has(date)) continue; | |
| 191 | + seen.add(date); | |
| 192 | + const day = isoDay(date); | |
| 193 | + if (!day) continue; | |
| 194 | + out.push(priceObservation({ kind: 'price_observation', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: `${card.card_uuid ?? card.name}:paper:${date}`, rawTitle, imageUrls: card.image ? [card.image] : [], attributes: a, observedAt: raw.fetchedAt, confidence: 0.7, parserVersion: PARSER_VERSION, priceKind: 'market', price, currency: 'USD', observationDate: day, sampleSize: null })); | |
| 195 | + } | |
| 196 | + return out; | |
| 197 | + } | |
| 198 | + const { card, setName } = SetPayloadSchema.parse(raw.payload); | |
| 199 | + const { a, variant } = this.attrsFor(card, setName); | |
| 200 | + const number = card.card_num !== null && card.card_num !== undefined ? String(card.card_num) : null; | |
| 201 | + const rawTitle = makeTitle({ name: card.display_name, set: setName ?? card.set, number, variant }); | |
| 202 | + const images = card.image ? [card.image] : []; | |
| 203 | + const out: NormalizedRecord[] = [ | |
| 204 | + catalogItem({ kind: 'catalog_item', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: card.card_uuid ?? card.name, rawTitle, imageUrls: images, attributes: a, observedAt: raw.fetchedAt, confidence: 0.85, parserVersion: PARSER_VERSION, releaseDate: null }), | |
| 205 | + ]; | |
| 206 | + if (card.paper) { | |
| 207 | + out.push(priceObservation({ kind: 'price_observation', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: `${card.card_uuid ?? card.name}:paper:current`, rawTitle, imageUrls: images, attributes: a, observedAt: raw.fetchedAt, confidence: 0.75, parserVersion: PARSER_VERSION, priceKind: 'market', price: card.paper, currency: 'USD', observationDate: dayOf(raw.fetchedAt), sampleSize: null })); | |
| 208 | + } | |
| 209 | + return out; | |
| 210 | + } | |
| 211 | +} | |
| 212 | + | |
| 213 | +export default (meta: ConnectorMeta) => new MtgGoldfishConnector(meta); | |
| 214 | +export { htmlUtil as _html }; | |
added
connectors/api/mtggoldfish/meta.json
+34 −0
@@ -0,0 +1,34 @@ | ||
| 1 | +{ | |
| 2 | + "id": "mtggoldfish", | |
| 3 | + "displayName": "MTGGoldfish (paper prices & history)", | |
| 4 | + "sourceId": "mtggoldfish", | |
| 5 | + "sourceName": "MTGGoldfish", | |
| 6 | + "sourceType": "pricing_guide", | |
| 7 | + "sourceUrl": "https://www.mtggoldfish.com", | |
| 8 | + "module": "api/mtggoldfish", | |
| 9 | + "enginePriority": ["api", "firecrawl"], | |
| 10 | + "categories": ["magic_the_gathering"], | |
| 11 | + "regions": ["US"], | |
| 12 | + "languages": ["en"], | |
| 13 | + "currency": ["USD"], | |
| 14 | + "supportsListings": false, | |
| 15 | + "supportsSold": false, | |
| 16 | + "supportsAuctions": false, | |
| 17 | + "supportsImages": true, | |
| 18 | + "supportsCatalog": true, | |
| 19 | + "supportsPopulation": false, | |
| 20 | + "supportsLookup": true, | |
| 21 | + "refreshFrequencyMinutes": 1440, | |
| 22 | + "priority": "medium", | |
| 23 | + "trustScore": 0.75, | |
| 24 | + "attributionRequired": true, | |
| 25 | + "termsUrl": "https://www.mtggoldfish.com/robots.txt", | |
| 26 | + "accessNotes": "Public set pages (https://www.mtggoldfish.com/sets/<Set+Name>) embed a JSON payload with every printing (card_uuid, set code, collector number, finish, current paper/online price, images). robots.txt allows '/' for generic agents (only widgets/embeds are disallowed; Content-Signal ai-train=no is respected — data is used as market reference, not for training). Paper price = TCGplayer-derived market price → price_observation 'market' dated by fetch day (confidence 0.75). For cards above `history.minPrice` the public price-history component (/price_history_component, daily series since 2010) is fetched, capped per run → dated observations that give assets a real multi-year guide-price history. Politeness 1.5 s/page; ~340 sets.", | |
| 27 | + "enabled": true, | |
| 28 | + "schemaVersion": "1.0", | |
| 29 | + "config": { | |
| 30 | + "seeds": [], | |
| 31 | + "maxSetsPerRun": 60, | |
| 32 | + "history": { "enabled": true, "minPrice": 100, "maxPerRun": 150, "days": 1095 } | |
| 33 | + } | |
| 34 | +} | |
added
connectors/api/pokemonprice/index.test.ts
+41 −0
@@ -0,0 +1,41 @@ | ||
| 1 | +import { describe, expect, it } from 'vitest'; | |
| 2 | +import meta from './meta.json' with { type: 'json' }; | |
| 3 | +import { localMeta } from '../_lib/local-meta.js'; | |
| 4 | +import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; | |
| 5 | +import createConnector, { extractJson, setAndVariant } from './index.js'; | |
| 6 | +import { parseCompactGrade } from '../_lib/tcg-shared.js'; | |
| 7 | + | |
| 8 | +const connector = createConnector(localMeta(meta)); | |
| 9 | + | |
| 10 | +describe('pokemonprice', () => { | |
| 11 | + runFixtureSuite(connector, it, expect); | |
| 12 | + | |
| 13 | + it('aligns set names and variants with the pokemontcg vocabulary', () => { | |
| 14 | + expect(setAndVariant('Base Set 1st Edition', 'Holo 1st edition')).toEqual({ set: 'Base Set', variant: '1st Edition Holo' }); | |
| 15 | + expect(setAndVariant('Base Set Unlimited', 'Holo')).toEqual({ set: 'Base Set', variant: 'Holo' }); | |
| 16 | + expect(setAndVariant('Base Set Shadowless', null)).toEqual({ set: 'Base Set', variant: 'Shadowless' }); | |
| 17 | + expect(setAndVariant('Evolving Skies', null)).toEqual({ set: 'Evolving Skies', variant: null }); | |
| 18 | + expect(parseCompactGrade('PSA9')).toEqual({ grader: 'psa', grade: '9' }); | |
| 19 | + expect(parseCompactGrade('Raw')).toEqual({ grader: 'raw', grade: null }); | |
| 20 | + expect(extractJson('x "rows":[{"a":[1,2]},{"b":"]"}] y', 'rows')).toEqual([{ a: [1, 2] }, { b: ']' }]); | |
| 21 | + }); | |
| 22 | + | |
| 23 | + it('emits per-grade guide values with grader/grade and transaction counts', async () => { | |
| 24 | + const [name] = listFixtures('pokemonprice'); | |
| 25 | + const out = await connector.normalize(loadFixture('pokemonprice', name!).raw); | |
| 26 | + const cat = out.find((r) => r.kind === 'catalog_item'); | |
| 27 | + if (!cat || cat.kind !== 'catalog_item') throw new Error('expected catalog item'); | |
| 28 | + expect(cat.attributes.categorySlug).toBe('pokemon'); | |
| 29 | + expect(cat.attributes.set).toBeTruthy(); | |
| 30 | + expect(cat.attributes.number).toMatch(/^\d+/); | |
| 31 | + const obs = out.filter((r) => r.kind === 'price_observation'); | |
| 32 | + expect(obs.length).toBeGreaterThan(3); | |
| 33 | + const graded = obs.filter((o) => o.kind === 'price_observation' && o.grade.grader && o.grade.grader !== 'raw'); | |
| 34 | + expect(graded.length).toBeGreaterThan(0); | |
| 35 | + for (const o of obs) { | |
| 36 | + if (o.kind !== 'price_observation') continue; | |
| 37 | + expect(o.currency).toBe('USD'); | |
| 38 | + expect(o.confidence).toBeLessThanOrEqual(0.8); | |
| 39 | + } | |
| 40 | + }); | |
| 41 | +}); | |
added
connectors/api/pokemonprice/index.ts
+262 −0
@@ -0,0 +1,262 @@ | ||
| 1 | +import { z } from 'zod'; | |
| 2 | +import { BaseConnector, type ConnectorMeta, type CrawlContext, type RawRecordInput, type RawRecordLike } from '@rareindex/connectors'; | |
| 3 | +import { type NormalizedRecord } from '@rareindex/shared'; | |
| 4 | +import { attrs, catalogItem, makeTitle, num, priceObservation, withRetries } from '../_lib/shared.js'; | |
| 5 | +import { dayOf, isoDay, parseCompactGrade } from '../_lib/tcg-shared.js'; | |
| 6 | + | |
| 7 | +/** PokemonPrice.com — per-grade fair-value model for Pokémon cards, embedded in server-rendered pages. */ | |
| 8 | +const BASE = 'https://www.pokemonprice.com'; | |
| 9 | +const PARSER_VERSION = '1.0.0'; | |
| 10 | +const HEADERS = { 'user-agent': 'RareIndexBot/0.1 (+https://www.rareindex.io/about/data)', accept: 'text/html,application/xhtml+xml' }; | |
| 11 | + | |
| 12 | +const GradeRowSchema = z.object({ | |
| 13 | + grade: z.string(), | |
| 14 | + fair_price: z.union([z.string(), z.number()]).nullable().optional(), | |
| 15 | + low_price: z.union([z.string(), z.number()]).nullable().optional(), | |
| 16 | + high_price: z.union([z.string(), z.number()]).nullable().optional(), | |
| 17 | + confidence: z.union([z.string(), z.number()]).nullable().optional(), | |
| 18 | + last_sale_date: z.string().nullable().optional(), | |
| 19 | +}); | |
| 20 | +const TxSchema = z.object({ month: z.string(), grade: z.string(), count: z.number() }); | |
| 21 | +const PayloadSchema = z.object({ | |
| 22 | + slug: z.string(), | |
| 23 | + setSlug: z.string(), | |
| 24 | + name: z.string(), | |
| 25 | + tag: z.string().nullable(), | |
| 26 | + number: z.string().nullable(), | |
| 27 | + total: z.string().nullable(), | |
| 28 | + setName: z.string().nullable(), | |
| 29 | + image: z.string().nullable(), | |
| 30 | + rows: z.array(GradeRowSchema), | |
| 31 | + transactions: z.array(TxSchema), | |
| 32 | +}); | |
| 33 | +export type PokemonPricePayload = z.infer<typeof PayloadSchema>; | |
| 34 | + | |
| 35 | +/** Decode the RSC flight strings of a Next.js app-router page into one searchable string. */ | |
| 36 | +export function flightText(html: string): string { | |
| 37 | + const parts: string[] = []; | |
| 38 | + for (const m of html.matchAll(/self\.__next_f\.push\(\[1,"((?:[^"\\]|\\.)*)"\]\)/g)) { | |
| 39 | + try { | |
| 40 | + parts.push(JSON.parse(`"${m[1]}"`) as string); | |
| 41 | + } catch { | |
| 42 | + /* skip undecodable chunk */ | |
| 43 | + } | |
| 44 | + } | |
| 45 | + return parts.join(''); | |
| 46 | +} | |
| 47 | + | |
| 48 | +/** Extract a balanced JSON array/object that starts right after `key":`. */ | |
| 49 | +export function extractJson(text: string, key: string): unknown | null { | |
| 50 | + const idx = text.indexOf(`"${key}":`); | |
| 51 | + if (idx < 0) return null; | |
| 52 | + let i = text.indexOf(':', idx) + 1; | |
| 53 | + while (text[i] === ' ') i++; | |
| 54 | + const open = text[i]; | |
| 55 | + if (open !== '[' && open !== '{') return null; | |
| 56 | + const close = open === '[' ? ']' : '}'; | |
| 57 | + let depth = 0; | |
| 58 | + let inStr = false; | |
| 59 | + for (let j = i; j < text.length; j++) { | |
| 60 | + const ch = text[j]!; | |
| 61 | + if (inStr) { | |
| 62 | + if (ch === '\\') j++; | |
| 63 | + else if (ch === '"') inStr = false; | |
| 64 | + continue; | |
| 65 | + } | |
| 66 | + if (ch === '"') inStr = true; | |
| 67 | + else if (ch === open) depth++; | |
| 68 | + else if (ch === close) { | |
| 69 | + depth--; | |
| 70 | + if (depth === 0) { | |
| 71 | + try { | |
| 72 | + return JSON.parse(text.slice(i, j + 1)); | |
| 73 | + } catch { | |
| 74 | + return null; | |
| 75 | + } | |
| 76 | + } | |
| 77 | + } | |
| 78 | + } | |
| 79 | + return null; | |
| 80 | +} | |
| 81 | + | |
| 82 | +export function parseCardPage(html: string, url: string): PokemonPricePayload | null { | |
| 83 | + const h1 = html.match(/<h1[^>]*>([\s\S]*?)<\/h1>/); | |
| 84 | + const spans = h1 ? [...h1[1]!.matchAll(/<span[^>]*>([^<]*)<\/span>/g)].map((m) => m[1]!.trim()).filter(Boolean) : []; | |
| 85 | + const titleSpan = spans[0] ?? ''; | |
| 86 | + const nm = titleSpan.match(/^(.*?)\s*(?:\[(.+?)\])?$/); | |
| 87 | + const name = (nm?.[1] ?? titleSpan).trim(); | |
| 88 | + const tag = nm?.[2]?.trim() ?? null; | |
| 89 | + const numSpan = spans.find((s) => /^\d+[A-Za-z]?\s*\/\s*\w+$/.test(s) || /^[A-Za-z0-9]+\s*\/\s*[A-Za-z0-9]+$/.test(s)); | |
| 90 | + const [number, total] = numSpan ? numSpan.split('/').map((s) => s.trim()) : [null, null]; | |
| 91 | + const setName = spans.filter((s) => s !== titleSpan && s !== numSpan).pop() ?? null; | |
| 92 | + const image = html.match(/https:\/\/cdn\.pokemonprice\.com\/cards\/[^"\\\s]+/)?.[0] ?? null; | |
| 93 | + const text = flightText(html); | |
| 94 | + const rowsRaw = extractJson(text, 'rows'); | |
| 95 | + const rows = Array.isArray(rowsRaw) ? rowsRaw.filter((r) => r && typeof r === 'object' && 'grade' in (r as object) && 'fair_price' in (r as object)).map((r) => { | |
| 96 | + const o = r as Record<string, unknown>; | |
| 97 | + return { grade: String(o.grade), fair_price: o.fair_price as string, low_price: o.low_price as string, high_price: o.high_price as string, confidence: o.confidence as string, last_sale_date: (o.last_sale_date as string) ?? null }; | |
| 98 | + }) : []; | |
| 99 | + const txRaw = extractJson(text, 'transactions'); | |
| 100 | + const transactions = Array.isArray(txRaw) ? txRaw.filter((t) => t && typeof t === 'object' && 'month' in (t as object)).map((t) => ({ month: String((t as Record<string, unknown>).month), grade: String((t as Record<string, unknown>).grade), count: Number((t as Record<string, unknown>).count) || 0 })) : []; | |
| 101 | + if (!name) return null; | |
| 102 | + const parts = new URL(url).pathname.split('/').filter(Boolean); | |
| 103 | + const parsed = PayloadSchema.safeParse({ slug: parts.slice(-1)[0] ?? '', setSlug: parts[0] ?? '', name, tag, number: number ?? null, total: total ?? null, setName, image, rows, transactions }); | |
| 104 | + return parsed.success ? parsed.data : null; | |
| 105 | +} | |
| 106 | + | |
| 107 | +/** "Base Set 1st Edition" + tag "Holo 1st edition" → { set: "Base Set", variant: "1st Edition Holo" } */ | |
| 108 | +export function setAndVariant(setName: string | null, tag: string | null): { set: string | null; variant: string | null } { | |
| 109 | + let set = setName?.trim() ?? null; | |
| 110 | + const flags = new Set<string>(); | |
| 111 | + if (set) { | |
| 112 | + const m = set.match(/^(.*?)\s+(1st Edition|Shadowless|Unlimited)$/i); | |
| 113 | + if (m) { | |
| 114 | + set = m[1]!.trim(); | |
| 115 | + const f = m[2]!.toLowerCase(); | |
| 116 | + if (f === '1st edition') flags.add('1st Edition'); | |
| 117 | + if (f === 'shadowless') flags.add('Shadowless'); | |
| 118 | + } | |
| 119 | + } | |
| 120 | + for (const t of (tag ?? '').split(/[,·]/).map((s) => s.trim().toLowerCase()).filter(Boolean)) { | |
| 121 | + if (t.includes('reverse')) flags.add('Reverse Holo'); | |
| 122 | + else if (t.includes('1st') && t.includes('holo')) { | |
| 123 | + flags.add('1st Edition'); | |
| 124 | + flags.add('Holo'); | |
| 125 | + } else if (t.includes('1st')) flags.add('1st Edition'); | |
| 126 | + else if (t.includes('holo')) flags.add('Holo'); | |
| 127 | + else if (t.includes('shadowless')) flags.add('Shadowless'); | |
| 128 | + else if (t !== 'unlimited') flags.add(t.replace(/\b\w/g, (c) => c.toUpperCase())); | |
| 129 | + } | |
| 130 | + const ordered: string[] = []; | |
| 131 | + if (flags.has('1st Edition') && flags.has('Holo')) ordered.push('1st Edition Holo'); | |
| 132 | + else { | |
| 133 | + if (flags.has('1st Edition')) ordered.push('1st Edition'); | |
| 134 | + if (flags.has('Holo')) ordered.push('Holo'); | |
| 135 | + } | |
| 136 | + if (flags.has('Shadowless')) ordered.push('Shadowless'); | |
| 137 | + if (flags.has('Reverse Holo')) ordered.push('Reverse Holo'); | |
| 138 | + for (const f of flags) if (!['1st Edition', 'Holo', 'Shadowless', 'Reverse Holo'].includes(f)) ordered.push(f); | |
| 139 | + return { set, variant: ordered.length ? ordered.join(' · ') : null }; | |
| 140 | +} | |
| 141 | + | |
| 142 | +export class PokemonPriceConnector extends BaseConnector { | |
| 143 | + readonly version = '1.0.0'; | |
| 144 | + readonly parserVersion = PARSER_VERSION; | |
| 145 | + protected override minIntervalMs = 1500; | |
| 146 | + override readonly urlPatterns = [/pokemonprice\.com\/[a-z0-9-]+\/[a-z0-9-]+$/i]; | |
| 147 | + | |
| 148 | + private async page(ctx: CrawlContext, url: string) { | |
| 149 | + await this.throttle(); | |
| 150 | + return withRetries(() => ctx.fetch(url, { engines: ['api', 'firecrawl'], headers: HEADERS, responseType: 'text', minQuality: 0.3 }), (r) => r.success && Boolean(r.html), 3, 2500); | |
| 151 | + } | |
| 152 | + | |
| 153 | + async *crawl(ctx: CrawlContext): AsyncIterable<RawRecordInput> { | |
| 154 | + const seeds = (ctx.options.seeds?.length ? ctx.options.seeds : (this.meta.config.seeds as string[] | undefined)) ?? []; | |
| 155 | + let sets = seeds; | |
| 156 | + if (!sets.length) { | |
| 157 | + const idx = await this.page(ctx, `${BASE}/card-lists`); | |
| 158 | + sets = [...new Set([...(idx.html ?? '').matchAll(/href="\/card-lists\/([a-z0-9-]+)"/g)].map((m) => m[1]!))]; | |
| 159 | + } | |
| 160 | + const maxCards = ctx.options.mode === 'backfill' ? Infinity : Number(this.meta.config.maxCardsPerRun ?? 300); | |
| 161 | + let setIdx = Number(ctx.options.cursor?.setIdx ?? 0); | |
| 162 | + if (setIdx >= sets.length) setIdx = 0; | |
| 163 | + let count = 0; | |
| 164 | + for (; setIdx < sets.length; setIdx++) { | |
| 165 | + const setSlug = sets[setIdx]!; | |
| 166 | + const cardSlugs: string[] = []; | |
| 167 | + for (let offset = 0; offset < 1200; offset += 30) { | |
| 168 | + const list = await this.page(ctx, `${BASE}/card-lists/${setSlug}${offset ? `?offset=${offset}` : ''}`); | |
| 169 | + if (!list.success || !list.html) { | |
| 170 | + ctx.anomaly('page_fetch_failed', `list ${setSlug}@${offset}: ${list.error ?? list.httpStatus}`); | |
| 171 | + break; | |
| 172 | + } | |
| 173 | + const found = [...new Set([...list.html.matchAll(new RegExp(`href="/${setSlug}/([a-z0-9-]+)"`, 'g'))].map((m) => m[1]!))]; | |
| 174 | + const fresh = found.filter((s) => !cardSlugs.includes(s)); | |
| 175 | + if (!fresh.length) break; | |
| 176 | + cardSlugs.push(...fresh); | |
| 177 | + if (!list.html.includes(`offset=${offset + 30}`)) break; | |
| 178 | + } | |
| 179 | + for (const cardSlug of cardSlugs) { | |
| 180 | + if (ctx.signal?.aborted) return; | |
| 181 | + if (this.reached(ctx, count) || count >= maxCards) { | |
| 182 | + await ctx.setCursor({ setIdx, updatedAt: new Date().toISOString() }); | |
| 183 | + return; | |
| 184 | + } | |
| 185 | + const url = `${BASE}/${setSlug}/${cardSlug}`; | |
| 186 | + const res = await this.page(ctx, url); | |
| 187 | + if (!res.success || !res.html) { | |
| 188 | + ctx.anomaly('page_fetch_failed', `${url}: ${res.error ?? res.httpStatus}`); | |
| 189 | + continue; | |
| 190 | + } | |
| 191 | + const payload = parseCardPage(res.html, url); | |
| 192 | + if (!payload) { | |
| 193 | + ctx.anomaly('parse_failure_card', url); | |
| 194 | + continue; | |
| 195 | + } | |
| 196 | + count++; | |
| 197 | + yield { url, externalId: `${setSlug}/${cardSlug}`, kind: 'catalog_item', engine: res.engine, httpStatus: res.httpStatus, payload, fetchedAt: res.fetchedAt }; | |
| 198 | + } | |
| 199 | + await ctx.setCursor({ setIdx: setIdx + 1, updatedAt: new Date().toISOString() }); | |
| 200 | + } | |
| 201 | + await ctx.setCursor({ setIdx: 0, updatedAt: new Date().toISOString() }); | |
| 202 | + } | |
| 203 | + | |
| 204 | + async lookup(url: string, ctx: CrawlContext): Promise<RawRecordInput[]> { | |
| 205 | + if (!this.urlPatterns[0]!.test(url)) return []; | |
| 206 | + const res = await this.page(ctx, url); | |
| 207 | + if (!res.success || !res.html) return []; | |
| 208 | + const payload = parseCardPage(res.html, url); | |
| 209 | + if (!payload) return []; | |
| 210 | + return [{ url, externalId: `${payload.setSlug}/${payload.slug}`, kind: 'catalog_item', engine: res.engine, httpStatus: res.httpStatus, payload, fetchedAt: res.fetchedAt }]; | |
| 211 | + } | |
| 212 | + | |
| 213 | + async normalize(raw: RawRecordLike): Promise<NormalizedRecord[]> { | |
| 214 | + const p = PayloadSchema.parse(raw.payload); | |
| 215 | + const { set, variant } = setAndVariant(p.setName, p.tag); | |
| 216 | + const a = attrs({ | |
| 217 | + categorySlug: 'pokemon', | |
| 218 | + franchise: 'Pokémon', | |
| 219 | + brand: 'The Pokémon Company', | |
| 220 | + set, | |
| 221 | + name: p.name, | |
| 222 | + number: p.number, | |
| 223 | + year: null, | |
| 224 | + variant, | |
| 225 | + language: 'English', | |
| 226 | + identifiers: { pokemonprice_slug: `${p.setSlug}/${p.slug}` }, | |
| 227 | + metadata: { total: p.total, set_label: p.setName, tag: p.tag }, | |
| 228 | + }); | |
| 229 | + const images = p.image ? [p.image] : []; | |
| 230 | + const rawTitle = makeTitle({ name: p.name, set, number: p.number, total: p.total, variant }); | |
| 231 | + const observedAt = raw.fetchedAt; | |
| 232 | + const out: NormalizedRecord[] = [ | |
| 233 | + catalogItem({ kind: 'catalog_item', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: `${p.setSlug}/${p.slug}`, rawTitle, imageUrls: images, attributes: a, observedAt, confidence: 0.8, parserVersion: PARSER_VERSION, releaseDate: null }), | |
| 234 | + ]; | |
| 235 | + const kinds = new Set(((this.meta.config.priceKinds as string[] | undefined) ?? ['guide_value', 'low', 'high'])); | |
| 236 | + const txByGrade = new Map<string, number>(); | |
| 237 | + for (const t of p.transactions) txByGrade.set(t.grade, (txByGrade.get(t.grade) ?? 0) + t.count); | |
| 238 | + for (const row of p.rows) { | |
| 239 | + const { grader, grade } = parseCompactGrade(row.grade); | |
| 240 | + if (!grader && !/^raw$/i.test(row.grade)) continue; | |
| 241 | + const fair = num(row.fair_price); | |
| 242 | + if (!fair) continue; | |
| 243 | + const conf = Math.min(0.8, Math.max(0.2, (Number(row.confidence) || 40) / 100)); | |
| 244 | + // The fair price is the model's current estimate → dated by fetch day; the last sale date is kept in the title context only. | |
| 245 | + const obsDate = dayOf(observedAt); | |
| 246 | + const lastSale = isoDay(row.last_sale_date); | |
| 247 | + const sample = txByGrade.get(row.grade) ?? null; | |
| 248 | + const gradeLabel = grader === 'raw' ? 'Raw' : `${grader!.toUpperCase()} ${grade}`; | |
| 249 | + const title = `${rawTitle} — ${gradeLabel}${lastSale ? ` (last sale ${lastSale.toISOString().slice(0, 10)})` : ''}`; | |
| 250 | + const emit = (kind: 'guide_value' | 'low' | 'high', price: number | null) => { | |
| 251 | + if (!price || !kinds.has(kind)) return; | |
| 252 | + out.push(priceObservation({ kind: 'price_observation', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: `${p.setSlug}/${p.slug}:${row.grade}:${kind}`, rawTitle: title, imageUrls: images, attributes: a, grade: { grader: grader === 'raw' ? 'raw' : grader, grade, qualifier: null, certificationNumber: null }, observedAt, confidence: conf, parserVersion: PARSER_VERSION, priceKind: kind, price, currency: 'USD', observationDate: obsDate, sampleSize: sample })); | |
| 253 | + }; | |
| 254 | + emit('guide_value', fair); | |
| 255 | + emit('low', num(row.low_price)); | |
| 256 | + emit('high', num(row.high_price)); | |
| 257 | + } | |
| 258 | + return out; | |
| 259 | + } | |
| 260 | +} | |
| 261 | + | |
| 262 | +export default (meta: ConnectorMeta) => new PokemonPriceConnector(meta); | |
added
connectors/api/pokemonprice/meta.json
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +{ | |
| 2 | + "id": "pokemonprice", | |
| 3 | + "displayName": "PokemonPrice (graded Pokémon price guide)", | |
| 4 | + "sourceId": "pokemonprice", | |
| 5 | + "sourceName": "PokemonPrice.com", | |
| 6 | + "sourceType": "pricing_guide", | |
| 7 | + "sourceUrl": "https://www.pokemonprice.com", | |
| 8 | + "module": "api/pokemonprice", | |
| 9 | + "enginePriority": ["api", "firecrawl"], | |
| 10 | + "categories": ["pokemon"], | |
| 11 | + "regions": ["US"], | |
| 12 | + "languages": ["en"], | |
| 13 | + "currency": ["USD"], | |
| 14 | + "supportsListings": false, | |
| 15 | + "supportsSold": false, | |
| 16 | + "supportsAuctions": false, | |
| 17 | + "supportsImages": true, | |
| 18 | + "supportsCatalog": true, | |
| 19 | + "supportsPopulation": false, | |
| 20 | + "supportsLookup": true, | |
| 21 | + "refreshFrequencyMinutes": 2880, | |
| 22 | + "priority": "medium", | |
| 23 | + "trustScore": 0.7, | |
| 24 | + "attributionRequired": true, | |
| 25 | + "termsUrl": "https://www.pokemonprice.com/about-us", | |
| 26 | + "accessNotes": "Public server-rendered card pages (robots.txt allows '/' for generic agents; Content-Signal ai-train=no respected). Each card page embeds, in its React Server Components payload, a per-grade price model (Raw, PSA 1–10, BGS, CGC: fair/low/high price, model confidence, last sale date) derived from eBay and other public sold listings, plus monthly transaction counts. Emitted as price_observations 'guide_value' (+ low/high) per grade with the model confidence (capped 0.8) and the monthly transaction count as sampleSize — not transactions. Set naming follows pokemontcg ('Base Set', 'Jungle'…); edition suffixes (1st Edition, Shadowless) become the variant so assets merge with the API catalogs. ~167 set lists, ~25k card pages; politeness 1.5 s, `maxCardsPerRun` caps a run and the cursor rotates through sets.", | |
| 27 | + "enabled": true, | |
| 28 | + "schemaVersion": "1.0", | |
| 29 | + "config": { "seeds": [], "maxCardsPerRun": 300, "priceKinds": ["guide_value", "low", "high"] } | |
| 30 | +} | |
added
connectors/api/sorcery-tcg/index.test.ts
+21 −0
@@ -0,0 +1,21 @@ | ||
| 1 | +import { describe, expect, it } from 'vitest'; | |
| 2 | +import meta from './meta.json' with { type: 'json' }; | |
| 3 | +import { localMeta } from '../_lib/local-meta.js'; | |
| 4 | +import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; | |
| 5 | +import createConnector from './index.js'; | |
| 6 | + | |
| 7 | +const connector = createConnector(localMeta(meta)); | |
| 8 | + | |
| 9 | +describe('sorcery-tcg', () => { | |
| 10 | + runFixtureSuite(connector, it, expect); | |
| 11 | + | |
| 12 | + it('emits one catalog item per printing with finish-derived variants', async () => { | |
| 13 | + const [name] = listFixtures('sorcery-tcg'); | |
| 14 | + const out = await connector.normalize(loadFixture('sorcery-tcg', name!).raw); | |
| 15 | + expect(out.length).toBeGreaterThanOrEqual(2); | |
| 16 | + const variants = out.map((c) => (c.kind === 'catalog_item' ? c.attributes.variant : undefined)); | |
| 17 | + expect(variants).toContain(null); | |
| 18 | + expect(variants.some((v) => v && v.includes('Foil'))).toBe(true); | |
| 19 | + for (const c of out) if (c.kind === 'catalog_item') expect(c.attributes.identifiers.sorcery_printing_id).toBeTruthy(); | |
| 20 | + }); | |
| 21 | +}); | |
added
connectors/api/sorcery-tcg/index.ts
+94 −0
@@ -0,0 +1,94 @@ | ||
| 1 | +import { z } from 'zod'; | |
| 2 | +import { BaseConnector, type ConnectorMeta, type CrawlContext, type RawRecordInput, type RawRecordLike } from '@rareindex/connectors'; | |
| 3 | +import { type NormalizedRecord } from '@rareindex/shared'; | |
| 4 | +import { attrs, catalogItem, makeTitle, withRetries } from '../_lib/shared.js'; | |
| 5 | +import { UA_HEADERS } from '../_lib/tcg-shared.js'; | |
| 6 | + | |
| 7 | +/** Sorcery: Contested Realm — official catalog with per-printing finish/product/set data. */ | |
| 8 | +const API = 'https://api.sorcerytcg.com/api'; | |
| 9 | +const PARSER_VERSION = '1.0.0'; | |
| 10 | + | |
| 11 | +const PrintingSchema = z.object({ | |
| 12 | + id: z.string(), | |
| 13 | + slug: z.string(), | |
| 14 | + printedAt: z.string().nullable().optional(), | |
| 15 | + set: z.object({ name: z.string(), code: z.string().nullable().optional(), releasedAt: z.string().nullable().optional() }), | |
| 16 | + meta: z.object({ finish: z.string().nullable().optional(), product: z.string().nullable().optional(), artist: z.object({ name: z.string().nullable().optional() }).nullable().optional(), typeline: z.string().nullable().optional() }).loose(), | |
| 17 | +}); | |
| 18 | +const CardSchema = z.object({ | |
| 19 | + id: z.string(), | |
| 20 | + name: z.string(), | |
| 21 | + slug: z.string().nullable().optional(), | |
| 22 | + engine: z.object({ type: z.string().nullable().optional(), category: z.string().nullable().optional(), rarity: z.string().nullable().optional(), elements: z.array(z.string()).optional(), subtypes: z.array(z.string()).optional() }).loose().optional(), | |
| 23 | + printings: z.array(PrintingSchema).default([]), | |
| 24 | +}); | |
| 25 | +export type SorceryCard = z.infer<typeof CardSchema>; | |
| 26 | + | |
| 27 | +export function trimCard(raw: Record<string, unknown>): SorceryCard { | |
| 28 | + const engine = (raw.engine ?? {}) as Record<string, unknown>; | |
| 29 | + const printings = Array.isArray(raw.printings) ? (raw.printings as Record<string, unknown>[]).map((p) => { | |
| 30 | + const meta = (p.meta ?? {}) as Record<string, unknown>; | |
| 31 | + return { id: p.id, slug: p.slug, printedAt: p.printedAt, set: p.set, meta: { finish: meta.finish, product: meta.product, artist: meta.artist, typeline: meta.typeline } }; | |
| 32 | + }) : []; | |
| 33 | + return CardSchema.parse({ id: raw.id, name: raw.name, slug: raw.slug, engine: { type: engine.type, category: engine.category, rarity: engine.rarity, elements: engine.elements, subtypes: engine.subtypes }, printings }); | |
| 34 | +} | |
| 35 | + | |
| 36 | +export class SorceryConnector extends BaseConnector { | |
| 37 | + readonly version = '1.0.0'; | |
| 38 | + readonly parserVersion = PARSER_VERSION; | |
| 39 | + protected override minIntervalMs = 1000; | |
| 40 | + | |
| 41 | + async *crawl(ctx: CrawlContext): AsyncIterable<RawRecordInput> { | |
| 42 | + await this.throttle(); | |
| 43 | + const res = await withRetries(() => ctx.fetch(`${API}/cards`, { engines: ['api'], headers: UA_HEADERS, timeoutMs: 90_000 }), (r) => r.success && Array.isArray(r.json), 3, 3000); | |
| 44 | + if (!res.success || !Array.isArray(res.json)) throw new Error(`sorcery-tcg: catalog unavailable (${res.error ?? res.httpStatus})`); | |
| 45 | + let count = 0; | |
| 46 | + for (const raw of res.json as Record<string, unknown>[]) { | |
| 47 | + let card: SorceryCard; | |
| 48 | + try { | |
| 49 | + card = trimCard(raw); | |
| 50 | + } catch (err) { | |
| 51 | + ctx.anomaly('parse_failure_card', err instanceof Error ? err.message : String(err)); | |
| 52 | + continue; | |
| 53 | + } | |
| 54 | + if (ctx.options.seeds?.length && !card.printings.some((p) => ctx.options.seeds!.includes(p.set.code ?? p.set.name))) continue; | |
| 55 | + if (this.reached(ctx, count)) return; | |
| 56 | + count++; | |
| 57 | + yield { url: `https://curiosa.io/cards/${card.slug ?? card.id}`, externalId: card.id, kind: 'catalog_item', engine: 'api', httpStatus: res.httpStatus, payload: { card }, fetchedAt: res.fetchedAt }; | |
| 58 | + } | |
| 59 | + await ctx.setCursor({ updatedAt: new Date().toISOString(), cards: count }); | |
| 60 | + } | |
| 61 | + | |
| 62 | + async normalize(raw: RawRecordLike): Promise<NormalizedRecord[]> { | |
| 63 | + const { card } = z.object({ card: CardSchema }).parse(raw.payload); | |
| 64 | + const out: NormalizedRecord[] = []; | |
| 65 | + for (const p of card.printings) { | |
| 66 | + const finish = (p.meta.finish ?? 'Standard').trim(); | |
| 67 | + const product = (p.meta.product ?? 'Booster').trim(); | |
| 68 | + const variantParts: string[] = []; | |
| 69 | + if (/foil/i.test(finish)) variantParts.push('Foil'); | |
| 70 | + if (product && !/^booster$/i.test(product)) variantParts.push(product); | |
| 71 | + const variant = variantParts.length ? variantParts.join(' · ') : null; | |
| 72 | + const year = p.printedAt ? Number(p.printedAt.slice(0, 4)) || null : null; | |
| 73 | + const a = attrs({ | |
| 74 | + categorySlug: 'other_tcg', | |
| 75 | + franchise: 'Sorcery: Contested Realm', | |
| 76 | + brand: "Erik's Curiosa", | |
| 77 | + set: p.set.name, | |
| 78 | + setCode: p.set.code ?? null, | |
| 79 | + name: card.name, | |
| 80 | + number: null, | |
| 81 | + year, | |
| 82 | + variant, | |
| 83 | + language: 'English', | |
| 84 | + rarity: card.engine?.rarity ?? null, | |
| 85 | + identifiers: { sorcery_printing_id: p.id, sorcery_card_id: card.id, sorcery_printing_slug: p.slug }, | |
| 86 | + metadata: { type: card.engine?.type ?? null, category: card.engine?.category ?? null, elements: card.engine?.elements ?? [], subtypes: card.engine?.subtypes ?? [], finish, product, artist: p.meta.artist?.name ?? null, typeline: p.meta.typeline ?? null }, | |
| 87 | + }); | |
| 88 | + out.push(catalogItem({ kind: 'catalog_item', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: p.id, rawTitle: makeTitle({ name: card.name, set: p.set.name, year, variant }), imageUrls: [], attributes: a, observedAt: raw.fetchedAt, confidence: 0.9, parserVersion: PARSER_VERSION, releaseDate: p.printedAt ? new Date(p.printedAt) : null })); | |
| 89 | + } | |
| 90 | + return out; | |
| 91 | + } | |
| 92 | +} | |
| 93 | + | |
| 94 | +export default (meta: ConnectorMeta) => new SorceryConnector(meta); | |
added
connectors/api/sorcery-tcg/meta.json
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +{ | |
| 2 | + "id": "sorcery-tcg", | |
| 3 | + "displayName": "Sorcery: Contested Realm (official card API)", | |
| 4 | + "sourceId": "sorcery-tcg", | |
| 5 | + "sourceName": "Sorcery TCG (Erik's Curiosa)", | |
| 6 | + "sourceType": "manufacturer", | |
| 7 | + "sourceUrl": "https://sorcerytcg.com", | |
| 8 | + "module": "api/sorcery-tcg", | |
| 9 | + "enginePriority": ["api"], | |
| 10 | + "categories": ["other_tcg"], | |
| 11 | + "regions": ["US", "EU"], | |
| 12 | + "languages": ["en"], | |
| 13 | + "currency": [], | |
| 14 | + "supportsListings": false, | |
| 15 | + "supportsSold": false, | |
| 16 | + "supportsAuctions": false, | |
| 17 | + "supportsImages": false, | |
| 18 | + "supportsCatalog": true, | |
| 19 | + "supportsPopulation": false, | |
| 20 | + "supportsLookup": false, | |
| 21 | + "refreshFrequencyMinutes": 10080, | |
| 22 | + "priority": "low", | |
| 23 | + "trustScore": 0.85, | |
| 24 | + "attributionRequired": true, | |
| 25 | + "termsUrl": "https://sorcerytcg.com", | |
| 26 | + "accessNotes": "Official public JSON (https://api.sorcerytcg.com/api/cards, one call ≈1,100 cards with every printing: set code/name, finish Standard/Foil, product Booster/Precon/Promo, print date). Catalog only, no prices or images; TCGCSV supplies Sorcery prices via TCGplayer product names. One catalog_item per printing (finish → 'Foil' variant; non-booster product noted in metadata).", | |
| 27 | + "enabled": true, | |
| 28 | + "schemaVersion": "1.0", | |
| 29 | + "config": {} | |
| 30 | +} | |
added
connectors/api/swu-db/index.test.ts
+25 −0
@@ -0,0 +1,25 @@ | ||
| 1 | +import { describe, expect, it } from 'vitest'; | |
| 2 | +import meta from './meta.json' with { type: 'json' }; | |
| 3 | +import { localMeta } from '../_lib/local-meta.js'; | |
| 4 | +import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; | |
| 5 | +import createConnector from './index.js'; | |
| 6 | + | |
| 7 | +const connector = createConnector(localMeta(meta)); | |
| 8 | + | |
| 9 | +describe('swu-db', () => { | |
| 10 | + runFixtureSuite(connector, it, expect); | |
| 11 | + | |
| 12 | + it('emits star_wars_tcg catalog items with tcgplayer ids and USD observations', async () => { | |
| 13 | + const [name] = listFixtures('swu-db'); | |
| 14 | + const out = await connector.normalize(loadFixture('swu-db', name!).raw); | |
| 15 | + const cats = out.filter((r) => r.kind === 'catalog_item'); | |
| 16 | + expect(cats.length).toBeGreaterThanOrEqual(1); | |
| 17 | + for (const c of cats) { | |
| 18 | + if (c.kind !== 'catalog_item') continue; | |
| 19 | + expect(c.attributes.categorySlug).toBe('star_wars_tcg'); | |
| 20 | + expect(c.attributes.setCode).toMatch(/^[A-Z]{3}$/); | |
| 21 | + expect(c.attributes.number).toMatch(/^\d+$/); | |
| 22 | + } | |
| 23 | + for (const o of out) if (o.kind === 'price_observation') expect(o.currency).toBe('USD'); | |
| 24 | + }); | |
| 25 | +}); | |
added
connectors/api/swu-db/index.ts
+138 −0
@@ -0,0 +1,138 @@ | ||
| 1 | +import { z } from 'zod'; | |
| 2 | +import { BaseConnector, type ConnectorMeta, type CrawlContext, type RawRecordInput, type RawRecordLike } from '@rareindex/connectors'; | |
| 3 | +import { type NormalizedRecord } from '@rareindex/shared'; | |
| 4 | +import { attrs, catalogItem, makeTitle, num, priceObservation, withRetries } from '../_lib/shared.js'; | |
| 5 | +import { UA_HEADERS, dayOf } from '../_lib/tcg-shared.js'; | |
| 6 | + | |
| 7 | +/** SWU-DB — Star Wars: Unlimited catalog with relayed TCGplayer market prices. */ | |
| 8 | +const API = 'https://api.swu-db.com'; | |
| 9 | +const PARSER_VERSION = '1.0.0'; | |
| 10 | + | |
| 11 | +const CardSchema = z | |
| 12 | + .object({ | |
| 13 | + Set: z.string(), | |
| 14 | + Number: z.string(), | |
| 15 | + Name: z.string(), | |
| 16 | + Subtitle: z.string().nullable().optional(), | |
| 17 | + Type: z.string().nullable().optional(), | |
| 18 | + Rarity: z.string().nullable().optional(), | |
| 19 | + VariantType: z.string().nullable().optional(), | |
| 20 | + Unique: z.boolean().optional(), | |
| 21 | + Artist: z.string().nullable().optional(), | |
| 22 | + cid: z.string().nullable().optional(), | |
| 23 | + tcgplayerId: z.string().nullable().optional(), | |
| 24 | + MarketPrice: z.string().nullable().optional(), | |
| 25 | + LowPrice: z.string().nullable().optional(), | |
| 26 | + FoilPrice: z.string().nullable().optional(), | |
| 27 | + LowFoilPrice: z.string().nullable().optional(), | |
| 28 | + FrontArt: z.string().nullable().optional(), | |
| 29 | + Aspects: z.array(z.union([z.string(), z.record(z.string(), z.string())])).optional(), | |
| 30 | + }) | |
| 31 | + .loose(); | |
| 32 | +export type SwuCard = z.infer<typeof CardSchema>; | |
| 33 | + | |
| 34 | +export function trimCard(raw: Record<string, unknown>): SwuCard { | |
| 35 | + const keep = ['Set', 'Number', 'Name', 'Subtitle', 'Type', 'Rarity', 'VariantType', 'Unique', 'Artist', 'cid', 'tcgplayerId', 'MarketPrice', 'LowPrice', 'FoilPrice', 'LowFoilPrice', 'FrontArt', 'Aspects']; | |
| 36 | + const out: Record<string, unknown> = {}; | |
| 37 | + for (const k of keep) if (raw[k] !== undefined) out[k] = raw[k]; | |
| 38 | + return CardSchema.parse(out); | |
| 39 | +} | |
| 40 | + | |
| 41 | +const RawPayloadSchema = z.object({ card: CardSchema, setName: z.string().nullable() }); | |
| 42 | + | |
| 43 | +export class SwuDbConnector extends BaseConnector { | |
| 44 | + readonly version = '1.0.0'; | |
| 45 | + readonly parserVersion = PARSER_VERSION; | |
| 46 | + protected override minIntervalMs = 500; | |
| 47 | + override readonly urlPatterns = [/swu-db\.com\/cards?\/[a-z]{3}\/\d+/i]; | |
| 48 | + | |
| 49 | + private sets(): Record<string, string> { | |
| 50 | + return (this.meta.config.sets ?? {}) as Record<string, string>; | |
| 51 | + } | |
| 52 | + | |
| 53 | + async *crawl(ctx: CrawlContext): AsyncIterable<RawRecordInput> { | |
| 54 | + const sets = Object.entries(this.sets()).filter(([code]) => !ctx.options.seeds?.length || ctx.options.seeds.map((s) => s.toUpperCase()).includes(code)); | |
| 55 | + let setIdx = Number(ctx.options.cursor?.setIdx ?? 0); | |
| 56 | + let count = 0; | |
| 57 | + for (; setIdx < sets.length; setIdx++) { | |
| 58 | + const [code, setName] = sets[setIdx]!; | |
| 59 | + await this.throttle(); | |
| 60 | + const res = await withRetries(() => ctx.fetch(`${API}/cards/${code.toLowerCase()}`, { engines: ['api'], headers: UA_HEADERS, timeoutMs: 60_000 }), (r) => r.success && r.json !== null, 3, 2000); | |
| 61 | + const data = (res.json as { data?: unknown[] } | null)?.data; | |
| 62 | + if (!res.success || !Array.isArray(data)) { | |
| 63 | + ctx.anomaly('page_fetch_failed', `${code}: ${res.error ?? res.httpStatus}`); | |
| 64 | + continue; | |
| 65 | + } | |
| 66 | + for (const raw of data) { | |
| 67 | + let card: SwuCard; | |
| 68 | + try { | |
| 69 | + card = trimCard(raw as Record<string, unknown>); | |
| 70 | + } catch (err) { | |
| 71 | + ctx.anomaly('parse_failure_card', `${code}: ${err instanceof Error ? err.message : String(err)}`); | |
| 72 | + continue; | |
| 73 | + } | |
| 74 | + if (this.reached(ctx, count)) return; | |
| 75 | + count++; | |
| 76 | + yield { url: `https://www.swu-db.com/card/${card.Set}/${card.Number}`, externalId: `${card.Set}-${card.Number}`, kind: 'catalog_item', engine: 'api', httpStatus: res.httpStatus, payload: { card, setName }, fetchedAt: res.fetchedAt }; | |
| 77 | + } | |
| 78 | + await ctx.setCursor({ setIdx: setIdx + 1, updatedAt: new Date().toISOString() }); | |
| 79 | + } | |
| 80 | + await ctx.setCursor({ setIdx: 0, updatedAt: new Date().toISOString() }); | |
| 81 | + } | |
| 82 | + | |
| 83 | + async lookup(url: string, ctx: CrawlContext): Promise<RawRecordInput[]> { | |
| 84 | + const m = url.match(/swu-db\.com\/cards?\/([a-z]{3})\/(\d+)/i); | |
| 85 | + if (!m) return []; | |
| 86 | + const res = await ctx.fetch(`${API}/cards/${m[1]!.toLowerCase()}/${m[2]}`, { engines: ['api'], headers: UA_HEADERS }); | |
| 87 | + if (!res.success || !res.json) return []; | |
| 88 | + const card = trimCard(res.json as Record<string, unknown>); | |
| 89 | + return [{ url, externalId: `${card.Set}-${card.Number}`, kind: 'catalog_item', engine: 'api', httpStatus: res.httpStatus, payload: { card, setName: this.sets()[card.Set] ?? null }, fetchedAt: res.fetchedAt }]; | |
| 90 | + } | |
| 91 | + | |
| 92 | + async normalize(raw: RawRecordLike): Promise<NormalizedRecord[]> { | |
| 93 | + const { card, setName } = RawPayloadSchema.parse(raw.payload); | |
| 94 | + const identifiers: Record<string, string> = { swudb_id: `${card.Set}-${card.Number}` }; | |
| 95 | + if (card.cid) identifiers.swudb_cid = card.cid; | |
| 96 | + if (card.tcgplayerId) identifiers.tcgplayer_id = card.tcgplayerId; | |
| 97 | + const name = card.Subtitle ? `${card.Name} - ${card.Subtitle}` : card.Name; | |
| 98 | + const foilEntry = /F$/i.test(card.Number); | |
| 99 | + const number = foilEntry ? card.Number.replace(/F$/i, '') : card.Number; | |
| 100 | + const typeVariant = card.VariantType && card.VariantType !== 'Normal' && card.VariantType !== 'Foil' ? card.VariantType : null; | |
| 101 | + const baseVariant = foilEntry || card.VariantType === 'Foil' ? (typeVariant ? `${typeVariant} Foil` : 'Foil') : typeVariant; | |
| 102 | + const images = card.FrontArt ? [card.FrontArt] : []; | |
| 103 | + const observedAt = raw.fetchedAt; | |
| 104 | + const obsDate = dayOf(observedAt); | |
| 105 | + const build = (variant: string | null) => | |
| 106 | + attrs({ | |
| 107 | + categorySlug: 'star_wars_tcg', | |
| 108 | + franchise: 'Star Wars: Unlimited', | |
| 109 | + brand: 'Fantasy Flight Games', | |
| 110 | + set: setName ?? card.Set, | |
| 111 | + setCode: card.Set, | |
| 112 | + name, | |
| 113 | + number, | |
| 114 | + year: null, | |
| 115 | + variant, | |
| 116 | + language: 'English', | |
| 117 | + rarity: card.Rarity ?? null, | |
| 118 | + identifiers, | |
| 119 | + metadata: { type: card.Type, unique: card.Unique ?? null, artist: card.Artist, variant_type: card.VariantType ?? 'Normal', foil_price_hint: num(card.FoilPrice) }, | |
| 120 | + }); | |
| 121 | + const out: NormalizedRecord[] = []; | |
| 122 | + const emit = (variant: string | null, market: number | null, low: number | null) => { | |
| 123 | + const a = build(variant); | |
| 124 | + const rawTitle = makeTitle({ name, set: setName ?? card.Set, number, variant }); | |
| 125 | + out.push(catalogItem({ kind: 'catalog_item', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: `${card.Set}-${card.Number}${variant ? `:${variant}` : ''}`, rawTitle, imageUrls: images, attributes: a, observedAt, confidence: 0.85, parserVersion: PARSER_VERSION, releaseDate: null })); | |
| 126 | + for (const [kind, price] of [['market', market], ['low', low]] as const) { | |
| 127 | + if (!price) continue; | |
| 128 | + // Prices are relayed from TCGplayer without a timestamp → fetch-day observation, moderate confidence. | |
| 129 | + out.push(priceObservation({ kind: 'price_observation', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: `${card.Set}-${card.Number}:${variant ?? 'normal'}:${kind}`, rawTitle, imageUrls: images, attributes: a, observedAt, confidence: 0.7, parserVersion: PARSER_VERSION, priceKind: kind, price, currency: 'USD', observationDate: obsDate, sampleSize: null })); | |
| 130 | + } | |
| 131 | + }; | |
| 132 | + // Foil printings are separate API entries (Number "059F"); FoilPrice on the base entry is informational only. | |
| 133 | + emit(baseVariant, num(card.MarketPrice), num(card.LowPrice)); | |
| 134 | + return out; | |
| 135 | + } | |
| 136 | +} | |
| 137 | + | |
| 138 | +export default (meta: ConnectorMeta) => new SwuDbConnector(meta); | |
added
connectors/api/swu-db/meta.json
+32 −0
@@ -0,0 +1,32 @@ | ||
| 1 | +{ | |
| 2 | + "id": "swu-db", | |
| 3 | + "displayName": "SWU-DB (Star Wars: Unlimited)", | |
| 4 | + "sourceId": "swu-db", | |
| 5 | + "sourceName": "SWU-DB", | |
| 6 | + "sourceType": "catalog", | |
| 7 | + "sourceUrl": "https://www.swu-db.com", | |
| 8 | + "module": "api/swu-db", | |
| 9 | + "enginePriority": ["api"], | |
| 10 | + "categories": ["star_wars_tcg"], | |
| 11 | + "regions": ["US"], | |
| 12 | + "languages": ["en"], | |
| 13 | + "currency": ["USD"], | |
| 14 | + "supportsListings": false, | |
| 15 | + "supportsSold": false, | |
| 16 | + "supportsAuctions": false, | |
| 17 | + "supportsImages": true, | |
| 18 | + "supportsCatalog": true, | |
| 19 | + "supportsPopulation": false, | |
| 20 | + "supportsLookup": true, | |
| 21 | + "refreshFrequencyMinutes": 1440, | |
| 22 | + "priority": "medium", | |
| 23 | + "trustScore": 0.7, | |
| 24 | + "attributionRequired": true, | |
| 25 | + "termsUrl": "https://www.swu-db.com/api", | |
| 26 | + "accessNotes": "Free community API (https://api.swu-db.com/cards/<set>, no key). Each card carries TCGplayer ids and USD MarketPrice/LowPrice (+ FoilPrice/LowFoilPrice) relayed from TCGplayer without a timestamp → observations dated by the fetch day, confidence 0.7. Set codes are configured (the /catalog/sets endpoint needs an API key); VariantType (Normal, Hyperspace, Showcase…) becomes the variant, foil prices create a 'Foil' variant.", | |
| 27 | + "enabled": true, | |
| 28 | + "schemaVersion": "1.0", | |
| 29 | + "config": { | |
| 30 | + "sets": { "SOR": "Spark of Rebellion", "SHD": "Shadows of the Galaxy", "TWI": "Twilight of the Republic", "JTL": "Jump to Lightspeed", "LOF": "Legends of the Force", "SEC": "Secrets of Power" } | |
| 31 | + } | |
| 32 | +} | |
added
connectors/api/tcgcsv/_capture.ts
+22 −0
@@ -0,0 +1,22 @@ | ||
| 1 | +/** Targeted live fixture: Pokémon Base Set (group 604) cards with multiple price subtypes. */ | |
| 2 | +import { saveFixture } from '@rareindex/connectors/testing'; | |
| 3 | +import { trimProduct } from './index.js'; | |
| 4 | +const UA = { 'user-agent': 'RareIndexBot/0.1 (+https://www.rareindex.io/about/data)' }; | |
| 5 | +const get = async (u: string) => (await (await fetch(u, { headers: UA })).json()) as { results: Record<string, unknown>[] }; | |
| 6 | +const stamp = (await (await fetch('https://tcgcsv.com/last-updated.txt', { headers: UA })).text()).trim(); | |
| 7 | +const groups = await get('https://tcgcsv.com/tcgplayer/3/groups'); | |
| 8 | +const group = groups.results.find((g) => g.groupId === 604)!; | |
| 9 | +const products = await get('https://tcgcsv.com/tcgplayer/3/604/products'); | |
| 10 | +const prices = await get('https://tcgcsv.com/tcgplayer/3/604/prices'); | |
| 11 | +const category = { slug: 'pokemon', franchise: 'Pokémon', brand: 'The Pokémon Company', language: 'English' }; | |
| 12 | +for (const [name, match] of [['pokemon-base-charizard', /^Charizard/], ['pokemon-base-energy', /^Psychic Energy/]] as const) { | |
| 13 | + const p = products.results.find((x) => match.test(String(x.name)))!; | |
| 14 | + const product = trimProduct(p); | |
| 15 | + const rows = prices.results.filter((r) => r.productId === product.productId); | |
| 16 | + saveFixture('tcgcsv', name, { | |
| 17 | + raw: { url: String(p.url), externalId: String(product.productId), kind: 'catalog_item', engine: 'api', fetchedAt: new Date(), payload: { categoryId: 3, category, group: { groupId: group.groupId, name: group.name, abbreviation: group.abbreviation, isSupplemental: group.isSupplemental, publishedOn: group.publishedOn, categoryId: 3 }, product, prices: rows, updatedAt: stamp } }, | |
| 18 | + expect: { minCount: 2, kinds: ['catalog_item', 'price_observation'], requiredFields: ['attributes.identifiers.tcgplayer_id', 'attributes.set', 'attributes.number'] }, | |
| 19 | + note: `Live capture ${new Date().toISOString().slice(0, 10)} — tcgcsv.com/tcgplayer/3/604 (Base Set), subtypes ${[...new Set(rows.map((r) => r.subTypeName))].join('/')}`, | |
| 20 | + }); | |
| 21 | + console.log(name, product.productId, rows.map((r) => `${r.subTypeName}:${r.marketPrice}`).join(' ')); | |
| 22 | +} | |
added
connectors/api/tcgcsv/index.test.ts
+44 −0
@@ -0,0 +1,44 @@ | ||
| 1 | +import { describe, expect, it } from 'vitest'; | |
| 2 | +import meta from './meta.json' with { type: 'json' }; | |
| 3 | +import { localMeta } from '../_lib/local-meta.js'; | |
| 4 | +import { listFixtures, loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; | |
| 5 | +import createConnector from './index.js'; | |
| 6 | +import { variantFromSubType, splitNumber } from '../_lib/tcg-shared.js'; | |
| 7 | + | |
| 8 | +const connector = createConnector(localMeta(meta)); | |
| 9 | + | |
| 10 | +describe('tcgcsv', () => { | |
| 11 | + runFixtureSuite(connector, it, expect); | |
| 12 | + | |
| 13 | + it('maps TCGplayer subtypes to the shared variant vocabulary', () => { | |
| 14 | + expect(variantFromSubType('Normal')).toBeNull(); | |
| 15 | + expect(variantFromSubType('Holofoil')).toBe('Holo'); | |
| 16 | + expect(variantFromSubType('Reverse Holofoil')).toBe('Reverse Holo'); | |
| 17 | + expect(variantFromSubType('1st Edition Holofoil')).toBe('1st Edition Holo'); | |
| 18 | + expect(variantFromSubType('Foil')).toBe('Foil'); | |
| 19 | + expect(splitNumber('4/102')).toEqual({ number: '4', total: 102 }); | |
| 20 | + }); | |
| 21 | + | |
| 22 | + it('emits a catalog item per printing subtype and dated price observations', async () => { | |
| 23 | + const names = listFixtures('tcgcsv'); | |
| 24 | + const pokemon = names.find((n) => n.startsWith('pokemon')) ?? names[0]!; | |
| 25 | + const fx = loadFixture('tcgcsv', pokemon); | |
| 26 | + const out = await connector.normalize(fx.raw); | |
| 27 | + const cats = out.filter((r) => r.kind === 'catalog_item'); | |
| 28 | + const obs = out.filter((r) => r.kind === 'price_observation'); | |
| 29 | + expect(cats.length).toBeGreaterThanOrEqual(1); | |
| 30 | + expect(obs.length).toBeGreaterThanOrEqual(1); | |
| 31 | + for (const c of cats) { | |
| 32 | + if (c.kind !== 'catalog_item') continue; | |
| 33 | + expect(c.attributes.identifiers.tcgplayer_id).toMatch(/^\d+$/); | |
| 34 | + expect(c.attributes.set).toBeTruthy(); | |
| 35 | + } | |
| 36 | + for (const o of obs) { | |
| 37 | + if (o.kind !== 'price_observation') continue; | |
| 38 | + expect(o.currency).toBe('USD'); | |
| 39 | + expect(['market', 'low', 'mid', 'high']).toContain(o.priceKind); | |
| 40 | + // dated by the mirror's last-updated stamp, never in the future | |
| 41 | + expect(o.observationDate.getTime()).toBeLessThanOrEqual(Date.now()); | |
| 42 | + } | |
| 43 | + }); | |
| 44 | +}); | |
added
connectors/api/tcgcsv/index.ts
+189 −0
@@ -0,0 +1,189 @@ | ||
| 1 | +import { z } from 'zod'; | |
| 2 | +import { BaseConnector, type ConnectorMeta, type CrawlContext, type RawRecordInput, type RawRecordLike } from '@rareindex/connectors'; | |
| 3 | +import { type NormalizedRecord } from '@rareindex/shared'; | |
| 4 | +import { attrs, catalogItem, makeTitle, num, priceObservation, withRetries } from '../_lib/shared.js'; | |
| 5 | +import { UA_HEADERS, dayOf, isoDay, splitNumber, tcgplayerImage, variantFromSubType } from '../_lib/tcg-shared.js'; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * TCGCSV connector — public daily mirror of TCGplayer's catalog + prices for ~90 card games. | |
| 9 | + * One raw record per TCGplayer product (with its price rows); normalize emits one catalog_item per | |
| 10 | + * printing subtype and price_observations (market/low/…) dated with the mirror's last-updated stamp. | |
| 11 | + */ | |
| 12 | +const API = 'https://tcgcsv.com/tcgplayer'; | |
| 13 | +const PARSER_VERSION = '1.0.0'; | |
| 14 | + | |
| 15 | +const GroupSchema = z.object({ groupId: z.number(), name: z.string(), abbreviation: z.string().nullable().optional(), isSupplemental: z.boolean().optional(), publishedOn: z.string().nullable().optional(), categoryId: z.number().optional() }); | |
| 16 | +const ExtSchema = z.object({ name: z.string(), displayName: z.string().optional(), value: z.string() }); | |
| 17 | +const ProductSchema = z.object({ | |
| 18 | + productId: z.number(), | |
| 19 | + name: z.string(), | |
| 20 | + cleanName: z.string().optional(), | |
| 21 | + imageUrl: z.string().nullable().optional(), | |
| 22 | + url: z.string().optional(), | |
| 23 | + modifiedOn: z.string().optional(), | |
| 24 | + extendedData: z.array(ExtSchema).default([]), | |
| 25 | +}); | |
| 26 | +const PriceSchema = z.object({ | |
| 27 | + productId: z.number(), | |
| 28 | + lowPrice: z.number().nullable().optional(), | |
| 29 | + midPrice: z.number().nullable().optional(), | |
| 30 | + highPrice: z.number().nullable().optional(), | |
| 31 | + marketPrice: z.number().nullable().optional(), | |
| 32 | + directLowPrice: z.number().nullable().optional(), | |
| 33 | + subTypeName: z.string().nullable().optional(), | |
| 34 | +}); | |
| 35 | +const CategoryCfgSchema = z.object({ slug: z.string(), franchise: z.string().nullable(), brand: z.string().nullable(), language: z.string().optional() }); | |
| 36 | +const RawPayloadSchema = z.object({ | |
| 37 | + categoryId: z.number(), | |
| 38 | + category: CategoryCfgSchema, | |
| 39 | + group: GroupSchema, | |
| 40 | + product: ProductSchema, | |
| 41 | + prices: z.array(PriceSchema), | |
| 42 | + updatedAt: z.string().nullable(), | |
| 43 | +}); | |
| 44 | +export type TcgcsvPayload = z.infer<typeof RawPayloadSchema>; | |
| 45 | + | |
| 46 | +export function trimProduct(p: Record<string, unknown>) { | |
| 47 | + const keep = ['productId', 'name', 'cleanName', 'imageUrl', 'url', 'modifiedOn', 'extendedData']; | |
| 48 | + const out: Record<string, unknown> = {}; | |
| 49 | + for (const k of keep) if (p[k] !== undefined) out[k] = p[k]; | |
| 50 | + return ProductSchema.parse(out); | |
| 51 | +} | |
| 52 | + | |
| 53 | +export class TcgcsvConnector extends BaseConnector { | |
| 54 | + readonly version = '1.0.0'; | |
| 55 | + readonly parserVersion = PARSER_VERSION; | |
| 56 | + protected override minIntervalMs = 260; | |
| 57 | + | |
| 58 | + private categories(): Array<[number, z.infer<typeof CategoryCfgSchema>]> { | |
| 59 | + const cfg = (this.meta.config.categories ?? {}) as Record<string, unknown>; | |
| 60 | + return Object.entries(cfg).map(([id, c]) => [Number(id), CategoryCfgSchema.parse(c)] as [number, z.infer<typeof CategoryCfgSchema>]); | |
| 61 | + } | |
| 62 | + | |
| 63 | + private async get(ctx: CrawlContext, url: string) { | |
| 64 | + await this.throttle(); | |
| 65 | + return withRetries(() => ctx.fetch(url, { engines: ['api'], headers: UA_HEADERS }), (r) => r.success && (r.json !== null || Boolean(r.html)), 4, 1500); | |
| 66 | + } | |
| 67 | + | |
| 68 | + async *crawl(ctx: CrawlContext): AsyncIterable<RawRecordInput> { | |
| 69 | + const stamp = await this.get(ctx, 'https://tcgcsv.com/last-updated.txt'); | |
| 70 | + const updatedAt = typeof stamp.html === 'string' ? stamp.html.trim() : typeof stamp.json === 'string' ? String(stamp.json).trim() : null; | |
| 71 | + const cats = this.categories().filter(([id]) => !ctx.options.seeds?.length || ctx.options.seeds.includes(String(id)) || ctx.options.seeds.includes(String(id))); | |
| 72 | + const maxGroups = ctx.options.mode === 'backfill' ? Infinity : Number(this.meta.config.maxGroupsPerCategory ?? 40); | |
| 73 | + let catIdx = Number(ctx.options.cursor?.catIdx ?? 0); | |
| 74 | + let groupIdx = Number(ctx.options.cursor?.groupIdx ?? 0); | |
| 75 | + let count = 0; | |
| 76 | + for (; catIdx < cats.length; catIdx++, groupIdx = 0) { | |
| 77 | + const [categoryId, category] = cats[catIdx]!; | |
| 78 | + const gRes = await this.get(ctx, `${API}/${categoryId}/groups`); | |
| 79 | + const gJson = gRes.json as { results?: unknown[] } | null; | |
| 80 | + if (!gRes.success || !Array.isArray(gJson?.results)) { | |
| 81 | + ctx.anomaly('page_fetch_failed', `groups ${categoryId}: ${gRes.error ?? gRes.httpStatus}`); | |
| 82 | + continue; | |
| 83 | + } | |
| 84 | + const groups = z | |
| 85 | + .array(GroupSchema.loose()) | |
| 86 | + .parse(gJson.results) | |
| 87 | + .sort((a, b) => (b.publishedOn ?? '').localeCompare(a.publishedOn ?? '')) | |
| 88 | + .slice(0, maxGroups); | |
| 89 | + for (; groupIdx < groups.length; groupIdx++) { | |
| 90 | + if (ctx.signal?.aborted) return; | |
| 91 | + const group = GroupSchema.parse(groups[groupIdx]); | |
| 92 | + const pRes = await this.get(ctx, `${API}/${categoryId}/${group.groupId}/products`); | |
| 93 | + const products = (pRes.json as { results?: unknown[] } | null)?.results; | |
| 94 | + if (!pRes.success || !Array.isArray(products)) { | |
| 95 | + ctx.anomaly('page_fetch_failed', `products ${categoryId}/${group.groupId}: ${pRes.error ?? pRes.httpStatus}`); | |
| 96 | + continue; | |
| 97 | + } | |
| 98 | + const prRes = await this.get(ctx, `${API}/${categoryId}/${group.groupId}/prices`); | |
| 99 | + const priceRows = (prRes.json as { results?: unknown[] } | null)?.results; | |
| 100 | + const byProduct = new Map<number, z.infer<typeof PriceSchema>[]>(); | |
| 101 | + if (Array.isArray(priceRows)) { | |
| 102 | + for (const row of priceRows) { | |
| 103 | + const parsed = PriceSchema.safeParse(row); | |
| 104 | + if (!parsed.success) continue; | |
| 105 | + const list = byProduct.get(parsed.data.productId) ?? []; | |
| 106 | + list.push(parsed.data); | |
| 107 | + byProduct.set(parsed.data.productId, list); | |
| 108 | + } | |
| 109 | + } else { | |
| 110 | + ctx.anomaly('page_fetch_failed', `prices ${categoryId}/${group.groupId}: ${prRes.error ?? prRes.httpStatus}`); | |
| 111 | + } | |
| 112 | + for (const raw of products) { | |
| 113 | + let product: z.infer<typeof ProductSchema>; | |
| 114 | + try { | |
| 115 | + product = trimProduct(raw as Record<string, unknown>); | |
| 116 | + } catch (err) { | |
| 117 | + ctx.anomaly('parse_failure_product', `${group.groupId}: ${err instanceof Error ? err.message : String(err)}`); | |
| 118 | + continue; | |
| 119 | + } | |
| 120 | + if (this.reached(ctx, count)) return; | |
| 121 | + count++; | |
| 122 | + const payload: TcgcsvPayload = { categoryId, category, group, product, prices: byProduct.get(product.productId) ?? [], updatedAt }; | |
| 123 | + yield { url: product.url ?? `https://www.tcgplayer.com/product/${product.productId}`, externalId: String(product.productId), kind: 'catalog_item', engine: 'api', httpStatus: pRes.httpStatus, payload, fetchedAt: pRes.fetchedAt }; | |
| 124 | + } | |
| 125 | + await ctx.setCursor({ catIdx, groupIdx: groupIdx + 1, updatedAt }); | |
| 126 | + } | |
| 127 | + } | |
| 128 | + await ctx.setCursor({ catIdx: 0, groupIdx: 0, updatedAt, completedAt: new Date().toISOString() }); | |
| 129 | + } | |
| 130 | + | |
| 131 | + async normalize(raw: RawRecordLike): Promise<NormalizedRecord[]> { | |
| 132 | + const { category, group, product, prices, updatedAt } = RawPayloadSchema.parse(raw.payload); | |
| 133 | + const ext = new Map(product.extendedData.map((e) => [e.name.toLowerCase(), e.value])); | |
| 134 | + const { number, total } = splitNumber(ext.get('number') ?? null); | |
| 135 | + const rarity = ext.get('rarity') ?? null; | |
| 136 | + const isSealed = !number && !rarity; | |
| 137 | + const year = group.publishedOn ? Number(group.publishedOn.slice(0, 4)) || null : null; | |
| 138 | + const images = tcgplayerImage(product.imageUrl); | |
| 139 | + // "Charizard - 4/102" style names carry the number; keep the clean name. | |
| 140 | + const name = (product.cleanName ?? product.name).replace(/\s+-\s+[A-Za-z0-9]+\/[A-Za-z0-9]+$/, '').trim() || product.name; | |
| 141 | + const observedAt = raw.fetchedAt; | |
| 142 | + const obsDate = isoDay(updatedAt) ?? dayOf(observedAt); | |
| 143 | + const priceKinds = new Set(((this.meta.config.priceKinds as string[] | undefined) ?? ['market', 'low'])); | |
| 144 | + const build = (variant: string | null) => | |
| 145 | + attrs({ | |
| 146 | + categorySlug: category.slug, | |
| 147 | + franchise: category.franchise, | |
| 148 | + brand: category.brand, | |
| 149 | + set: group.name, | |
| 150 | + setCode: group.abbreviation ?? null, | |
| 151 | + name, | |
| 152 | + number, | |
| 153 | + year, | |
| 154 | + variant, | |
| 155 | + language: category.language ?? 'English', | |
| 156 | + rarity, | |
| 157 | + identifiers: { tcgplayer_id: String(product.productId), tcgplayer_group_id: String(group.groupId) }, | |
| 158 | + metadata: { sealed: isSealed, total, card_type: ext.get('card type') ?? ext.get('cardtype') ?? null, supplemental: group.isSupplemental ?? false, tcgplayer_url: product.url ?? null }, | |
| 159 | + }); | |
| 160 | + const out: NormalizedRecord[] = []; | |
| 161 | + const subTypes = prices.length ? [...new Set(prices.map((p) => p.subTypeName ?? 'Normal'))] : ['Normal']; | |
| 162 | + const seenVariants = new Set<string>(); | |
| 163 | + for (const subType of subTypes) { | |
| 164 | + const variant = variantFromSubType(subType); | |
| 165 | + const key = variant ?? ''; | |
| 166 | + const a = build(variant); | |
| 167 | + const rawTitle = makeTitle({ name, set: group.name, number, total, year, variant }); | |
| 168 | + if (!seenVariants.has(key)) { | |
| 169 | + seenVariants.add(key); | |
| 170 | + out.push(catalogItem({ kind: 'catalog_item', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: `${product.productId}${variant ? `:${variant}` : ''}`, rawTitle, imageUrls: images, attributes: a, observedAt, confidence: 0.85, parserVersion: PARSER_VERSION, releaseDate: group.publishedOn ? new Date(group.publishedOn) : null })); | |
| 171 | + } | |
| 172 | + const row = prices.find((p) => (p.subTypeName ?? 'Normal') === subType); | |
| 173 | + if (!row) continue; | |
| 174 | + const pairs: Array<['market' | 'low' | 'mid' | 'high', number | null]> = [ | |
| 175 | + ['market', num(row.marketPrice)], | |
| 176 | + ['low', num(row.lowPrice)], | |
| 177 | + ['mid', num(row.midPrice)], | |
| 178 | + ['high', num(row.highPrice)], | |
| 179 | + ]; | |
| 180 | + for (const [kind, price] of pairs) { | |
| 181 | + if (!price || !priceKinds.has(kind)) continue; | |
| 182 | + out.push(priceObservation({ kind: 'price_observation', connectorId: this.meta.id, sourceId: this.meta.sourceId, sourceUrl: raw.url, externalId: `${product.productId}:${subType}:${kind}`, rawTitle, imageUrls: images, attributes: a, observedAt, confidence: 0.75, parserVersion: PARSER_VERSION, priceKind: kind, price, currency: 'USD', observationDate: obsDate, sampleSize: null })); | |
| 183 | + } | |
| 184 | + } | |
| 185 | + return out; | |
| 186 | + } | |
| 187 | +} | |
| 188 | + | |
| 189 | +export default (meta: ConnectorMeta) => new TcgcsvConnector(meta); | |
added
connectors/api/tcgcsv/meta.json
+60 −0
@@ -0,0 +1,60 @@ | ||
| 1 | +{ | |
| 2 | + "id": "tcgcsv", | |
| 3 | + "displayName": "TCGCSV (TCGplayer catalog & prices)", | |
| 4 | + "sourceId": "tcgcsv", | |
| 5 | + "sourceName": "TCGCSV", | |
| 6 | + "sourceType": "pricing_guide", | |
| 7 | + "sourceUrl": "https://tcgcsv.com", | |
| 8 | + "module": "api/tcgcsv", | |
| 9 | + "enginePriority": ["api"], | |
| 10 | + "categories": ["trading_cards", "pokemon", "magic_the_gathering", "yugioh", "disney_lorcana", "one_piece_card_game", "digimon_tcg", "flesh_and_blood", "star_wars_tcg", "dragon_ball_tcg", "weiss_schwarz", "final_fantasy_tcg", "other_tcg", "funko"], | |
| 11 | + "regions": ["US"], | |
| 12 | + "languages": ["en", "ja"], | |
| 13 | + "currency": ["USD"], | |
| 14 | + "supportsListings": false, | |
| 15 | + "supportsSold": false, | |
| 16 | + "supportsAuctions": false, | |
| 17 | + "supportsImages": true, | |
| 18 | + "supportsCatalog": true, | |
| 19 | + "supportsPopulation": false, | |
| 20 | + "supportsLookup": false, | |
| 21 | + "refreshFrequencyMinutes": 1440, | |
| 22 | + "priority": "high", | |
| 23 | + "trustScore": 0.8, | |
| 24 | + "attributionRequired": true, | |
| 25 | + "termsUrl": "https://tcgcsv.com/faq", | |
| 26 | + "accessNotes": "Public daily mirror of TCGplayer's catalog and price API (JSON endpoints /tcgplayer/{categoryId}/groups, /{groupId}/products, /{groupId}/prices; robots.txt allows everything and the FAQ explicitly invites programmatic access with an identifying User-Agent and ~4 req/s). Prices (low/mid/high/market per printing subtype) are TCGplayer market data, not transactions: stored as price_observations dated with the mirror's last-updated timestamp (/last-updated.txt), confidence 0.75. Categories are mapped to taxonomy slugs in config; incremental runs cover the newest `maxGroupsPerCategory` sets per category, backfill covers all. Identifiers: tcgplayer_id (product) + tcgplayer_group_id. Historical price archives (2024-02-08 →) exist as daily 7z files and can be backfilled later.", | |
| 27 | + "enabled": true, | |
| 28 | + "schemaVersion": "1.0", | |
| 29 | + "config": { | |
| 30 | + "categories": { | |
| 31 | + "3": { "slug": "pokemon", "franchise": "Pokémon", "brand": "The Pokémon Company", "language": "English" }, | |
| 32 | + "85": { "slug": "pokemon", "franchise": "Pokémon", "brand": "The Pokémon Company", "language": "Japanese" }, | |
| 33 | + "1": { "slug": "magic_the_gathering", "franchise": "Magic: The Gathering", "brand": "Wizards of the Coast" }, | |
| 34 | + "2": { "slug": "yugioh", "franchise": "Yu-Gi-Oh!", "brand": "Konami" }, | |
| 35 | + "71": { "slug": "disney_lorcana", "franchise": "Disney Lorcana", "brand": "Ravensburger" }, | |
| 36 | + "68": { "slug": "one_piece_card_game", "franchise": "One Piece", "brand": "Bandai" }, | |
| 37 | + "63": { "slug": "digimon_tcg", "franchise": "Digimon", "brand": "Bandai" }, | |
| 38 | + "62": { "slug": "flesh_and_blood", "franchise": "Flesh and Blood", "brand": "Legend Story Studios" }, | |
| 39 | + "79": { "slug": "star_wars_tcg", "franchise": "Star Wars: Unlimited", "brand": "Fantasy Flight Games" }, | |
| 40 | + "80": { "slug": "dragon_ball_tcg", "franchise": "Dragon Ball Super Fusion World", "brand": "Bandai" }, | |
| 41 | + "27": { "slug": "dragon_ball_tcg", "franchise": "Dragon Ball Super Card Game", "brand": "Bandai" }, | |
| 42 | + "20": { "slug": "weiss_schwarz", "franchise": "Weiß Schwarz", "brand": "Bushiroad" }, | |
| 43 | + "24": { "slug": "final_fantasy_tcg", "franchise": "Final Fantasy TCG", "brand": "Square Enix" }, | |
| 44 | + "74": { "slug": "other_tcg", "franchise": "Grand Archive", "brand": "Weebs of the Shore" }, | |
| 45 | + "77": { "slug": "other_tcg", "franchise": "Sorcery: Contested Realm", "brand": "Erik's Curiosa" }, | |
| 46 | + "81": { "slug": "other_tcg", "franchise": "Union Arena", "brand": "Bandai" }, | |
| 47 | + "86": { "slug": "other_tcg", "franchise": "Gundam Card Game", "brand": "Bandai" }, | |
| 48 | + "89": { "slug": "other_tcg", "franchise": "Riftbound", "brand": "Riot Games" }, | |
| 49 | + "16": { "slug": "other_tcg", "franchise": "Cardfight!! Vanguard", "brand": "Bushiroad" }, | |
| 50 | + "17": { "slug": "other_tcg", "franchise": "Force of Will", "brand": "Force of Will Co." }, | |
| 51 | + "59": { "slug": "other_tcg", "franchise": "KeyForge", "brand": "Ghost Galaxy" }, | |
| 52 | + "66": { "slug": "other_tcg", "franchise": "MetaZoo", "brand": "MetaZoo Games" }, | |
| 53 | + "72": { "slug": "other_tcg", "franchise": "Battle Spirits Saga", "brand": "Bandai" }, | |
| 54 | + "25": { "slug": "other_tcg", "franchise": "UniVersus", "brand": "UVS Games" }, | |
| 55 | + "29": { "slug": "funko", "franchise": null, "brand": "Funko" } | |
| 56 | + }, | |
| 57 | + "maxGroupsPerCategory": 40, | |
| 58 | + "priceKinds": ["market", "low"] | |
| 59 | + } | |
| 60 | +} | |
modified
connectors/registry.json
+2762 −355
@@ -1,6 +1,263 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "version": "1.0", |
| 3 | 3 | "connectors": [ |
| 4 | + { | |
| 5 | + "id": "amiami", | |
| 6 | + "displayName": "AmiAmi (figures & hobby — new + pre-owned, JPY)", | |
| 7 | + "sourceId": "amiami", | |
| 8 | + "sourceName": "AmiAmi", | |
| 9 | + "sourceType": "marketplace", | |
| 10 | + "sourceUrl": "https://www.amiami.com", | |
| 11 | + "module": "scrapfly/amiami", | |
| 12 | + "enginePriority": [ | |
| 13 | + "scrapfly" | |
| 14 | + ], | |
| 15 | + "categories": [ | |
| 16 | + "action_figures", | |
| 17 | + "gundam", | |
| 18 | + "plush", | |
| 19 | + "designer_toys" | |
| 20 | + ], | |
| 21 | + "regions": [ | |
| 22 | + "JP" | |
| 23 | + ], | |
| 24 | + "languages": [ | |
| 25 | + "en", | |
| 26 | + "ja" | |
| 27 | + ], | |
| 28 | + "currency": [ | |
| 29 | + "JPY" | |
| 30 | + ], | |
| 31 | + "supportsListings": true, | |
| 32 | + "supportsSold": false, | |
| 33 | + "supportsAuctions": false, | |
| 34 | + "supportsImages": true, | |
| 35 | + "supportsCatalog": true, | |
| 36 | + "supportsPopulation": false, | |
| 37 | + "supportsLookup": true, | |
| 38 | + "refreshFrequencyMinutes": 720, | |
| 39 | + "priority": "medium", | |
| 40 | + "trustScore": 0.8, | |
| 41 | + "attributionRequired": true, | |
| 42 | + "termsUrl": "https://www.amiami.com/eng/guide/", | |
| 43 | + "accessNotes": "Reads the public JSON API that AmiAmi's own storefront calls (api.amiami.com/api/v1.0/items with the storefront's X-User-Key header, no account or login). Plain requests from data-centre IPs get a Cloudflare 'Attention Required' page, so the request is routed through Scrapfly (asp, no JS, country jp; 1 credit per 50-item page). Data: item code, JAN, maker, release date, list price (tax-in), current sell price (pre-owned range min/max or new price), stock and pre-owned flags — all JPY. Emits catalog items + fixed-price listings; pre-owned condition comes from the storefront's ITEM/BOX grades when present in the title. No purchase, cart or account endpoints are touched.", | |
| 44 | + "enabled": true, | |
| 45 | + "schemaVersion": "1.0", | |
| 46 | + "config": { | |
| 47 | + "seeds": [ | |
| 48 | + { | |
| 49 | + "params": "s_cate_tag=14&s_st_condition_flg=1", | |
| 50 | + "categorySlug": "action_figures", | |
| 51 | + "label": "Figures — pre-owned" | |
| 52 | + }, | |
| 53 | + { | |
| 54 | + "params": "s_cate_tag=14&s_st_condition_flg=0", | |
| 55 | + "categorySlug": "action_figures", | |
| 56 | + "label": "Figures — new" | |
| 57 | + }, | |
| 58 | + { | |
| 59 | + "params": "s_keywords=Gundam&s_st_condition_flg=1", | |
| 60 | + "categorySlug": "gundam", | |
| 61 | + "label": "Gundam — pre-owned" | |
| 62 | + }, | |
| 63 | + { | |
| 64 | + "params": "s_keywords=Nendoroid", | |
| 65 | + "categorySlug": "action_figures", | |
| 66 | + "label": "Nendoroid" | |
| 67 | + } | |
| 68 | + ], | |
| 69 | + "pagesPerSeed": 2, | |
| 70 | + "pageSize": 50 | |
| 71 | + } | |
| 72 | + }, | |
| 73 | + { | |
| 74 | + "id": "antiquorum", | |
| 75 | + "displayName": "Antiquorum (watch auction results & upcoming lots)", | |
| 76 | + "sourceId": "antiquorum", | |
| 77 | + "sourceName": "Antiquorum", | |
| 78 | + "sourceType": "auction_house", | |
| 79 | + "sourceUrl": "https://catalog.antiquorum.swiss", | |
| 80 | + "module": "api/antiquorum", | |
| 81 | + "enginePriority": [ | |
| 82 | + "api", | |
| 83 | + "firecrawl" | |
| 84 | + ], | |
| 85 | + "categories": [ | |
| 86 | + "watches", | |
| 87 | + "rolex", | |
| 88 | + "patek_philippe", | |
| 89 | + "audemars_piguet", | |
| 90 | + "omega", | |
| 91 | + "other_watches", | |
| 92 | + "jewelry" | |
| 93 | + ], | |
| 94 | + "regions": [ | |
| 95 | + "CH", | |
| 96 | + "HK", | |
| 97 | + "MC" | |
| 98 | + ], | |
| 99 | + "languages": [ | |
| 100 | + "en" | |
| 101 | + ], | |
| 102 | + "currency": [ | |
| 103 | + "CHF", | |
| 104 | + "HKD", | |
| 105 | + "EUR", | |
| 106 | + "USD" | |
| 107 | + ], | |
| 108 | + "supportsListings": false, | |
| 109 | + "supportsSold": true, | |
| 110 | + "supportsAuctions": true, | |
| 111 | + "supportsImages": true, | |
| 112 | + "supportsCatalog": false, | |
| 113 | + "supportsPopulation": false, | |
| 114 | + "supportsLookup": true, | |
| 115 | + "refreshFrequencyMinutes": 1440, | |
| 116 | + "priority": "high", | |
| 117 | + "trustScore": 0.9, | |
| 118 | + "attributionRequired": true, | |
| 119 | + "termsUrl": "https://www.antiquorum.swiss/conditions-of-sale/", | |
| 120 | + "accessNotes": "Public auction catalogue (catalog.antiquorum.swiss/en → /en/auctions/<slug>/lots?page=N) fetched over plain HTTPS with the RareIndex user agent; robots.txt only disallows /wp-admin/. Each lot carries schema.org RDFa (name, sku, brand, price, currency, image) and a spec table (Brand, Model, Reference, Year, Material). Sold lots show 'Sold: <CCY> <amount>' which Antiquorum's own price lists confirm is hammer + buyer's premium (e.g. lot 387-1: hammer 230,000 → sold 287,500 HKD), so buyerPremiumIncluded=true. Sale date = the auction date on the catalogue home. Unsold/upcoming lots become auction lots with estimates. 2 s between pages.", | |
| 121 | + "enabled": true, | |
| 122 | + "schemaVersion": "1.0", | |
| 123 | + "config": { | |
| 124 | + "auctionsPerRun": 3, | |
| 125 | + "maxPagesPerAuction": 25 | |
| 126 | + } | |
| 127 | + }, | |
| 128 | + { | |
| 129 | + "id": "apmex", | |
| 130 | + "displayName": "APMEX (coins & bullion dealer, USD asks)", | |
| 131 | + "sourceId": "apmex", | |
| 132 | + "sourceName": "APMEX", | |
| 133 | + "sourceType": "dealer", | |
| 134 | + "sourceUrl": "https://www.apmex.com", | |
| 135 | + "module": "firecrawl/apmex", | |
| 136 | + "enginePriority": [ | |
| 137 | + "firecrawl", | |
| 138 | + "scrapfly" | |
| 139 | + ], | |
| 140 | + "categories": [ | |
| 141 | + "coins" | |
| 142 | + ], | |
| 143 | + "regions": [ | |
| 144 | + "US" | |
| 145 | + ], | |
| 146 | + "languages": [ | |
| 147 | + "en" | |
| 148 | + ], | |
| 149 | + "currency": [ | |
| 150 | + "USD" | |
| 151 | + ], | |
| 152 | + "supportsListings": true, | |
| 153 | + "supportsSold": false, | |
| 154 | + "supportsAuctions": false, | |
| 155 | + "supportsImages": true, | |
| 156 | + "supportsCatalog": true, | |
| 157 | + "supportsPopulation": false, | |
| 158 | + "supportsLookup": false, | |
| 159 | + "refreshFrequencyMinutes": 1440, | |
| 160 | + "priority": "low", | |
| 161 | + "trustScore": 0.85, | |
| 162 | + "attributionRequired": true, | |
| 163 | + "termsUrl": "https://www.apmex.com/terms-and-conditions", | |
| 164 | + "accessNotes": "Public category grids (apmex.com/category/<id>/<slug>) of one of the largest US coin dealers; robots.txt disallows /catalog/, /cart/, /account/, /spotprice/ etc., none of which are used. Plain HTTPS gets an Akamai 403 for non-browser clients, so pages are fetched with Firecrawl (1 credit per ~80-product page). Parsed per product card: APMEX product id, title, 'As Low As' price (USD, cash/wire tier), stock message, image. Year, mint mark, grade and grading service (PCGS/NGC/ANACS/CAC) are parsed from the title; graded coins become grader/grade variants, raw/BU coins stay raw. Emits catalog items + dealer fixed-price listings (asks, not transactions).", | |
| 165 | + "enabled": true, | |
| 166 | + "schemaVersion": "1.0", | |
| 167 | + "config": { | |
| 168 | + "seeds": [ | |
| 169 | + { | |
| 170 | + "url": "https://www.apmex.com/category/11903/10-liberty-eagle-coins-1795-1907", | |
| 171 | + "series": "$10 Liberty Gold Eagle", | |
| 172 | + "country": "US" | |
| 173 | + }, | |
| 174 | + { | |
| 175 | + "url": "https://www.apmex.com/category/11904/20-liberty-double-eagle-coins-1849-1907", | |
| 176 | + "series": "$20 Liberty Double Eagle", | |
| 177 | + "country": "US" | |
| 178 | + }, | |
| 179 | + { | |
| 180 | + "url": "https://www.apmex.com/category/11905/20-saint-gaudens-double-eagle-coins-1907-1933", | |
| 181 | + "series": "$20 Saint-Gaudens Double Eagle", | |
| 182 | + "country": "US" | |
| 183 | + }, | |
| 184 | + { | |
| 185 | + "url": "https://www.apmex.com/category/25310/morgan-dollars-1878-1921", | |
| 186 | + "series": "Morgan Dollar", | |
| 187 | + "country": "US" | |
| 188 | + }, | |
| 189 | + { | |
| 190 | + "url": "https://www.apmex.com/category/25320/peace-dollars-1921-1935", | |
| 191 | + "series": "Peace Dollar", | |
| 192 | + "country": "US" | |
| 193 | + }, | |
| 194 | + { | |
| 195 | + "url": "https://www.apmex.com/category/11440/american-gold-eagles", | |
| 196 | + "series": "American Gold Eagle", | |
| 197 | + "country": "US" | |
| 198 | + } | |
| 199 | + ], | |
| 200 | + "pagesPerSeed": 1 | |
| 201 | + } | |
| 202 | + }, | |
| 203 | + { | |
| 204 | + "id": "artcurial", | |
| 205 | + "displayName": "Artcurial (sale results)", | |
| 206 | + "sourceId": "artcurial", | |
| 207 | + "sourceName": "Artcurial", | |
| 208 | + "sourceType": "auction_house", | |
| 209 | + "sourceUrl": "https://www.artcurial.com", | |
| 210 | + "module": "firecrawl/artcurial", | |
| 211 | + "enginePriority": [ | |
| 212 | + "firecrawl", | |
| 213 | + "scrapfly" | |
| 214 | + ], | |
| 215 | + "categories": [ | |
| 216 | + "art", | |
| 217 | + "contemporary_art", | |
| 218 | + "photography", | |
| 219 | + "design_furniture", | |
| 220 | + "jewelry", | |
| 221 | + "other_watches", | |
| 222 | + "luxury_handbags", | |
| 223 | + "wine", | |
| 224 | + "comics", | |
| 225 | + "books", | |
| 226 | + "automobiles", | |
| 227 | + "automotive_memorabilia", | |
| 228 | + "antiques" | |
| 229 | + ], | |
| 230 | + "regions": [ | |
| 231 | + "FR", | |
| 232 | + "MC" | |
| 233 | + ], | |
| 234 | + "languages": [ | |
| 235 | + "en", | |
| 236 | + "fr" | |
| 237 | + ], | |
| 238 | + "currency": [ | |
| 239 | + "EUR" | |
| 240 | + ], | |
| 241 | + "supportsListings": false, | |
| 242 | + "supportsSold": true, | |
| 243 | + "supportsAuctions": true, | |
| 244 | + "supportsImages": true, | |
| 245 | + "supportsCatalog": false, | |
| 246 | + "supportsPopulation": false, | |
| 247 | + "supportsLookup": false, | |
| 248 | + "refreshFrequencyMinutes": 1440, | |
| 249 | + "priority": "medium", | |
| 250 | + "trustScore": 0.9, | |
| 251 | + "attributionRequired": true, | |
| 252 | + "termsUrl": "https://www.artcurial.com/en/conditions-of-sale", | |
| 253 | + "accessNotes": "Paris/Monaco auction house (Nuxt app). The public results index (/en/results-auctions-sales) and each sale page (/en/sales/<n>) are rendered through Firecrawl (JS wait ~5 s, 1 credit each); the sale page lists every lot with lot number, title, sub-title, estimate and 'Sold €X' plus the public lot URL. robots.txt disallows press, tracking and /motorcars paths only (car sales are listed under /en/sales/<n>, which is allowed). Artcurial does not state on the results list whether the published price includes buyer's premium, so buyer_premium_included is left null (unknown, never assumed). Sale date = the sale date on the results index. 3 s politeness delay; salesPerRun caps each run.", | |
| 254 | + "enabled": true, | |
| 255 | + "schemaVersion": "1.0", | |
| 256 | + "config": { | |
| 257 | + "resultsUrl": "https://www.artcurial.com/en/results-auctions-sales", | |
| 258 | + "salesPerRun": 3 | |
| 259 | + } | |
| 260 | + }, | |
| 4 | 261 | { |
| 5 | 262 | "id": "aucfree", |
| 6 | 263 | "displayName": "aucfree (Yahoo! Auctions Japan closed lots)", |
@@ -92,6 +349,66 @@ | ||
| 92 | 349 | "pagesPerSeed": 2 |
| 93 | 350 | } |
| 94 | 351 | }, |
| 352 | + { | |
| 353 | + "id": "bobs-watches", | |
| 354 | + "displayName": "Bob's Watches (pre-owned Rolex listings)", | |
| 355 | + "sourceId": "bobs-watches", | |
| 356 | + "sourceName": "Bob's Watches", | |
| 357 | + "sourceType": "dealer", | |
| 358 | + "sourceUrl": "https://www.bobswatches.com", | |
| 359 | + "module": "api/bobs-watches", | |
| 360 | + "enginePriority": [ | |
| 361 | + "api", | |
| 362 | + "firecrawl" | |
| 363 | + ], | |
| 364 | + "categories": [ | |
| 365 | + "rolex", | |
| 366 | + "watches", | |
| 367 | + "other_watches" | |
| 368 | + ], | |
| 369 | + "regions": [ | |
| 370 | + "US" | |
| 371 | + ], | |
| 372 | + "languages": [ | |
| 373 | + "en" | |
| 374 | + ], | |
| 375 | + "currency": [ | |
| 376 | + "USD" | |
| 377 | + ], | |
| 378 | + "supportsListings": true, | |
| 379 | + "supportsSold": false, | |
| 380 | + "supportsAuctions": false, | |
| 381 | + "supportsImages": true, | |
| 382 | + "supportsCatalog": false, | |
| 383 | + "supportsPopulation": false, | |
| 384 | + "supportsLookup": true, | |
| 385 | + "refreshFrequencyMinutes": 720, | |
| 386 | + "priority": "medium", | |
| 387 | + "trustScore": 0.8, | |
| 388 | + "attributionRequired": true, | |
| 389 | + "termsUrl": "https://www.bobswatches.com/terms-and-conditions", | |
| 390 | + "accessNotes": "Public model-family pages (bobswatches.com/rolex-<model>) fetched over plain HTTPS with the RareIndex user agent; robots.txt allows them (sort/query/search URLs are disallowed and never used). Each page embeds schema.org Product JSON-LD per watch (name, mpn = reference, sku = stock id, colour/material, USD price, availability). Listings only; Bob's does not publish realised prices. 1.5 s between pages.", | |
| 391 | + "enabled": true, | |
| 392 | + "schemaVersion": "1.0", | |
| 393 | + "config": { | |
| 394 | + "seeds": [ | |
| 395 | + "rolex-submariner", | |
| 396 | + "rolex-daytona", | |
| 397 | + "rolex-gmt-master-ii", | |
| 398 | + "rolex-datejust", | |
| 399 | + "rolex-day-date", | |
| 400 | + "rolex-explorer", | |
| 401 | + "rolex-sea-dweller", | |
| 402 | + "rolex-yacht-master", | |
| 403 | + "rolex-oyster-perpetual", | |
| 404 | + "rolex-sky-dweller", | |
| 405 | + "rolex-milgauss", | |
| 406 | + "rolex-air-king", | |
| 407 | + "vintage-rolex", | |
| 408 | + "used-rolex-watches" | |
| 409 | + ] | |
| 410 | + } | |
| 411 | + }, | |
| 95 | 412 | { |
| 96 | 413 | "id": "bonhams", |
| 97 | 414 | "displayName": "Bonhams (auction results & upcoming lots)", |
@@ -333,44 +650,138 @@ | ||
| 333 | 650 | } |
| 334 | 651 | }, |
| 335 | 652 | { |
| 336 | − "id": "catawiki", | |
| 337 | − "displayName": "Catawiki (closed-lot results & live lots)", | |
| 338 | − "sourceId": "catawiki", | |
| 339 | − "sourceName": "Catawiki", | |
| 653 | + "id": "bring-a-trailer", | |
| 654 | + "displayName": "Bring a Trailer (auction results)", | |
| 655 | + "sourceId": "bring-a-trailer", | |
| 656 | + "sourceName": "Bring a Trailer", | |
| 340 | 657 | "sourceType": "auction_house", |
| 341 | − "sourceUrl": "https://www.catawiki.com", | |
| 342 | − "module": "scrapfly/catawiki", | |
| 658 | + "sourceUrl": "https://bringatrailer.com", | |
| 659 | + "module": "api/bring-a-trailer", | |
| 343 | 660 | "enginePriority": [ |
| 344 | − "scrapfly" | |
| 661 | + "api" | |
| 345 | 662 | ], |
| 346 | 663 | "categories": [ |
| 347 | − "pokemon", | |
| 348 | − "magic_the_gathering", | |
| 349 | − "yugioh", | |
| 350 | − "one_piece_card_game", | |
| 351 | − "disney_lorcana", | |
| 352 | − "other_tcg", | |
| 353 | − "basketball_cards", | |
| 354 | − "baseball_cards", | |
| 355 | − "football_cards", | |
| 356 | − "hockey_cards", | |
| 357 | − "soccer_cards", | |
| 358 | − "f1_cards", | |
| 359 | − "non_sport_cards", | |
| 360 | − "rolex", | |
| 361 | − "patek_philippe", | |
| 362 | − "audemars_piguet", | |
| 363 | − "omega", | |
| 364 | − "other_watches", | |
| 365 | − "pens", | |
| 366 | − "lighters", | |
| 367 | − "marvel_comics", | |
| 368 | − "dc_comics", | |
| 369 | − "independent_comics", | |
| 370 | − "manga", | |
| 371 | − "animation_art", | |
| 372 | − "lego_sets", | |
| 373 | − "funko", | |
| 664 | + "automobiles", | |
| 665 | + "motorcycles" | |
| 666 | + ], | |
| 667 | + "regions": [ | |
| 668 | + "US", | |
| 669 | + "CA", | |
| 670 | + "GB", | |
| 671 | + "EU" | |
| 672 | + ], | |
| 673 | + "languages": [ | |
| 674 | + "en" | |
| 675 | + ], | |
| 676 | + "currency": [ | |
| 677 | + "USD", | |
| 678 | + "CAD", | |
| 679 | + "GBP", | |
| 680 | + "EUR" | |
| 681 | + ], | |
| 682 | + "supportsListings": false, | |
| 683 | + "supportsSold": true, | |
| 684 | + "supportsAuctions": false, | |
| 685 | + "supportsImages": true, | |
| 686 | + "supportsCatalog": false, | |
| 687 | + "supportsPopulation": false, | |
| 688 | + "supportsLookup": true, | |
| 689 | + "refreshFrequencyMinutes": 360, | |
| 690 | + "priority": "high", | |
| 691 | + "trustScore": 0.9, | |
| 692 | + "attributionRequired": true, | |
| 693 | + "termsUrl": "https://bringatrailer.com/terms-of-use/", | |
| 694 | + "accessNotes": "Completed auctions read from the same public JSON the /auctions/results/ page uses (POST /wp-json/bringatrailer/1.0/data/listings-filter, 36 items/page, ~262k results, newest first) over plain HTTPS with the RareIndex user agent; no login, no bidder data. robots.txt allows the results pages (search and member areas are disallowed and not used). 'Sold for <CUR> <amount> on <date>' entries become sales; 'Bid to' (reserve not met) entries are skipped. Prices are the winning bid: BaT's buyer fee (5%, capped) is NOT included → buyer_premium_included=false. Lookup of a listing URL parses the public listing page (title, sold line, chassis/VIN, mileage). 1.5 s politeness delay.", | |
| 695 | + "enabled": true, | |
| 696 | + "schemaVersion": "1.0", | |
| 697 | + "config": { | |
| 698 | + "pagesPerRun": 20, | |
| 699 | + "perPage": 36 | |
| 700 | + } | |
| 701 | + }, | |
| 702 | + { | |
| 703 | + "id": "cars-and-bids", | |
| 704 | + "displayName": "Cars & Bids (past auctions)", | |
| 705 | + "sourceId": "cars-and-bids", | |
| 706 | + "sourceName": "Cars & Bids", | |
| 707 | + "sourceType": "auction_house", | |
| 708 | + "sourceUrl": "https://carsandbids.com", | |
| 709 | + "module": "firecrawl/cars-and-bids", | |
| 710 | + "enginePriority": [ | |
| 711 | + "firecrawl" | |
| 712 | + ], | |
| 713 | + "categories": [ | |
| 714 | + "automobiles" | |
| 715 | + ], | |
| 716 | + "regions": [ | |
| 717 | + "US", | |
| 718 | + "CA" | |
| 719 | + ], | |
| 720 | + "languages": [ | |
| 721 | + "en" | |
| 722 | + ], | |
| 723 | + "currency": [ | |
| 724 | + "USD", | |
| 725 | + "CAD" | |
| 726 | + ], | |
| 727 | + "supportsListings": false, | |
| 728 | + "supportsSold": true, | |
| 729 | + "supportsAuctions": false, | |
| 730 | + "supportsImages": true, | |
| 731 | + "supportsCatalog": false, | |
| 732 | + "supportsPopulation": false, | |
| 733 | + "supportsLookup": false, | |
| 734 | + "refreshFrequencyMinutes": 360, | |
| 735 | + "priority": "high", | |
| 736 | + "trustScore": 0.9, | |
| 737 | + "attributionRequired": true, | |
| 738 | + "termsUrl": "https://carsandbids.com/terms-of-use/", | |
| 739 | + "accessNotes": "Public 'Past Results' list (/past-auctions/?page=N, ~50 auctions per page, newest first) rendered through Firecrawl (plain HTTPS returns a Cloudflare interstitial; we do not bypass it — Firecrawl fetches the public page like a browser). robots.txt allows the page (only /sell-car/, /widgets/, /dealers/ are disallowed). 'Sold for $X' + 'Ended m/d/yy' rows become sales; 'Bid to' rows (reserve not met) are skipped. Price is the winning bid — the 4.5% buyer fee is NOT included → buyer_premium_included=false. 1 Firecrawl credit per page, 2 s politeness delay.", | |
| 740 | + "enabled": true, | |
| 741 | + "schemaVersion": "1.0", | |
| 742 | + "config": { | |
| 743 | + "pagesPerRun": 10 | |
| 744 | + } | |
| 745 | + }, | |
| 746 | + { | |
| 747 | + "id": "catawiki", | |
| 748 | + "displayName": "Catawiki (closed-lot results & live lots)", | |
| 749 | + "sourceId": "catawiki", | |
| 750 | + "sourceName": "Catawiki", | |
| 751 | + "sourceType": "auction_house", | |
| 752 | + "sourceUrl": "https://www.catawiki.com", | |
| 753 | + "module": "scrapfly/catawiki", | |
| 754 | + "enginePriority": [ | |
| 755 | + "scrapfly" | |
| 756 | + ], | |
| 757 | + "categories": [ | |
| 758 | + "pokemon", | |
| 759 | + "magic_the_gathering", | |
| 760 | + "yugioh", | |
| 761 | + "one_piece_card_game", | |
| 762 | + "disney_lorcana", | |
| 763 | + "other_tcg", | |
| 764 | + "basketball_cards", | |
| 765 | + "baseball_cards", | |
| 766 | + "football_cards", | |
| 767 | + "hockey_cards", | |
| 768 | + "soccer_cards", | |
| 769 | + "f1_cards", | |
| 770 | + "non_sport_cards", | |
| 771 | + "rolex", | |
| 772 | + "patek_philippe", | |
| 773 | + "audemars_piguet", | |
| 774 | + "omega", | |
| 775 | + "other_watches", | |
| 776 | + "pens", | |
| 777 | + "lighters", | |
| 778 | + "marvel_comics", | |
| 779 | + "dc_comics", | |
| 780 | + "independent_comics", | |
| 781 | + "manga", | |
| 782 | + "animation_art", | |
| 783 | + "lego_sets", | |
| 784 | + "funko", | |
| 374 | 785 | "model_cars", |
| 375 | 786 | "model_trains", |
| 376 | 787 | "action_figures", |
@@ -677,6 +1088,58 @@ | ||
| 677 | 1088 | "pageSize": 120 |
| 678 | 1089 | } |
| 679 | 1090 | }, |
| 1091 | + { | |
| 1092 | + "id": "collecting-cars", | |
| 1093 | + "displayName": "Collecting Cars (results)", | |
| 1094 | + "sourceId": "collecting-cars", | |
| 1095 | + "sourceName": "Collecting Cars", | |
| 1096 | + "sourceType": "auction_house", | |
| 1097 | + "sourceUrl": "https://collectingcars.com", | |
| 1098 | + "module": "firecrawl/collecting-cars", | |
| 1099 | + "enginePriority": [ | |
| 1100 | + "firecrawl" | |
| 1101 | + ], | |
| 1102 | + "categories": [ | |
| 1103 | + "automobiles", | |
| 1104 | + "motorcycles", | |
| 1105 | + "automotive_memorabilia" | |
| 1106 | + ], | |
| 1107 | + "regions": [ | |
| 1108 | + "GB", | |
| 1109 | + "EU", | |
| 1110 | + "AU", | |
| 1111 | + "US", | |
| 1112 | + "AE" | |
| 1113 | + ], | |
| 1114 | + "languages": [ | |
| 1115 | + "en" | |
| 1116 | + ], | |
| 1117 | + "currency": [ | |
| 1118 | + "GBP", | |
| 1119 | + "EUR", | |
| 1120 | + "AUD", | |
| 1121 | + "USD", | |
| 1122 | + "AED" | |
| 1123 | + ], | |
| 1124 | + "supportsListings": false, | |
| 1125 | + "supportsSold": true, | |
| 1126 | + "supportsAuctions": false, | |
| 1127 | + "supportsImages": true, | |
| 1128 | + "supportsCatalog": false, | |
| 1129 | + "supportsPopulation": false, | |
| 1130 | + "supportsLookup": false, | |
| 1131 | + "refreshFrequencyMinutes": 720, | |
| 1132 | + "priority": "low", | |
| 1133 | + "trustScore": 0.9, | |
| 1134 | + "attributionRequired": true, | |
| 1135 | + "termsUrl": "https://collectingcars.com/terms-and-conditions", | |
| 1136 | + "accessNotes": "Public results list (/sold?page=N, 48 lots per page, ~26k sold lots, newest first) rendered through Firecrawl (plain HTTPS gets a Cloudflare interstitial; not bypassed). robots.txt: Allow / for generic agents (named AI-training crawlers are disallowed; RareIndexBot is not one — we only record public sale facts and link back). Most sold prices are shown only to signed-in users ('Sign in to view sold price'); we NEVER log in, so only lots whose price is displayed publicly (a minority, typically the most recent/featured results) become sales — expect a few sales per page. Prices are hammer prices — the buyer's premium is charged on top → buyer_premium_included=false. 1 Firecrawl credit per page, 2 s politeness delay.", | |
| 1137 | + "enabled": true, | |
| 1138 | + "schemaVersion": "1.0", | |
| 1139 | + "config": { | |
| 1140 | + "pagesPerRun": 10 | |
| 1141 | + } | |
| 1142 | + }, | |
| 680 | 1143 | { |
| 681 | 1144 | "id": "comicconnect", |
| 682 | 1145 | "displayName": "ComicConnect (sold archive)", |
@@ -726,6 +1189,110 @@ | ||
| 726 | 1189 | "sortType": "ended_desc" |
| 727 | 1190 | } |
| 728 | 1191 | }, |
| 1192 | + { | |
| 1193 | + "id": "crown-caliber", | |
| 1194 | + "displayName": "European Watch Company (crownandcaliber.com) — pre-owned watch listings", | |
| 1195 | + "sourceId": "europeanwatch", | |
| 1196 | + "sourceName": "European Watch Company", | |
| 1197 | + "sourceType": "dealer", | |
| 1198 | + "sourceUrl": "https://www.europeanwatch.com", | |
| 1199 | + "module": "api/crown-caliber", | |
| 1200 | + "enginePriority": [ | |
| 1201 | + "api", | |
| 1202 | + "firecrawl" | |
| 1203 | + ], | |
| 1204 | + "categories": [ | |
| 1205 | + "watches", | |
| 1206 | + "rolex", | |
| 1207 | + "patek_philippe", | |
| 1208 | + "audemars_piguet", | |
| 1209 | + "omega", | |
| 1210 | + "other_watches" | |
| 1211 | + ], | |
| 1212 | + "regions": [ | |
| 1213 | + "US" | |
| 1214 | + ], | |
| 1215 | + "languages": [ | |
| 1216 | + "en" | |
| 1217 | + ], | |
| 1218 | + "currency": [ | |
| 1219 | + "USD" | |
| 1220 | + ], | |
| 1221 | + "supportsListings": true, | |
| 1222 | + "supportsSold": false, | |
| 1223 | + "supportsAuctions": false, | |
| 1224 | + "supportsImages": true, | |
| 1225 | + "supportsCatalog": false, | |
| 1226 | + "supportsPopulation": false, | |
| 1227 | + "supportsLookup": true, | |
| 1228 | + "refreshFrequencyMinutes": 720, | |
| 1229 | + "priority": "medium", | |
| 1230 | + "trustScore": 0.8, | |
| 1231 | + "attributionRequired": true, | |
| 1232 | + "termsUrl": "https://www.europeanwatch.com/terms", | |
| 1233 | + "accessNotes": "crownandcaliber.com now redirects to europeanwatch.com (Boston dealer). Public brand pages (/brand/<slug>?page=N) embed a schema.org ItemList of Products (name, sku, USD price, availability, condition) — fetched over plain HTTPS with the RareIndex user agent; robots.txt only disallows /login. References and circa years are parsed from the product name; box/papers/condition details live on product pages and are only fetched by lookup(). 1.5 s between pages, the whole brand inventory is served on one page (~130 watches for Rolex; pagination parameters are ignored, which the crawler detects).", | |
| 1234 | + "enabled": true, | |
| 1235 | + "schemaVersion": "1.0", | |
| 1236 | + "config": { | |
| 1237 | + "seeds": [ | |
| 1238 | + "rolex", | |
| 1239 | + "patek-philippe", | |
| 1240 | + "audemars-piguet", | |
| 1241 | + "omega", | |
| 1242 | + "cartier", | |
| 1243 | + "tudor", | |
| 1244 | + "vacheron-constantin", | |
| 1245 | + "a-lange-and-sohne", | |
| 1246 | + "iwc", | |
| 1247 | + "jaeger-lecoultre", | |
| 1248 | + "breitling", | |
| 1249 | + "panerai", | |
| 1250 | + "grand-seiko", | |
| 1251 | + "f-p-journe", | |
| 1252 | + "zenith" | |
| 1253 | + ], | |
| 1254 | + "pagesPerSeed": 1 | |
| 1255 | + } | |
| 1256 | + }, | |
| 1257 | + { | |
| 1258 | + "id": "digimoncard", | |
| 1259 | + "displayName": "DigimonCard.io (Digimon Card Game)", | |
| 1260 | + "sourceId": "digimoncard", | |
| 1261 | + "sourceName": "DigimonCard.io", | |
| 1262 | + "sourceType": "catalog", | |
| 1263 | + "sourceUrl": "https://digimoncard.io", | |
| 1264 | + "module": "api/digimoncard", | |
| 1265 | + "enginePriority": [ | |
| 1266 | + "api" | |
| 1267 | + ], | |
| 1268 | + "categories": [ | |
| 1269 | + "digimon_tcg" | |
| 1270 | + ], | |
| 1271 | + "regions": [ | |
| 1272 | + "US", | |
| 1273 | + "JP" | |
| 1274 | + ], | |
| 1275 | + "languages": [ | |
| 1276 | + "en" | |
| 1277 | + ], | |
| 1278 | + "currency": [], | |
| 1279 | + "supportsListings": false, | |
| 1280 | + "supportsSold": false, | |
| 1281 | + "supportsAuctions": false, | |
| 1282 | + "supportsImages": true, | |
| 1283 | + "supportsCatalog": true, | |
| 1284 | + "supportsPopulation": false, | |
| 1285 | + "supportsLookup": true, | |
| 1286 | + "refreshFrequencyMinutes": 10080, | |
| 1287 | + "priority": "low", | |
| 1288 | + "trustScore": 0.7, | |
| 1289 | + "attributionRequired": true, | |
| 1290 | + "termsUrl": "https://digimoncard.io/api-public/", | |
| 1291 | + "accessNotes": "Free public JSON API (https://digimoncard.io/api-public/search?series=Digimon%20Card%20Game returns every card in one call; the documented '.php' paths 301 to the extensionless ones). Catalog only — no prices; TCGplayer product ids are included and used as the deterministic identifier (tcgplayer_id) so TCGCSV prices attach to the same assets. Images from images.digimoncard.io/images/cards/<id>.jpg. Primary set derived from the card number prefix (BT5 → 'BT-05' set entry).", | |
| 1292 | + "enabled": true, | |
| 1293 | + "schemaVersion": "1.0", | |
| 1294 | + "config": {} | |
| 1295 | + }, | |
| 729 | 1296 | { |
| 730 | 1297 | "id": "discogs", |
| 731 | 1298 | "displayName": "Discogs (releases + marketplace lows)", |
@@ -805,31 +1372,23 @@ | ||
| 805 | 1372 | } |
| 806 | 1373 | }, |
| 807 | 1374 | { |
| 808 | − "id": "goldin", | |
| 809 | − "displayName": "Goldin (sold auction results)", | |
| 810 | − "sourceId": "goldin", | |
| 811 | − "sourceName": "Goldin", | |
| 812 | − "sourceType": "auction_house", | |
| 813 | − "sourceUrl": "https://goldin.co", | |
| 814 | − "module": "scrapfly/goldin", | |
| 1375 | + "id": "fashionphile", | |
| 1376 | + "displayName": "FASHIONPHILE (pre-owned luxury listings & sold items)", | |
| 1377 | + "sourceId": "fashionphile", | |
| 1378 | + "sourceName": "FASHIONPHILE", | |
| 1379 | + "sourceType": "marketplace", | |
| 1380 | + "sourceUrl": "https://www.fashionphile.com", | |
| 1381 | + "module": "api/fashionphile", | |
| 815 | 1382 | "enginePriority": [ |
| 816 | − "scrapfly" | |
| 1383 | + "api" | |
| 817 | 1384 | ], |
| 818 | 1385 | "categories": [ |
| 819 | − "pokemon", | |
| 820 | − "magic_the_gathering", | |
| 821 | − "yugioh", | |
| 822 | − "one_piece_card_game", | |
| 823 | − "basketball_cards", | |
| 824 | − "baseball_cards", | |
| 825 | − "football_cards", | |
| 826 | − "hockey_cards", | |
| 827 | − "soccer_cards", | |
| 828 | − "f1_cards", | |
| 829 | − "sports_memorabilia", | |
| 830 | − "marvel_comics", | |
| 831 | − "nintendo_games", | |
| 832 | − "non_sport_cards" | |
| 1386 | + "luxury_handbags", | |
| 1387 | + "watches", | |
| 1388 | + "other_watches", | |
| 1389 | + "rolex", | |
| 1390 | + "jewelry", | |
| 1391 | + "fashion_streetwear" | |
| 833 | 1392 | ], |
| 834 | 1393 | "regions": [ |
| 835 | 1394 | "US" |
@@ -840,27 +1399,224 @@ | ||
| 840 | 1399 | "currency": [ |
| 841 | 1400 | "USD" |
| 842 | 1401 | ], |
| 843 | − "supportsListings": false, | |
| 844 | − "supportsSold": true, | |
| 1402 | + "supportsListings": true, | |
| 1403 | + "supportsSold": false, | |
| 845 | 1404 | "supportsAuctions": false, |
| 846 | 1405 | "supportsImages": true, |
| 847 | 1406 | "supportsCatalog": false, |
| 848 | 1407 | "supportsPopulation": false, |
| 849 | 1408 | "supportsLookup": true, |
| 850 | − "refreshFrequencyMinutes": 360, | |
| 851 | − "priority": "high", | |
| 852 | − "trustScore": 0.9, | |
| 1409 | + "refreshFrequencyMinutes": 720, | |
| 1410 | + "priority": "medium", | |
| 1411 | + "trustScore": 0.8, | |
| 853 | 1412 | "attributionRequired": true, |
| 854 | − "termsUrl": "https://goldin.co/useragreement", | |
| 855 | − "accessNotes": "Public 'Sold Items' result grids (https://goldin.co/buy/sc/<subcategory>?show_only=Sold%20Items&sort=Most_Recent_Bids&number_of_lots=240) rendered through Scrapfly (asp + JS rendering, ~6 credits per page of 240 sold lots). goldin.co/robots.txt allows /buy/ and /item/ and disallows /api/; we never call goldin.co/api ourselves — the lot data is read from the XHR (lots_v2 on their CloudFront search endpoint) that the public page performs during rendering, exactly what a browser does. Plain HTTP and Firecrawl return the empty SPA shell / an Akamai challenge. Prices: the grid shows the final price INCLUDING buyer's premium (current_price × (1 + buyer_premium%)); we store that as the sale price with buyerPremiumIncluded=true and keep the hammer price and BP % in metadata. Sale date = lot end_timestamp; lots whose end_timestamp is in the future (placeholder private-sale dates like 2235-06-04) are skipped and counted as anomalies. Cert numbers appear only in item-page descriptions (lookup).", | |
| 1413 | + "termsUrl": "https://www.fashionphile.com/terms-of-use", | |
| 1414 | + "accessNotes": "Public Shopify storefront feed (www.fashionphile.com/collections/<brand>/products.json) fetched over plain HTTPS with the RareIndex user agent; robots.txt allows /collections/ (only sort/filter permutations, cart, account and checkout paths are disallowed). Each product is one authenticated pre-owned item priced in USD; `available:false` on a still-published product means the item shows SOLD on the site, which is recorded as a listing with availability=sold (the last asking price is not asserted as the transaction price). Condition ratings are only on HTML product pages and are not fetched. 1.5 s between requests, 250 products per page.", | |
| 856 | 1415 | "enabled": true, |
| 857 | 1416 | "schemaVersion": "1.0", |
| 858 | 1417 | "config": { |
| 859 | 1418 | "seeds": [ |
| 860 | − { | |
| 861 | − "path": "/buy/sc/pokemon", | |
| 862 | − "categorySlug": "pokemon" | |
| 863 | − }, | |
| 1419 | + "hermes", | |
| 1420 | + "chanel", | |
| 1421 | + "louis-vuitton", | |
| 1422 | + "dior", | |
| 1423 | + "gucci", | |
| 1424 | + "goyard", | |
| 1425 | + "fendi", | |
| 1426 | + "bottega-veneta", | |
| 1427 | + "prada", | |
| 1428 | + "celine", | |
| 1429 | + "loewe", | |
| 1430 | + "saint-laurent", | |
| 1431 | + "rolex", | |
| 1432 | + "cartier", | |
| 1433 | + "van-cleef-arpels" | |
| 1434 | + ], | |
| 1435 | + "pagesPerSeed": 2 | |
| 1436 | + } | |
| 1437 | + }, | |
| 1438 | + { | |
| 1439 | + "id": "flight-club", | |
| 1440 | + "displayName": "Flight Club (sneaker lowest asks & retail)", | |
| 1441 | + "sourceId": "flight-club", | |
| 1442 | + "sourceName": "Flight Club", | |
| 1443 | + "sourceType": "marketplace", | |
| 1444 | + "sourceUrl": "https://www.flightclub.com", | |
| 1445 | + "module": "firecrawl/flight-club", | |
| 1446 | + "enginePriority": [ | |
| 1447 | + "firecrawl", | |
| 1448 | + "scrapfly" | |
| 1449 | + ], | |
| 1450 | + "categories": [ | |
| 1451 | + "sneakers", | |
| 1452 | + "nike_jordan", | |
| 1453 | + "adidas_yeezy", | |
| 1454 | + "new_balance_asics_other" | |
| 1455 | + ], | |
| 1456 | + "regions": [ | |
| 1457 | + "US" | |
| 1458 | + ], | |
| 1459 | + "languages": [ | |
| 1460 | + "en" | |
| 1461 | + ], | |
| 1462 | + "currency": [ | |
| 1463 | + "USD" | |
| 1464 | + ], | |
| 1465 | + "supportsListings": true, | |
| 1466 | + "supportsSold": false, | |
| 1467 | + "supportsAuctions": false, | |
| 1468 | + "supportsImages": true, | |
| 1469 | + "supportsCatalog": true, | |
| 1470 | + "supportsPopulation": false, | |
| 1471 | + "supportsLookup": false, | |
| 1472 | + "refreshFrequencyMinutes": 1440, | |
| 1473 | + "priority": "medium", | |
| 1474 | + "trustScore": 0.75, | |
| 1475 | + "attributionRequired": true, | |
| 1476 | + "termsUrl": "https://www.flightclub.com/terms", | |
| 1477 | + "accessNotes": "Plain HTTPS requests receive 403 (robots.txt itself is not served to non-browser agents); Firecrawl's standard fetch returns the public category pages (flightclub.com/air-jordans?page=N …) whose __NEXT_DATA__ holds the search grid: product id, name, brand, image, lowest price, retail price and a slug ending with the style code. 1 Firecrawl credit per page of 30 products; 2 s between pages. Listings/asks only — Flight Club does not publish sale history publicly.", | |
| 1478 | + "enabled": true, | |
| 1479 | + "schemaVersion": "1.0", | |
| 1480 | + "config": { | |
| 1481 | + "seeds": [ | |
| 1482 | + "air-jordans", | |
| 1483 | + "nike", | |
| 1484 | + "adidas/adidas-yeezy", | |
| 1485 | + "adidas", | |
| 1486 | + "new-balance", | |
| 1487 | + "asics" | |
| 1488 | + ], | |
| 1489 | + "pagesPerSeed": 2 | |
| 1490 | + } | |
| 1491 | + }, | |
| 1492 | + { | |
| 1493 | + "id": "fossilera", | |
| 1494 | + "displayName": "FossilEra (fossils, minerals & meteorites dealer, USD)", | |
| 1495 | + "sourceId": "fossilera", | |
| 1496 | + "sourceName": "FossilEra", | |
| 1497 | + "sourceType": "dealer", | |
| 1498 | + "sourceUrl": "https://www.fossilera.com", | |
| 1499 | + "module": "api/fossilera", | |
| 1500 | + "enginePriority": [ | |
| 1501 | + "api", | |
| 1502 | + "firecrawl" | |
| 1503 | + ], | |
| 1504 | + "categories": [ | |
| 1505 | + "fossils", | |
| 1506 | + "minerals", | |
| 1507 | + "meteorites" | |
| 1508 | + ], | |
| 1509 | + "regions": [ | |
| 1510 | + "US" | |
| 1511 | + ], | |
| 1512 | + "languages": [ | |
| 1513 | + "en" | |
| 1514 | + ], | |
| 1515 | + "currency": [ | |
| 1516 | + "USD" | |
| 1517 | + ], | |
| 1518 | + "supportsListings": true, | |
| 1519 | + "supportsSold": false, | |
| 1520 | + "supportsAuctions": false, | |
| 1521 | + "supportsImages": true, | |
| 1522 | + "supportsCatalog": false, | |
| 1523 | + "supportsPopulation": false, | |
| 1524 | + "supportsLookup": true, | |
| 1525 | + "refreshFrequencyMinutes": 1440, | |
| 1526 | + "priority": "low", | |
| 1527 | + "trustScore": 0.85, | |
| 1528 | + "attributionRequired": true, | |
| 1529 | + "termsUrl": "https://www.fossilera.com/pages/terms-of-service", | |
| 1530 | + "accessNotes": "Public category pages (fossilera.com/fossils-for-sale/<category>?page=N) and specimen pages fetched over plain HTTPS with the RareIndex user agent (robots.txt only disallows gift-card/checkout helpers). Each card gives the specimen title, price (and previous price when reduced), image and the FossilEra item number; specimen pages add species, geological age, location, formation, size and category. Every specimen is unique, so each becomes its own asset with one fixed-price listing (available or, when the page states it, sold — no sale date is published, so sold items are listings with availability=sold, never transactions). Compliance: fossilera states all items are legally collected; locality is kept in attributes.country/region so jurisdiction flags (taxonomy compliance 'jurisdiction_check') can be applied downstream.", | |
| 1531 | + "enabled": true, | |
| 1532 | + "schemaVersion": "1.0", | |
| 1533 | + "config": { | |
| 1534 | + "seeds": [ | |
| 1535 | + { | |
| 1536 | + "path": "/fossils-for-sale/dinosaur-teeth", | |
| 1537 | + "categorySlug": "fossils" | |
| 1538 | + }, | |
| 1539 | + { | |
| 1540 | + "path": "/fossils-for-sale/megalodon-teeth", | |
| 1541 | + "categorySlug": "fossils" | |
| 1542 | + }, | |
| 1543 | + { | |
| 1544 | + "path": "/fossils-for-sale/ammonites", | |
| 1545 | + "categorySlug": "fossils" | |
| 1546 | + }, | |
| 1547 | + { | |
| 1548 | + "path": "/fossils-for-sale/trilobites", | |
| 1549 | + "categorySlug": "fossils" | |
| 1550 | + }, | |
| 1551 | + { | |
| 1552 | + "path": "/minerals-for-sale", | |
| 1553 | + "categorySlug": "minerals" | |
| 1554 | + }, | |
| 1555 | + { | |
| 1556 | + "path": "/meteorites-for-sale", | |
| 1557 | + "categorySlug": "meteorites" | |
| 1558 | + } | |
| 1559 | + ], | |
| 1560 | + "pagesPerSeed": 1 | |
| 1561 | + } | |
| 1562 | + }, | |
| 1563 | + { | |
| 1564 | + "id": "goldin", | |
| 1565 | + "displayName": "Goldin (sold auction results)", | |
| 1566 | + "sourceId": "goldin", | |
| 1567 | + "sourceName": "Goldin", | |
| 1568 | + "sourceType": "auction_house", | |
| 1569 | + "sourceUrl": "https://goldin.co", | |
| 1570 | + "module": "scrapfly/goldin", | |
| 1571 | + "enginePriority": [ | |
| 1572 | + "scrapfly" | |
| 1573 | + ], | |
| 1574 | + "categories": [ | |
| 1575 | + "pokemon", | |
| 1576 | + "magic_the_gathering", | |
| 1577 | + "yugioh", | |
| 1578 | + "one_piece_card_game", | |
| 1579 | + "basketball_cards", | |
| 1580 | + "baseball_cards", | |
| 1581 | + "football_cards", | |
| 1582 | + "hockey_cards", | |
| 1583 | + "soccer_cards", | |
| 1584 | + "f1_cards", | |
| 1585 | + "sports_memorabilia", | |
| 1586 | + "marvel_comics", | |
| 1587 | + "nintendo_games", | |
| 1588 | + "non_sport_cards" | |
| 1589 | + ], | |
| 1590 | + "regions": [ | |
| 1591 | + "US" | |
| 1592 | + ], | |
| 1593 | + "languages": [ | |
| 1594 | + "en" | |
| 1595 | + ], | |
| 1596 | + "currency": [ | |
| 1597 | + "USD" | |
| 1598 | + ], | |
| 1599 | + "supportsListings": false, | |
| 1600 | + "supportsSold": true, | |
| 1601 | + "supportsAuctions": false, | |
| 1602 | + "supportsImages": true, | |
| 1603 | + "supportsCatalog": false, | |
| 1604 | + "supportsPopulation": false, | |
| 1605 | + "supportsLookup": true, | |
| 1606 | + "refreshFrequencyMinutes": 360, | |
| 1607 | + "priority": "high", | |
| 1608 | + "trustScore": 0.9, | |
| 1609 | + "attributionRequired": true, | |
| 1610 | + "termsUrl": "https://goldin.co/useragreement", | |
| 1611 | + "accessNotes": "Public 'Sold Items' result grids (https://goldin.co/buy/sc/<subcategory>?show_only=Sold%20Items&sort=Most_Recent_Bids&number_of_lots=240) rendered through Scrapfly (asp + JS rendering, ~6 credits per page of 240 sold lots). goldin.co/robots.txt allows /buy/ and /item/ and disallows /api/; we never call goldin.co/api ourselves — the lot data is read from the XHR (lots_v2 on their CloudFront search endpoint) that the public page performs during rendering, exactly what a browser does. Plain HTTP and Firecrawl return the empty SPA shell / an Akamai challenge. Prices: the grid shows the final price INCLUDING buyer's premium (current_price × (1 + buyer_premium%)); we store that as the sale price with buyerPremiumIncluded=true and keep the hammer price and BP % in metadata. Sale date = lot end_timestamp; lots whose end_timestamp is in the future (placeholder private-sale dates like 2235-06-04) are skipped and counted as anomalies. Cert numbers appear only in item-page descriptions (lookup).", | |
| 1612 | + "enabled": true, | |
| 1613 | + "schemaVersion": "1.0", | |
| 1614 | + "config": { | |
| 1615 | + "seeds": [ | |
| 1616 | + { | |
| 1617 | + "path": "/buy/sc/pokemon", | |
| 1618 | + "categorySlug": "pokemon" | |
| 1619 | + }, | |
| 864 | 1620 | { |
| 865 | 1621 | "path": "/buy/sc/Magic_The_Gathering", |
| 866 | 1622 | "categorySlug": "magic_the_gathering" |
@@ -918,80 +1674,71 @@ | ||
| 918 | 1674 | } |
| 919 | 1675 | }, |
| 920 | 1676 | { |
| 921 | − "id": "lorcast", | |
| 922 | − "displayName": "Lorcast (Disney Lorcana)", | |
| 923 | − "sourceId": "lorcast", | |
| 924 | − "sourceName": "Lorcast", | |
| 925 | − "sourceType": "catalog", | |
| 926 | − "sourceUrl": "https://lorcast.com", | |
| 927 | − "module": "api/lorcast", | |
| 1677 | + "id": "grand-archive", | |
| 1678 | + "displayName": "Grand Archive Index (official card API)", | |
| 1679 | + "sourceId": "grand-archive", | |
| 1680 | + "sourceName": "Grand Archive TCG Index", | |
| 1681 | + "sourceType": "manufacturer", | |
| 1682 | + "sourceUrl": "https://index.gatcg.com", | |
| 1683 | + "module": "api/grand-archive", | |
| 928 | 1684 | "enginePriority": [ |
| 929 | 1685 | "api" |
| 930 | 1686 | ], |
| 931 | 1687 | "categories": [ |
| 932 | − "disney_lorcana" | |
| 1688 | + "other_tcg" | |
| 933 | 1689 | ], |
| 934 | 1690 | "regions": [ |
| 935 | − "US", | |
| 936 | − "EU" | |
| 1691 | + "US" | |
| 937 | 1692 | ], |
| 938 | 1693 | "languages": [ |
| 939 | 1694 | "en" |
| 940 | 1695 | ], |
| 941 | − "currency": [ | |
| 942 | − "USD" | |
| 943 | − ], | |
| 1696 | + "currency": [], | |
| 944 | 1697 | "supportsListings": false, |
| 945 | 1698 | "supportsSold": false, |
| 946 | 1699 | "supportsAuctions": false, |
| 947 | 1700 | "supportsImages": true, |
| 948 | 1701 | "supportsCatalog": true, |
| 949 | − "supportsPopulation": false, | |
| 1702 | + "supportsPopulation": true, | |
| 950 | 1703 | "supportsLookup": false, |
| 951 | − "refreshFrequencyMinutes": 1440, | |
| 952 | − "priority": "medium", | |
| 953 | − "trustScore": 0.75, | |
| 1704 | + "refreshFrequencyMinutes": 10080, | |
| 1705 | + "priority": "low", | |
| 1706 | + "trustScore": 0.85, | |
| 954 | 1707 | "attributionRequired": true, |
| 955 | − "termsUrl": "https://lorcast.com/docs/api", | |
| 956 | − "accessNotes": "Open Lorcast REST API (v0, no key): /sets then /cards/search?q=set:<code>&page=N. Cards carry TCGplayer ids and USD market prices (normal / foil) without a timestamp, so observations are dated by the fetch day with confidence 0.7. Enchanted cards are emitted as a single 'Enchanted' variant. ~4 req/s self-throttled.", | |
| 1708 | + "termsUrl": "https://index.gatcg.com", | |
| 1709 | + "accessNotes": "Official Grand Archive card index API (https://api.gatcg.com/cards/search, paginated, no key). Emits one catalog_item per edition × circulation (non-foil / foil) with the publisher's declared print run in `productionQuantity` when the operator is exact ('=') and in metadata when approximate ('≈') — a rare case of manufacturer-published production counts. No prices (TCGCSV supplies Grand Archive prices via tcgplayer_id when available).", | |
| 957 | 1710 | "enabled": true, |
| 958 | 1711 | "schemaVersion": "1.0", |
| 959 | − "config": {} | |
| 1712 | + "config": { | |
| 1713 | + "pageSize": 50 | |
| 1714 | + } | |
| 960 | 1715 | }, |
| 961 | 1716 | { |
| 962 | − "id": "novelship", | |
| 963 | − "displayName": "Novelship (sneaker catalog, last sale & lowest ask)", | |
| 964 | − "sourceId": "novelship", | |
| 965 | − "sourceName": "Novelship", | |
| 966 | − "sourceType": "marketplace", | |
| 967 | − "sourceUrl": "https://novelship.com", | |
| 968 | − "module": "api/novelship", | |
| 1717 | + "id": "hobbysearch", | |
| 1718 | + "displayName": "HobbySearch 1999.co.jp (Gunpla & model kits, JPY)", | |
| 1719 | + "sourceId": "hobbysearch", | |
| 1720 | + "sourceName": "HobbySearch", | |
| 1721 | + "sourceType": "dealer", | |
| 1722 | + "sourceUrl": "https://www.1999.co.jp", | |
| 1723 | + "module": "firecrawl/hobbysearch", | |
| 969 | 1724 | "enginePriority": [ |
| 970 | − "api", | |
| 971 | 1725 | "firecrawl", |
| 972 | 1726 | "scrapfly" |
| 973 | 1727 | ], |
| 974 | 1728 | "categories": [ |
| 975 | − "sneakers", | |
| 976 | − "nike_jordan", | |
| 977 | − "adidas_yeezy", | |
| 978 | − "new_balance_asics_other" | |
| 1729 | + "gundam", | |
| 1730 | + "action_figures", | |
| 1731 | + "model_cars" | |
| 979 | 1732 | ], |
| 980 | 1733 | "regions": [ |
| 981 | − "SG", | |
| 982 | − "AU", | |
| 983 | − "NZ", | |
| 984 | − "TW", | |
| 985 | − "HK", | |
| 986 | − "MY", | |
| 987 | − "JP", | |
| 988 | − "US" | |
| 1734 | + "JP" | |
| 989 | 1735 | ], |
| 990 | 1736 | "languages": [ |
| 991 | − "en" | |
| 1737 | + "en", | |
| 1738 | + "ja" | |
| 992 | 1739 | ], |
| 993 | 1740 | "currency": [ |
| 994 | − "USD" | |
| 1741 | + "JPY" | |
| 995 | 1742 | ], |
| 996 | 1743 | "supportsListings": true, |
| 997 | 1744 | "supportsSold": false, |
@@ -1000,200 +1747,281 @@ | ||
| 1000 | 1747 | "supportsCatalog": true, |
| 1001 | 1748 | "supportsPopulation": false, |
| 1002 | 1749 | "supportsLookup": true, |
| 1003 | − "refreshFrequencyMinutes": 720, | |
| 1004 | − "priority": "medium", | |
| 1005 | − "trustScore": 0.7, | |
| 1750 | + "refreshFrequencyMinutes": 1440, | |
| 1751 | + "priority": "low", | |
| 1752 | + "trustScore": 0.8, | |
| 1006 | 1753 | "attributionRequired": true, |
| 1007 | − "termsUrl": "https://novelship.com/terms", | |
| 1008 | − "accessNotes": "Public Novelship browse pages (novelship.com/sneakers/<brand>?page=N) and product pages fetched over plain HTTPS with the RareIndex user agent; robots.txt only disallows sell/auth/dashboard/pay paths. The server-rendered payload embeds each product's SKU (style code), colorway, retail cost, release date, last sale price, lowest listing price and 180-day sales count — no login or private endpoint is used. Prices are read as USD (the anonymous international storefront prices in US$; cost_retail matches US retail) with confidence 0.7; per-size asks are not exposed on listing pages. Sizes/stock are not fetched. 1.5 s politeness delay.", | |
| 1754 | + "termsUrl": "https://www.1999.co.jp/eng/guide/", | |
| 1755 | + "accessNotes": "Public English search/list pages (1999.co.jp/eng/search?...; robots.txt allows all for generic agents) and item pages. Direct HTTPS returns 403 to non-browser clients, so pages go through Firecrawl (1 credit per page of 60 items). Parsed per card: HobbySearch item id, title, street price and list price (JPY), discount, stock state (In Stock / Sold Out / Pre-Order / Back-order), image. Emits catalog items (list price = manufacturer MSRP) and the retailer's fixed-price listing. Gundam categorisation is keyword-based on the seed + title; other kits fall back to the seed's category.", | |
| 1009 | 1756 | "enabled": true, |
| 1010 | 1757 | "schemaVersion": "1.0", |
| 1011 | 1758 | "config": { |
| 1012 | 1759 | "seeds": [ |
| 1013 | − "jordan", | |
| 1014 | − "nike", | |
| 1015 | − "adidas", | |
| 1016 | − "new-balance", | |
| 1017 | − "asics", | |
| 1018 | − "yeezy" | |
| 1760 | + { | |
| 1761 | + "key": "Gundam MG", | |
| 1762 | + "categorySlug": "gundam" | |
| 1763 | + }, | |
| 1764 | + { | |
| 1765 | + "key": "Gundam RG", | |
| 1766 | + "categorySlug": "gundam" | |
| 1767 | + }, | |
| 1768 | + { | |
| 1769 | + "key": "Gundam PG", | |
| 1770 | + "categorySlug": "gundam" | |
| 1771 | + }, | |
| 1772 | + { | |
| 1773 | + "key": "HGUC", | |
| 1774 | + "categorySlug": "gundam" | |
| 1775 | + }, | |
| 1776 | + { | |
| 1777 | + "key": "Nendoroid", | |
| 1778 | + "categorySlug": "action_figures" | |
| 1779 | + }, | |
| 1780 | + { | |
| 1781 | + "key": "figma", | |
| 1782 | + "categorySlug": "action_figures" | |
| 1783 | + } | |
| 1019 | 1784 | ], |
| 1020 | − "pagesPerSeed": 3 | |
| 1785 | + "pagesPerSeed": 1 | |
| 1021 | 1786 | } |
| 1022 | 1787 | }, |
| 1023 | 1788 | { |
| 1024 | − "id": "optcg", | |
| 1025 | − "displayName": "OPTCG API (One Piece Card Game)", | |
| 1026 | − "sourceId": "optcgapi", | |
| 1027 | − "sourceName": "OPTCG API", | |
| 1028 | − "sourceType": "catalog", | |
| 1029 | − "sourceUrl": "https://optcgapi.com", | |
| 1030 | − "module": "api/optcg", | |
| 1789 | + "id": "hypeboost", | |
| 1790 | + "displayName": "Hypeboost (EU sneaker marketplace — lowest asks)", | |
| 1791 | + "sourceId": "hypeboost", | |
| 1792 | + "sourceName": "Hypeboost", | |
| 1793 | + "sourceType": "marketplace", | |
| 1794 | + "sourceUrl": "https://hypeboost.com", | |
| 1795 | + "module": "firecrawl/hypeboost", | |
| 1031 | 1796 | "enginePriority": [ |
| 1032 | − "api" | |
| 1797 | + "firecrawl", | |
| 1798 | + "scrapfly" | |
| 1033 | 1799 | ], |
| 1034 | 1800 | "categories": [ |
| 1035 | − "one_piece_card_game" | |
| 1801 | + "sneakers", | |
| 1802 | + "nike_jordan", | |
| 1803 | + "adidas_yeezy", | |
| 1804 | + "new_balance_asics_other" | |
| 1036 | 1805 | ], |
| 1037 | 1806 | "regions": [ |
| 1038 | − "US" | |
| 1807 | + "NL", | |
| 1808 | + "EU" | |
| 1039 | 1809 | ], |
| 1040 | 1810 | "languages": [ |
| 1041 | 1811 | "en" |
| 1042 | 1812 | ], |
| 1043 | 1813 | "currency": [ |
| 1044 | − "USD" | |
| 1814 | + "EUR" | |
| 1045 | 1815 | ], |
| 1046 | − "supportsListings": false, | |
| 1816 | + "supportsListings": true, | |
| 1047 | 1817 | "supportsSold": false, |
| 1048 | 1818 | "supportsAuctions": false, |
| 1049 | 1819 | "supportsImages": true, |
| 1050 | 1820 | "supportsCatalog": true, |
| 1051 | 1821 | "supportsPopulation": false, |
| 1052 | − "supportsLookup": false, | |
| 1822 | + "supportsLookup": true, | |
| 1053 | 1823 | "refreshFrequencyMinutes": 1440, |
| 1054 | − "priority": "medium", | |
| 1055 | − "trustScore": 0.65, | |
| 1824 | + "priority": "low", | |
| 1825 | + "trustScore": 0.7, | |
| 1056 | 1826 | "attributionRequired": true, |
| 1057 | − "termsUrl": "https://optcgapi.com/", | |
| 1058 | − "accessNotes": "Open community API (no key): /api/allSets/ then /api/sets/<set_id>/ returning every card of the set with TCGplayer-derived market_price / inventory_price and a date_scraped, used as the observation date (confidence 0.7 / 0.65). Alternate arts share the card code and are distinguished by image id. One request per set, 0.5 s politeness.", | |
| 1827 | + "termsUrl": "https://hypeboost.com/en/terms-and-conditions", | |
| 1828 | + "accessNotes": "Plain HTTPS gets 403 (robots.txt not served to non-browser agents); Firecrawl's standard fetch returns the public category grid (hypeboost.com/en/category/sneakers/<brand>[?page=N]) with product name, EUR lowest price, brand/category data attributes and product id (36 per page, 1 credit). Product pages (lookup) expose the style code in schema.org Product sku. 2 s between pages.", | |
| 1059 | 1829 | "enabled": true, |
| 1060 | 1830 | "schemaVersion": "1.0", |
| 1061 | − "config": {} | |
| 1831 | + "config": { | |
| 1832 | + "seeds": [ | |
| 1833 | + "air-jordan", | |
| 1834 | + "nike", | |
| 1835 | + "adidas", | |
| 1836 | + "yeezy", | |
| 1837 | + "new-balance", | |
| 1838 | + "asics" | |
| 1839 | + ], | |
| 1840 | + "pagesPerSeed": 2 | |
| 1841 | + } | |
| 1062 | 1842 | }, |
| 1063 | 1843 | { |
| 1064 | − "id": "pcgs-priceguide", | |
| 1065 | − "displayName": "PCGS Price Guide (US coins)", | |
| 1066 | − "sourceId": "pcgs", | |
| 1067 | − "sourceName": "PCGS", | |
| 1068 | − "sourceType": "grading_company", | |
| 1069 | − "sourceUrl": "https://www.pcgs.com", | |
| 1070 | − "module": "firecrawl/pcgs-priceguide", | |
| 1844 | + "id": "idealwine", | |
| 1845 | + "displayName": "iDealwine Price Estimate (wine price index)", | |
| 1846 | + "sourceId": "idealwine", | |
| 1847 | + "sourceName": "iDealwine", | |
| 1848 | + "sourceType": "pricing_guide", | |
| 1849 | + "sourceUrl": "https://www.idealwine.com", | |
| 1850 | + "module": "api/idealwine", | |
| 1071 | 1851 | "enginePriority": [ |
| 1852 | + "api", | |
| 1072 | 1853 | "firecrawl", |
| 1073 | 1854 | "scrapfly" |
| 1074 | 1855 | ], |
| 1075 | 1856 | "categories": [ |
| 1076 | − "coins" | |
| 1857 | + "wine" | |
| 1077 | 1858 | ], |
| 1078 | 1859 | "regions": [ |
| 1079 | − "US" | |
| 1860 | + "FR", | |
| 1861 | + "EU" | |
| 1080 | 1862 | ], |
| 1081 | 1863 | "languages": [ |
| 1082 | − "en" | |
| 1864 | + "en", | |
| 1865 | + "fr" | |
| 1083 | 1866 | ], |
| 1084 | 1867 | "currency": [ |
| 1085 | − "USD" | |
| 1868 | + "EUR" | |
| 1086 | 1869 | ], |
| 1087 | 1870 | "supportsListings": false, |
| 1088 | 1871 | "supportsSold": false, |
| 1089 | 1872 | "supportsAuctions": false, |
| 1090 | − "supportsImages": false, | |
| 1873 | + "supportsImages": true, | |
| 1091 | 1874 | "supportsCatalog": true, |
| 1092 | 1875 | "supportsPopulation": false, |
| 1093 | 1876 | "supportsLookup": false, |
| 1094 | 1877 | "refreshFrequencyMinutes": 10080, |
| 1095 | − "priority": "low", | |
| 1096 | − "trustScore": 0.9, | |
| 1878 | + "priority": "medium", | |
| 1879 | + "trustScore": 0.85, | |
| 1097 | 1880 | "attributionRequired": true, |
| 1098 | − "termsUrl": "https://www.pcgs.com/legal", | |
| 1099 | − "accessNotes": "Public PCGS Price Guide category pages (pcgs.com/prices/detail/<series>/<id>/most-active) — no login required; robots.txt does not restrict /prices. Plain HTTPS is refused by the CDN (403) so pages are rendered through Firecrawl (1 credit per category page ≈ 100 coins × 10 grades). Values are PCGS retail guide values in USD per grade (columns 4…70 and '+' grades), dated with the page's 'Last Update' stamp. Stored as guide_value observations with grader 'pcgs' and grade like MS65 / PR65 / MS65+; PCGS coin numbers are kept as identifiers (pcgs_number). Auction Prices Realized and the Population Report on pcgs.com require a Collectors account and are not fetched.", | |
| 1881 | + "termsUrl": "https://www.idealwine.com/en/cgv", | |
| 1882 | + "accessNotes": "Public price-index pages (/en/cote/<region>?page=N) of iDealwine, the leading French wine auction house: each card lists a wine (region > appellation, colour, producer picture) with the iDealwine Price Estimate per vintage in EUR per 75 cl bottle. The estimate is computed by iDealwine from auction results (hammer price INCLUDING buyer's premium, updated weekly from 3M+ French auction prices since 1992) — we store it as a guide_value price observation, never as a transaction. Plain HTTPS with the RareIndex user agent; robots.txt disallows only /api/, account and login paths. No timestamp is published per estimate, so observationDate = fetch date (confidence 0.75). 2 s politeness delay; regions and pages per run are configurable.", | |
| 1883 | + "enabled": true, | |
| 1884 | + "schemaVersion": "1.0", | |
| 1885 | + "config": { | |
| 1886 | + "regions": [ | |
| 1887 | + "bordeaux", | |
| 1888 | + "bourgogne", | |
| 1889 | + "rhone", | |
| 1890 | + "champagne", | |
| 1891 | + "loire", | |
| 1892 | + "italie", | |
| 1893 | + "alsace", | |
| 1894 | + "languedoc", | |
| 1895 | + "provence", | |
| 1896 | + "jura" | |
| 1897 | + ], | |
| 1898 | + "pagesPerRegion": 3 | |
| 1899 | + } | |
| 1900 | + }, | |
| 1901 | + { | |
| 1902 | + "id": "laced", | |
| 1903 | + "displayName": "Laced (UK sneaker marketplace — asks per size)", | |
| 1904 | + "sourceId": "laced", | |
| 1905 | + "sourceName": "Laced", | |
| 1906 | + "sourceType": "marketplace", | |
| 1907 | + "sourceUrl": "https://www.laced.com", | |
| 1908 | + "module": "api/laced", | |
| 1909 | + "enginePriority": [ | |
| 1910 | + "api", | |
| 1911 | + "firecrawl" | |
| 1912 | + ], | |
| 1913 | + "categories": [ | |
| 1914 | + "sneakers", | |
| 1915 | + "nike_jordan", | |
| 1916 | + "adidas_yeezy", | |
| 1917 | + "new_balance_asics_other" | |
| 1918 | + ], | |
| 1919 | + "regions": [ | |
| 1920 | + "GB", | |
| 1921 | + "EU" | |
| 1922 | + ], | |
| 1923 | + "languages": [ | |
| 1924 | + "en" | |
| 1925 | + ], | |
| 1926 | + "currency": [ | |
| 1927 | + "GBP" | |
| 1928 | + ], | |
| 1929 | + "supportsListings": true, | |
| 1930 | + "supportsSold": false, | |
| 1931 | + "supportsAuctions": false, | |
| 1932 | + "supportsImages": true, | |
| 1933 | + "supportsCatalog": true, | |
| 1934 | + "supportsPopulation": false, | |
| 1935 | + "supportsLookup": true, | |
| 1936 | + "refreshFrequencyMinutes": 720, | |
| 1937 | + "priority": "medium", | |
| 1938 | + "trustScore": 0.75, | |
| 1939 | + "attributionRequired": true, | |
| 1940 | + "termsUrl": "https://www.laced.com/pages/terms-and-conditions", | |
| 1941 | + "accessNotes": "Public brand pages (laced.com/<brand>) list product slugs; each product page embeds a schema.org ProductGroup with the style code (sku), brand and one Product per size carrying the current lowest ask in GBP. Plain HTTPS with the RareIndex user agent; robots.txt allows everything except account/admin. New-condition marketplace (deadstock). 1.5 s between pages; productsPerSeed caps product-page fetches per run.", | |
| 1100 | 1942 | "enabled": true, |
| 1101 | 1943 | "schemaVersion": "1.0", |
| 1102 | 1944 | "config": { |
| 1103 | 1945 | "seeds": [ |
| 1104 | − "morgan-dollar/744", | |
| 1105 | − "peace-dollar/26", | |
| 1106 | − "lincoln-cent-wheat-reverse/46", | |
| 1107 | − "indian-cent/44", | |
| 1108 | − "buffalo-nickel/83", | |
| 1109 | − "mercury-dime/703", | |
| 1110 | − "walking-liberty-half-dollar/733", | |
| 1111 | − "standing-liberty-quarter/111", | |
| 1112 | − "franklin-half-dollar/734", | |
| 1113 | − "kennedy-half-dollar/125", | |
| 1114 | − "washington-quarter/112", | |
| 1115 | − "trade-dollar/743", | |
| 1116 | − "flying-eagle-cent/664", | |
| 1117 | − "barber-half-dollar/732", | |
| 1118 | − "silver-eagles/939", | |
| 1119 | − "liberty-seated-dollar/29" | |
| 1946 | + "air-jordan", | |
| 1947 | + "nike", | |
| 1948 | + "adidas", | |
| 1949 | + "yeezy", | |
| 1950 | + "new-balance", | |
| 1951 | + "asics" | |
| 1120 | 1952 | ], |
| 1121 | − "designations": [ | |
| 1122 | − "ms" | |
| 1123 | − ] | |
| 1953 | + "productsPerSeed": 24 | |
| 1124 | 1954 | } |
| 1125 | 1955 | }, |
| 1126 | 1956 | { |
| 1127 | − "id": "phillips-watches", | |
| 1128 | − "displayName": "Phillips Watches (auction results)", | |
| 1129 | − "sourceId": "phillips", | |
| 1130 | − "sourceName": "Phillips", | |
| 1131 | − "sourceType": "auction_house", | |
| 1132 | − "sourceUrl": "https://www.phillips.com", | |
| 1133 | − "module": "firecrawl/phillips-watches", | |
| 1957 | + "id": "lego-shop", | |
| 1958 | + "displayName": "LEGO.com Shop (official retail price & availability)", | |
| 1959 | + "sourceId": "lego-shop", | |
| 1960 | + "sourceName": "LEGO Shop (official)", | |
| 1961 | + "sourceType": "manufacturer", | |
| 1962 | + "sourceUrl": "https://www.lego.com", | |
| 1963 | + "module": "firecrawl/lego-shop", | |
| 1134 | 1964 | "enginePriority": [ |
| 1135 | 1965 | "firecrawl", |
| 1136 | 1966 | "scrapfly" |
| 1137 | 1967 | ], |
| 1138 | 1968 | "categories": [ |
| 1139 | − "watches", | |
| 1140 | − "rolex", | |
| 1141 | − "patek_philippe", | |
| 1142 | − "audemars_piguet", | |
| 1143 | − "omega", | |
| 1144 | − "other_watches" | |
| 1969 | + "lego_sets", | |
| 1970 | + "lego" | |
| 1145 | 1971 | ], |
| 1146 | 1972 | "regions": [ |
| 1147 | − "CH", | |
| 1148 | − "US", | |
| 1149 | − "HK", | |
| 1150 | − "GB" | |
| 1973 | + "US" | |
| 1151 | 1974 | ], |
| 1152 | 1975 | "languages": [ |
| 1153 | 1976 | "en" |
| 1154 | 1977 | ], |
| 1155 | 1978 | "currency": [ |
| 1156 | − "CHF", | |
| 1157 | − "USD", | |
| 1158 | − "HKD", | |
| 1159 | − "GBP", | |
| 1160 | − "EUR" | |
| 1979 | + "USD" | |
| 1161 | 1980 | ], |
| 1162 | − "supportsListings": false, | |
| 1163 | − "supportsSold": true, | |
| 1164 | − "supportsAuctions": true, | |
| 1981 | + "supportsListings": true, | |
| 1982 | + "supportsSold": false, | |
| 1983 | + "supportsAuctions": false, | |
| 1165 | 1984 | "supportsImages": true, |
| 1166 | − "supportsCatalog": false, | |
| 1985 | + "supportsCatalog": true, | |
| 1167 | 1986 | "supportsPopulation": false, |
| 1168 | − "supportsLookup": false, | |
| 1169 | − "refreshFrequencyMinutes": 10080, | |
| 1170 | − "priority": "high", | |
| 1987 | + "supportsLookup": true, | |
| 1988 | + "refreshFrequencyMinutes": 1440, | |
| 1989 | + "priority": "medium", | |
| 1171 | 1990 | "trustScore": 0.95, |
| 1172 | 1991 | "attributionRequired": true, |
| 1173 | − "termsUrl": "https://www.phillips.com/about/terms", | |
| 1174 | − "accessNotes": "Past watch sales are discovered from the public /auctions/past page (plain HTTPS; embedded JSON with saleNumber, title, end date, location). Each sale page is rendered through Firecrawl (JS wait ~6 s, 1 credit) to read the public lot list: lot number, maker, reference, model, estimate and 'Sold For' (Phillips publishes prices realised including buyer's premium). robots.txt disallows only /search and filter paths. No login, no bidder data. Large sales may lazy-load beyond the first ~70 lots — coverage is recorded per run.", | |
| 1992 | + "termsUrl": "https://www.lego.com/en-us/legal/notices-and-policies/terms-of-use", | |
| 1993 | + "accessNotes": "Official manufacturer store. Public theme pages (lego.com/en-us/themes/<theme>?page=N) and product pages are rendered client-side and return 403 to plain non-browser clients, so they are fetched with Firecrawl (1 credit per page; ~22 products per theme page). Parsed per product leaf: set number (from the product URL), name, US retail price, badges (New / Retiring soon / Coming Soon / Exclusive / Hard to find / Sold out), image; product pages add availability, age and piece count. Emits catalog items (MSRP USD, identifiers.lego_set_number shared with PriceCharting/Brickset/BrickEconomy) and the official-store fixed-price listing. robots.txt only disallows account/checkout/identity paths.", | |
| 1175 | 1994 | "enabled": true, |
| 1176 | 1995 | "schemaVersion": "1.0", |
| 1177 | 1996 | "config": { |
| 1178 | − "pastAuctionsUrl": "https://www.phillips.com/auctions/past", | |
| 1179 | − "salesPerRun": 6, | |
| 1180 | − "titleFilter": "watch" | |
| 1997 | + "themes": [ | |
| 1998 | + "star-wars", | |
| 1999 | + "icons", | |
| 2000 | + "technic", | |
| 2001 | + "ideas", | |
| 2002 | + "harry-potter", | |
| 2003 | + "marvel", | |
| 2004 | + "architecture", | |
| 2005 | + "ninjago", | |
| 2006 | + "creator-expert", | |
| 2007 | + "botanical-collection" | |
| 2008 | + ], | |
| 2009 | + "pagesPerTheme": 1 | |
| 1181 | 2010 | } |
| 1182 | 2011 | }, |
| 1183 | 2012 | { |
| 1184 | − "id": "pokemontcg", | |
| 1185 | − "displayName": "Pokémon TCG API (pokemontcg.io)", | |
| 1186 | − "sourceId": "pokemontcg", | |
| 1187 | − "sourceName": "Pokémon TCG API", | |
| 2013 | + "id": "lorcast", | |
| 2014 | + "displayName": "Lorcast (Disney Lorcana)", | |
| 2015 | + "sourceId": "lorcast", | |
| 2016 | + "sourceName": "Lorcast", | |
| 1188 | 2017 | "sourceType": "catalog", |
| 1189 | − "sourceUrl": "https://pokemontcg.io", | |
| 1190 | − "module": "api/pokemontcg", | |
| 2018 | + "sourceUrl": "https://lorcast.com", | |
| 2019 | + "module": "api/lorcast", | |
| 1191 | 2020 | "enginePriority": [ |
| 1192 | − "api", | |
| 1193 | − "feed" | |
| 2021 | + "api" | |
| 1194 | 2022 | ], |
| 1195 | 2023 | "categories": [ |
| 1196 | − "pokemon" | |
| 2024 | + "disney_lorcana" | |
| 1197 | 2025 | ], |
| 1198 | 2026 | "regions": [ |
| 1199 | 2027 | "US", |
@@ -1203,8 +2031,7 @@ | ||
| 1203 | 2031 | "en" |
| 1204 | 2032 | ], |
| 1205 | 2033 | "currency": [ |
| 1206 | − "USD", | |
| 1207 | − "EUR" | |
| 2034 | + "USD" | |
| 1208 | 2035 | ], |
| 1209 | 2036 | "supportsListings": false, |
| 1210 | 2037 | "supportsSold": false, |
@@ -1214,92 +2041,781 @@ | ||
| 1214 | 2041 | "supportsPopulation": false, |
| 1215 | 2042 | "supportsLookup": false, |
| 1216 | 2043 | "refreshFrequencyMinutes": 1440, |
| 1217 | − "priority": "high", | |
| 1218 | − "trustScore": 0.8, | |
| 2044 | + "priority": "medium", | |
| 2045 | + "trustScore": 0.75, | |
| 1219 | 2046 | "attributionRequired": true, |
| 1220 | − "termsUrl": "https://docs.pokemontcg.io/", | |
| 1221 | − "accessNotes": "Public REST API v2 (https://api.pokemontcg.io/v2). Optional X-Api-Key (POKEMONTCG_API_KEY) raises limits (20k/day) — without a key ~1k/day and 30/min. Crawl = sets, then cards per set (pageSize 250, cursor {setIndex,page}). The API is intermittently unavailable (5xx); the connector retries with backoff and falls back to the maintainers' public GitHub mirror (PokemonTCG/pokemon-tcg-data, same card schema, no prices) for the catalog when the API keeps failing. Prices are TCGplayer (USD, per printing variant: normal/holofoil/reverseHolofoil/1stEdition…) and Cardmarket (EUR) daily aggregates with their own updatedAt → stored as price_observations, never as sales. Pokémon © Nintendo/Creatures/GAME FREAK; data attributed to pokemontcg.io.", | |
| 2047 | + "termsUrl": "https://lorcast.com/docs/api", | |
| 2048 | + "accessNotes": "Open Lorcast REST API (v0, no key): /sets then /cards/search?q=set:<code>&page=N. Cards carry TCGplayer ids and USD market prices (normal / foil) without a timestamp, so observations are dated by the fetch day with confidence 0.7. Enchanted cards are emitted as a single 'Enchanted' variant. ~4 req/s self-throttled.", | |
| 1222 | 2049 | "enabled": true, |
| 1223 | 2050 | "schemaVersion": "1.0", |
| 1224 | − "config": { | |
| 1225 | − "pageSize": 250, | |
| 1226 | − "requestIntervalMs": 2100, | |
| 1227 | − "mirrorBase": "https://raw.githubusercontent.com/PokemonTCG/pokemon-tcg-data/master" | |
| 1228 | − } | |
| 2051 | + "config": {} | |
| 1229 | 2052 | }, |
| 1230 | 2053 | { |
| 1231 | − "id": "pricecharting", | |
| 1232 | − "displayName": "PriceCharting", | |
| 1233 | − "sourceId": "pricecharting", | |
| 1234 | − "sourceName": "PriceCharting", | |
| 1235 | − "sourceType": "pricing_guide", | |
| 1236 | − "sourceUrl": "https://www.pricecharting.com", | |
| 1237 | − "module": "api/pricecharting", | |
| 2054 | + "id": "lyon-turnbull", | |
| 2055 | + "displayName": "Lyon & Turnbull (auction results)", | |
| 2056 | + "sourceId": "lyon-turnbull", | |
| 2057 | + "sourceName": "Lyon & Turnbull", | |
| 2058 | + "sourceType": "auction_house", | |
| 2059 | + "sourceUrl": "https://www.lyonandturnbull.com", | |
| 2060 | + "module": "api/lyon-turnbull", | |
| 1238 | 2061 | "enginePriority": [ |
| 1239 | 2062 | "api", |
| 1240 | 2063 | "firecrawl", |
| 1241 | 2064 | "scrapfly" |
| 1242 | 2065 | ], |
| 1243 | 2066 | "categories": [ |
| 1244 | − "video_games", | |
| 1245 | − "nintendo_games", | |
| 1246 | − "sega_games", | |
| 1247 | − "playstation_games", | |
| 1248 | − "xbox_games", | |
| 1249 | − "atari_retro_games", | |
| 1250 | − "pc_games", | |
| 1251 | − "lego_sets", | |
| 1252 | − "funko", | |
| 1253 | − "comics", | |
| 1254 | − "marvel_comics", | |
| 1255 | − "dc_comics", | |
| 1256 | − "independent_comics", | |
| 1257 | − "trading_cards", | |
| 1258 | − "pokemon", | |
| 1259 | − "magic_the_gathering", | |
| 1260 | − "yugioh" | |
| 2067 | + "art", | |
| 2068 | + "contemporary_art", | |
| 2069 | + "photography", | |
| 2070 | + "design_furniture", | |
| 2071 | + "antiques", | |
| 2072 | + "jewelry", | |
| 2073 | + "silver", | |
| 2074 | + "watches", | |
| 2075 | + "other_watches", | |
| 2076 | + "books", | |
| 2077 | + "whisky", | |
| 2078 | + "porcelain", | |
| 2079 | + "glass_crystal", | |
| 2080 | + "clocks", | |
| 2081 | + "scientific_instruments" | |
| 1261 | 2082 | ], |
| 1262 | 2083 | "regions": [ |
| 1263 | − "US" | |
| 2084 | + "GB" | |
| 1264 | 2085 | ], |
| 1265 | 2086 | "languages": [ |
| 1266 | 2087 | "en" |
| 1267 | 2088 | ], |
| 1268 | 2089 | "currency": [ |
| 1269 | − "USD" | |
| 2090 | + "GBP" | |
| 1270 | 2091 | ], |
| 1271 | 2092 | "supportsListings": false, |
| 1272 | 2093 | "supportsSold": true, |
| 1273 | − "supportsAuctions": false, | |
| 2094 | + "supportsAuctions": true, | |
| 1274 | 2095 | "supportsImages": true, |
| 1275 | − "supportsCatalog": true, | |
| 2096 | + "supportsCatalog": false, | |
| 1276 | 2097 | "supportsPopulation": false, |
| 1277 | − "supportsLookup": true, | |
| 2098 | + "supportsLookup": false, | |
| 1278 | 2099 | "refreshFrequencyMinutes": 1440, |
| 1279 | − "priority": "high", | |
| 1280 | − "trustScore": 0.7, | |
| 2100 | + "priority": "medium", | |
| 2101 | + "trustScore": 0.9, | |
| 1281 | 2102 | "attributionRequired": true, |
| 1282 | − "termsUrl": "https://www.pricecharting.com/page/terms-of-service", | |
| 1283 | − "accessNotes": "Public product pages fetched over plain HTTPS with the RareIndex user agent (robots.txt only disallows /buy, /publish-offer, /stripe-connect). Each product page exposes PriceCharting guide values per condition and a table of recently completed eBay sales (date, title, price, eBay item id) — we store those as sales with the PriceCharting page as source URL. No account, no API key, no photos (TimeWarp is paid and not used). Console listings paginate through the public ?format=json cursor endpoint. ~1 request per product, 1.5 s politeness delay. Trading-card consoles (Pokémon, Magic, Yu-Gi-Oh!) are discovered from the public /category/<game>-cards pages; card pages expose guide values and eBay sales per grade (Ungraded, Grade 1–9.5, TAG/ACE/SGC/CGC/PSA/BGS 10, BGS 10 Black, CGC 10 Pristine). Graded rows without a named company use the generic grader 'graded' unless the eBay title names it. Pokémon sets are mapped to pokemontcg ids/codes through the maintainers' GitHub mirror and Magic sets/cards to Scryfall (one exact-name API lookup per Magic product, ≤ 10 req/s) so sales attach to the API catalogs' canonical assets.", | |
| 2103 | + "termsUrl": "https://www.lyonandturnbull.com/auctions/buy-sell/buy", | |
| 2104 | + "accessNotes": "Edinburgh/London/Glasgow auction house. Past auctions are listed in the public /auctions/past-auctions page (Next.js __NEXT_DATA__: title, sale number, date, link); each auction page embeds the full lot list as JSON (lot number, title, sub-title, estimate, hammer_price, status sold/unsold, images, buyer's premium tiers). Plain HTTPS with the RareIndex user agent; robots.txt disallows only /account, /auth, /search and collection_* paths. We store the HAMMER price (buyer_premium_included=false) and record the published premium tiers in metadata; sale date = auction session date. No login, no bidder data. 2 s politeness delay.", | |
| 1284 | 2105 | "enabled": true, |
| 1285 | − "schemaVersion": "2.0", | |
| 2106 | + "schemaVersion": "1.0", | |
| 1286 | 2107 | "config": { |
| 1287 | − "seeds": [ | |
| 1288 | − "nintendo-64", | |
| 1289 | − "nes", | |
| 1290 | − "super-nintendo", | |
| 1291 | − "gamecube", | |
| 1292 | − "gameboy", | |
| 1293 | − "gameboy-color", | |
| 1294 | − "gameboy-advance", | |
| 1295 | − "nintendo-ds", | |
| 1296 | − "nintendo-switch", | |
| 1297 | − "wii", | |
| 1298 | − "playstation", | |
| 1299 | − "playstation-2", | |
| 1300 | − "playstation-3", | |
| 1301 | − "playstation-4", | |
| 1302 | − "playstation-5", | |
| 2108 | + "pastAuctionsUrl": "https://www.lyonandturnbull.com/auctions/past-auctions", | |
| 2109 | + "auctionsPerRun": 3 | |
| 2110 | + } | |
| 2111 | + }, | |
| 2112 | + { | |
| 2113 | + "id": "ma-shops", | |
| 2114 | + "displayName": "MA-Shops (European coin, banknote & militaria dealer network)", | |
| 2115 | + "sourceId": "ma-shops", | |
| 2116 | + "sourceName": "MA-Shops", | |
| 2117 | + "sourceType": "marketplace", | |
| 2118 | + "sourceUrl": "https://www.ma-shops.com", | |
| 2119 | + "module": "api/ma-shops", | |
| 2120 | + "enginePriority": [ | |
| 2121 | + "api", | |
| 2122 | + "firecrawl" | |
| 2123 | + ], | |
| 2124 | + "categories": [ | |
| 2125 | + "coins", | |
| 2126 | + "banknotes", | |
| 2127 | + "medals", | |
| 2128 | + "militaria" | |
| 2129 | + ], | |
| 2130 | + "regions": [ | |
| 2131 | + "DE", | |
| 2132 | + "EU" | |
| 2133 | + ], | |
| 2134 | + "languages": [ | |
| 2135 | + "en", | |
| 2136 | + "de" | |
| 2137 | + ], | |
| 2138 | + "currency": [ | |
| 2139 | + "EUR", | |
| 2140 | + "USD", | |
| 2141 | + "CAD" | |
| 2142 | + ], | |
| 2143 | + "supportsListings": true, | |
| 2144 | + "supportsSold": false, | |
| 2145 | + "supportsAuctions": false, | |
| 2146 | + "supportsImages": true, | |
| 2147 | + "supportsCatalog": false, | |
| 2148 | + "supportsPopulation": false, | |
| 2149 | + "supportsLookup": false, | |
| 2150 | + "refreshFrequencyMinutes": 1440, | |
| 2151 | + "priority": "low", | |
| 2152 | + "trustScore": 0.75, | |
| 2153 | + "attributionRequired": true, | |
| 2154 | + "termsUrl": "https://www.ma-shops.com/shops/help.php?id=1", | |
| 2155 | + "accessNotes": "Public search gallery pages (ma-shops.com/search.php?keywords=…, 45 items per page; category landing pages only show a handful of featured items) fetched over plain HTTPS with the RareIndex user agent; robots.txt is fully permissive. Each gallery cell gives the dealer's item URL, title, price with the displayed currency (the site picks a currency per visitor — CAN$, US$ or EUR — which is parsed and stored as-is), the dealer name and the primary image. Emits dealer asks (fixed-price listings). Militaria items carry the taxonomy compliance flags (jurisdiction_check, restricted_symbols_review) and are presented neutrally.", | |
| 2156 | + "enabled": true, | |
| 2157 | + "schemaVersion": "1.0", | |
| 2158 | + "config": { | |
| 2159 | + "seeds": [ | |
| 2160 | + { | |
| 2161 | + "path": "/search.php?keywords=denarius", | |
| 2162 | + "categorySlug": "coins", | |
| 2163 | + "label": "Roman denarius" | |
| 2164 | + }, | |
| 2165 | + { | |
| 2166 | + "path": "/search.php?keywords=thaler", | |
| 2167 | + "categorySlug": "coins", | |
| 2168 | + "label": "Thaler" | |
| 2169 | + }, | |
| 2170 | + { | |
| 2171 | + "path": "/search.php?keywords=tetradrachm", | |
| 2172 | + "categorySlug": "coins", | |
| 2173 | + "label": "Greek tetradrachm" | |
| 2174 | + }, | |
| 2175 | + { | |
| 2176 | + "path": "/search.php?keywords=20+mark+gold", | |
| 2177 | + "categorySlug": "coins", | |
| 2178 | + "label": "German 20 Mark gold" | |
| 2179 | + }, | |
| 2180 | + { | |
| 2181 | + "path": "/search.php?keywords=sovereign", | |
| 2182 | + "categorySlug": "coins", | |
| 2183 | + "label": "Sovereign" | |
| 2184 | + }, | |
| 2185 | + { | |
| 2186 | + "path": "/search.php?keywords=banknote", | |
| 2187 | + "categorySlug": "banknotes", | |
| 2188 | + "label": "Banknotes" | |
| 2189 | + }, | |
| 2190 | + { | |
| 2191 | + "path": "/search.php?keywords=medaille", | |
| 2192 | + "categorySlug": "medals", | |
| 2193 | + "label": "Medals" | |
| 2194 | + } | |
| 2195 | + ], | |
| 2196 | + "pagesPerSeed": 1 | |
| 2197 | + } | |
| 2198 | + }, | |
| 2199 | + { | |
| 2200 | + "id": "mecum", | |
| 2201 | + "displayName": "Mecum Auctions (results)", | |
| 2202 | + "sourceId": "mecum", | |
| 2203 | + "sourceName": "Mecum Auctions", | |
| 2204 | + "sourceType": "auction_house", | |
| 2205 | + "sourceUrl": "https://www.mecum.com", | |
| 2206 | + "module": "firecrawl/mecum", | |
| 2207 | + "enginePriority": [ | |
| 2208 | + "firecrawl" | |
| 2209 | + ], | |
| 2210 | + "categories": [ | |
| 2211 | + "automobiles", | |
| 2212 | + "motorcycles", | |
| 2213 | + "automotive_memorabilia" | |
| 2214 | + ], | |
| 2215 | + "regions": [ | |
| 2216 | + "US" | |
| 2217 | + ], | |
| 2218 | + "languages": [ | |
| 2219 | + "en" | |
| 2220 | + ], | |
| 2221 | + "currency": [ | |
| 2222 | + "USD" | |
| 2223 | + ], | |
| 2224 | + "supportsListings": false, | |
| 2225 | + "supportsSold": true, | |
| 2226 | + "supportsAuctions": false, | |
| 2227 | + "supportsImages": true, | |
| 2228 | + "supportsCatalog": false, | |
| 2229 | + "supportsPopulation": false, | |
| 2230 | + "supportsLookup": false, | |
| 2231 | + "refreshFrequencyMinutes": 720, | |
| 2232 | + "priority": "high", | |
| 2233 | + "trustScore": 0.9, | |
| 2234 | + "attributionRequired": true, | |
| 2235 | + "termsUrl": "https://www.mecum.com/terms-and-conditions/", | |
| 2236 | + "accessNotes": "Public results: the /results/ page lists completed auctions (plain HTTPS), each auction's lot list (/auctions/<slug>/lots/?page=N, 24 lots per page) is rendered through Firecrawl because prices are client-rendered. robots.txt allows the pages (only /search/ is disallowed). Lots carrying the 'sold' badge with a price become sales; 'bid goes on'/unsold lots are skipped. Sale date = first day of the auction's date range (range kept in metadata). Mecum does not state on the results page whether displayed prices include the buyer's premium → buyer_premium_included=null (flagged for review). 1 credit per page, 2 s politeness delay.", | |
| 2237 | + "enabled": true, | |
| 2238 | + "schemaVersion": "1.0", | |
| 2239 | + "config": { | |
| 2240 | + "auctionsPerRun": 1, | |
| 2241 | + "lotPagesPerAuction": 10 | |
| 2242 | + } | |
| 2243 | + }, | |
| 2244 | + { | |
| 2245 | + "id": "mtggoldfish", | |
| 2246 | + "displayName": "MTGGoldfish (paper prices & history)", | |
| 2247 | + "sourceId": "mtggoldfish", | |
| 2248 | + "sourceName": "MTGGoldfish", | |
| 2249 | + "sourceType": "pricing_guide", | |
| 2250 | + "sourceUrl": "https://www.mtggoldfish.com", | |
| 2251 | + "module": "api/mtggoldfish", | |
| 2252 | + "enginePriority": [ | |
| 2253 | + "api", | |
| 2254 | + "firecrawl" | |
| 2255 | + ], | |
| 2256 | + "categories": [ | |
| 2257 | + "magic_the_gathering" | |
| 2258 | + ], | |
| 2259 | + "regions": [ | |
| 2260 | + "US" | |
| 2261 | + ], | |
| 2262 | + "languages": [ | |
| 2263 | + "en" | |
| 2264 | + ], | |
| 2265 | + "currency": [ | |
| 2266 | + "USD" | |
| 2267 | + ], | |
| 2268 | + "supportsListings": false, | |
| 2269 | + "supportsSold": false, | |
| 2270 | + "supportsAuctions": false, | |
| 2271 | + "supportsImages": true, | |
| 2272 | + "supportsCatalog": true, | |
| 2273 | + "supportsPopulation": false, | |
| 2274 | + "supportsLookup": true, | |
| 2275 | + "refreshFrequencyMinutes": 1440, | |
| 2276 | + "priority": "medium", | |
| 2277 | + "trustScore": 0.75, | |
| 2278 | + "attributionRequired": true, | |
| 2279 | + "termsUrl": "https://www.mtggoldfish.com/robots.txt", | |
| 2280 | + "accessNotes": "Public set pages (https://www.mtggoldfish.com/sets/<Set+Name>) embed a JSON payload with every printing (card_uuid, set code, collector number, finish, current paper/online price, images). robots.txt allows '/' for generic agents (only widgets/embeds are disallowed; Content-Signal ai-train=no is respected — data is used as market reference, not for training). Paper price = TCGplayer-derived market price → price_observation 'market' dated by fetch day (confidence 0.75). For cards above `history.minPrice` the public price-history component (/price_history_component, daily series since 2010) is fetched, capped per run → dated observations that give assets a real multi-year guide-price history. Politeness 1.5 s/page; ~340 sets.", | |
| 2281 | + "enabled": true, | |
| 2282 | + "schemaVersion": "1.0", | |
| 2283 | + "config": { | |
| 2284 | + "seeds": [], | |
| 2285 | + "maxSetsPerRun": 60, | |
| 2286 | + "history": { | |
| 2287 | + "enabled": true, | |
| 2288 | + "minPrice": 100, | |
| 2289 | + "maxPerRun": 150, | |
| 2290 | + "days": 1095 | |
| 2291 | + } | |
| 2292 | + } | |
| 2293 | + }, | |
| 2294 | + { | |
| 2295 | + "id": "novelship", | |
| 2296 | + "displayName": "Novelship (sneaker catalog, last sale & lowest ask)", | |
| 2297 | + "sourceId": "novelship", | |
| 2298 | + "sourceName": "Novelship", | |
| 2299 | + "sourceType": "marketplace", | |
| 2300 | + "sourceUrl": "https://novelship.com", | |
| 2301 | + "module": "api/novelship", | |
| 2302 | + "enginePriority": [ | |
| 2303 | + "api", | |
| 2304 | + "firecrawl", | |
| 2305 | + "scrapfly" | |
| 2306 | + ], | |
| 2307 | + "categories": [ | |
| 2308 | + "sneakers", | |
| 2309 | + "nike_jordan", | |
| 2310 | + "adidas_yeezy", | |
| 2311 | + "new_balance_asics_other" | |
| 2312 | + ], | |
| 2313 | + "regions": [ | |
| 2314 | + "SG", | |
| 2315 | + "AU", | |
| 2316 | + "NZ", | |
| 2317 | + "TW", | |
| 2318 | + "HK", | |
| 2319 | + "MY", | |
| 2320 | + "JP", | |
| 2321 | + "US" | |
| 2322 | + ], | |
| 2323 | + "languages": [ | |
| 2324 | + "en" | |
| 2325 | + ], | |
| 2326 | + "currency": [ | |
| 2327 | + "USD" | |
| 2328 | + ], | |
| 2329 | + "supportsListings": true, | |
| 2330 | + "supportsSold": false, | |
| 2331 | + "supportsAuctions": false, | |
| 2332 | + "supportsImages": true, | |
| 2333 | + "supportsCatalog": true, | |
| 2334 | + "supportsPopulation": false, | |
| 2335 | + "supportsLookup": true, | |
| 2336 | + "refreshFrequencyMinutes": 720, | |
| 2337 | + "priority": "medium", | |
| 2338 | + "trustScore": 0.7, | |
| 2339 | + "attributionRequired": true, | |
| 2340 | + "termsUrl": "https://novelship.com/terms", | |
| 2341 | + "accessNotes": "Public Novelship browse pages (novelship.com/sneakers/<brand>?page=N) and product pages fetched over plain HTTPS with the RareIndex user agent; robots.txt only disallows sell/auth/dashboard/pay paths. The server-rendered payload embeds each product's SKU (style code), colorway, retail cost, release date, last sale price, lowest listing price and 180-day sales count — no login or private endpoint is used. Prices are read as USD (the anonymous international storefront prices in US$; cost_retail matches US retail) with confidence 0.7; per-size asks are not exposed on listing pages. Sizes/stock are not fetched. 1.5 s politeness delay.", | |
| 2342 | + "enabled": true, | |
| 2343 | + "schemaVersion": "1.0", | |
| 2344 | + "config": { | |
| 2345 | + "seeds": [ | |
| 2346 | + "jordan", | |
| 2347 | + "nike", | |
| 2348 | + "adidas", | |
| 2349 | + "new-balance", | |
| 2350 | + "asics", | |
| 2351 | + "yeezy" | |
| 2352 | + ], | |
| 2353 | + "pagesPerSeed": 3 | |
| 2354 | + } | |
| 2355 | + }, | |
| 2356 | + { | |
| 2357 | + "id": "optcg", | |
| 2358 | + "displayName": "OPTCG API (One Piece Card Game)", | |
| 2359 | + "sourceId": "optcgapi", | |
| 2360 | + "sourceName": "OPTCG API", | |
| 2361 | + "sourceType": "catalog", | |
| 2362 | + "sourceUrl": "https://optcgapi.com", | |
| 2363 | + "module": "api/optcg", | |
| 2364 | + "enginePriority": [ | |
| 2365 | + "api" | |
| 2366 | + ], | |
| 2367 | + "categories": [ | |
| 2368 | + "one_piece_card_game" | |
| 2369 | + ], | |
| 2370 | + "regions": [ | |
| 2371 | + "US" | |
| 2372 | + ], | |
| 2373 | + "languages": [ | |
| 2374 | + "en" | |
| 2375 | + ], | |
| 2376 | + "currency": [ | |
| 2377 | + "USD" | |
| 2378 | + ], | |
| 2379 | + "supportsListings": false, | |
| 2380 | + "supportsSold": false, | |
| 2381 | + "supportsAuctions": false, | |
| 2382 | + "supportsImages": true, | |
| 2383 | + "supportsCatalog": true, | |
| 2384 | + "supportsPopulation": false, | |
| 2385 | + "supportsLookup": false, | |
| 2386 | + "refreshFrequencyMinutes": 1440, | |
| 2387 | + "priority": "medium", | |
| 2388 | + "trustScore": 0.65, | |
| 2389 | + "attributionRequired": true, | |
| 2390 | + "termsUrl": "https://optcgapi.com/", | |
| 2391 | + "accessNotes": "Open community API (no key): /api/allSets/ then /api/sets/<set_id>/ returning every card of the set with TCGplayer-derived market_price / inventory_price and a date_scraped, used as the observation date (confidence 0.7 / 0.65). Alternate arts share the card code and are distinguished by image id. One request per set, 0.5 s politeness.", | |
| 2392 | + "enabled": true, | |
| 2393 | + "schemaVersion": "1.0", | |
| 2394 | + "config": {} | |
| 2395 | + }, | |
| 2396 | + { | |
| 2397 | + "id": "pba-galleries", | |
| 2398 | + "displayName": "PBA Galleries (prices realized)", | |
| 2399 | + "sourceId": "pba-galleries", | |
| 2400 | + "sourceName": "PBA Galleries", | |
| 2401 | + "sourceType": "auction_house", | |
| 2402 | + "sourceUrl": "https://www.pbagalleries.com", | |
| 2403 | + "module": "firecrawl/pba-galleries", | |
| 2404 | + "enginePriority": [ | |
| 2405 | + "firecrawl" | |
| 2406 | + ], | |
| 2407 | + "categories": [ | |
| 2408 | + "books", | |
| 2409 | + "comics", | |
| 2410 | + "marvel_comics", | |
| 2411 | + "dc_comics", | |
| 2412 | + "independent_comics", | |
| 2413 | + "photography", | |
| 2414 | + "maps", | |
| 2415 | + "movie_posters", | |
| 2416 | + "art", | |
| 2417 | + "historical_documents" | |
| 2418 | + ], | |
| 2419 | + "regions": [ | |
| 2420 | + "US" | |
| 2421 | + ], | |
| 2422 | + "languages": [ | |
| 2423 | + "en" | |
| 2424 | + ], | |
| 2425 | + "currency": [ | |
| 2426 | + "USD" | |
| 2427 | + ], | |
| 2428 | + "supportsListings": false, | |
| 2429 | + "supportsSold": true, | |
| 2430 | + "supportsAuctions": false, | |
| 2431 | + "supportsImages": true, | |
| 2432 | + "supportsCatalog": false, | |
| 2433 | + "supportsPopulation": false, | |
| 2434 | + "supportsLookup": false, | |
| 2435 | + "refreshFrequencyMinutes": 1440, | |
| 2436 | + "priority": "medium", | |
| 2437 | + "trustScore": 0.85, | |
| 2438 | + "attributionRequired": true, | |
| 2439 | + "termsUrl": "https://www.pbagalleries.com/terms-conditions/", | |
| 2440 | + "accessNotes": "Public auction list (pbagalleries.com/auctions/?page=N: sale number, title, date, catalog id) and closed catalogs (/auctions/catalog/id/<id>?page=N, 'Sold for $X' + 'Status Sold' per lot with title/publisher/date fields) rendered through Firecrawl (plain HTTPS gets a Cloudflare interstitial; not bypassed; robots.txt allows the site). PBA's realized prices include the 25% buyer's premium (verified: every price is a hammer bid × 1.25) → buyer_premium_included=true. Category from the sale title (comics/books/photographs/maps/posters). 1 credit per page, 2 s politeness delay.", | |
| 2441 | + "enabled": true, | |
| 2442 | + "schemaVersion": "1.0", | |
| 2443 | + "config": { | |
| 2444 | + "auctionsPerRun": 2, | |
| 2445 | + "catalogPagesPerAuction": 5 | |
| 2446 | + } | |
| 2447 | + }, | |
| 2448 | + { | |
| 2449 | + "id": "pcgs-priceguide", | |
| 2450 | + "displayName": "PCGS Price Guide (US coins)", | |
| 2451 | + "sourceId": "pcgs", | |
| 2452 | + "sourceName": "PCGS", | |
| 2453 | + "sourceType": "grading_company", | |
| 2454 | + "sourceUrl": "https://www.pcgs.com", | |
| 2455 | + "module": "firecrawl/pcgs-priceguide", | |
| 2456 | + "enginePriority": [ | |
| 2457 | + "firecrawl", | |
| 2458 | + "scrapfly" | |
| 2459 | + ], | |
| 2460 | + "categories": [ | |
| 2461 | + "coins" | |
| 2462 | + ], | |
| 2463 | + "regions": [ | |
| 2464 | + "US" | |
| 2465 | + ], | |
| 2466 | + "languages": [ | |
| 2467 | + "en" | |
| 2468 | + ], | |
| 2469 | + "currency": [ | |
| 2470 | + "USD" | |
| 2471 | + ], | |
| 2472 | + "supportsListings": false, | |
| 2473 | + "supportsSold": false, | |
| 2474 | + "supportsAuctions": false, | |
| 2475 | + "supportsImages": false, | |
| 2476 | + "supportsCatalog": true, | |
| 2477 | + "supportsPopulation": false, | |
| 2478 | + "supportsLookup": false, | |
| 2479 | + "refreshFrequencyMinutes": 10080, | |
| 2480 | + "priority": "low", | |
| 2481 | + "trustScore": 0.9, | |
| 2482 | + "attributionRequired": true, | |
| 2483 | + "termsUrl": "https://www.pcgs.com/legal", | |
| 2484 | + "accessNotes": "Public PCGS Price Guide category pages (pcgs.com/prices/detail/<series>/<id>/most-active) — no login required; robots.txt does not restrict /prices. Plain HTTPS is refused by the CDN (403) so pages are rendered through Firecrawl (1 credit per category page ≈ 100 coins × 10 grades). Values are PCGS retail guide values in USD per grade (columns 4…70 and '+' grades), dated with the page's 'Last Update' stamp. Stored as guide_value observations with grader 'pcgs' and grade like MS65 / PR65 / MS65+; PCGS coin numbers are kept as identifiers (pcgs_number). Auction Prices Realized and the Population Report on pcgs.com require a Collectors account and are not fetched.", | |
| 2485 | + "enabled": true, | |
| 2486 | + "schemaVersion": "1.0", | |
| 2487 | + "config": { | |
| 2488 | + "seeds": [ | |
| 2489 | + "morgan-dollar/744", | |
| 2490 | + "peace-dollar/26", | |
| 2491 | + "lincoln-cent-wheat-reverse/46", | |
| 2492 | + "indian-cent/44", | |
| 2493 | + "buffalo-nickel/83", | |
| 2494 | + "mercury-dime/703", | |
| 2495 | + "walking-liberty-half-dollar/733", | |
| 2496 | + "standing-liberty-quarter/111", | |
| 2497 | + "franklin-half-dollar/734", | |
| 2498 | + "kennedy-half-dollar/125", | |
| 2499 | + "washington-quarter/112", | |
| 2500 | + "trade-dollar/743", | |
| 2501 | + "flying-eagle-cent/664", | |
| 2502 | + "barber-half-dollar/732", | |
| 2503 | + "silver-eagles/939", | |
| 2504 | + "liberty-seated-dollar/29" | |
| 2505 | + ], | |
| 2506 | + "designations": [ | |
| 2507 | + "ms" | |
| 2508 | + ] | |
| 2509 | + } | |
| 2510 | + }, | |
| 2511 | + { | |
| 2512 | + "id": "phillips-art", | |
| 2513 | + "displayName": "Phillips Art, Editions, Design & Photographs (auction results)", | |
| 2514 | + "sourceId": "phillips", | |
| 2515 | + "sourceName": "Phillips", | |
| 2516 | + "sourceType": "auction_house", | |
| 2517 | + "sourceUrl": "https://www.phillips.com", | |
| 2518 | + "module": "firecrawl/phillips-art", | |
| 2519 | + "enginePriority": [ | |
| 2520 | + "firecrawl", | |
| 2521 | + "scrapfly" | |
| 2522 | + ], | |
| 2523 | + "categories": [ | |
| 2524 | + "art", | |
| 2525 | + "contemporary_art", | |
| 2526 | + "photography", | |
| 2527 | + "design_furniture", | |
| 2528 | + "jewelry", | |
| 2529 | + "luxury_handbags" | |
| 2530 | + ], | |
| 2531 | + "regions": [ | |
| 2532 | + "US", | |
| 2533 | + "GB", | |
| 2534 | + "HK", | |
| 2535 | + "CH" | |
| 2536 | + ], | |
| 2537 | + "languages": [ | |
| 2538 | + "en" | |
| 2539 | + ], | |
| 2540 | + "currency": [ | |
| 2541 | + "USD", | |
| 2542 | + "GBP", | |
| 2543 | + "HKD", | |
| 2544 | + "CHF", | |
| 2545 | + "EUR" | |
| 2546 | + ], | |
| 2547 | + "supportsListings": false, | |
| 2548 | + "supportsSold": true, | |
| 2549 | + "supportsAuctions": true, | |
| 2550 | + "supportsImages": true, | |
| 2551 | + "supportsCatalog": false, | |
| 2552 | + "supportsPopulation": false, | |
| 2553 | + "supportsLookup": false, | |
| 2554 | + "refreshFrequencyMinutes": 10080, | |
| 2555 | + "priority": "medium", | |
| 2556 | + "trustScore": 0.95, | |
| 2557 | + "attributionRequired": true, | |
| 2558 | + "termsUrl": "https://www.phillips.com/about/terms", | |
| 2559 | + "accessNotes": "Same public mechanism as phillips-watches (past-sales JSON on /auctions/past over plain HTTPS, then one Firecrawl render per sale page, 1 credit) applied to the Editions, Design, 20th Century & Contemporary Art, Photographs, Jewels and Handbags departments. Prices realised are published by Phillips including buyer's premium (buyer_premium_included=true). Sale date = sale end date. robots.txt disallows only /search and filter paths; no login, no bidder data. 3 s politeness delay; salesPerRun caps each run.", | |
| 2560 | + "enabled": true, | |
| 2561 | + "schemaVersion": "1.0", | |
| 2562 | + "config": { | |
| 2563 | + "pastAuctionsUrl": "https://www.phillips.com/auctions/past", | |
| 2564 | + "salesPerRun": 4, | |
| 2565 | + "departments": [ | |
| 2566 | + { | |
| 2567 | + "filter": "editions", | |
| 2568 | + "category": "contemporary_art" | |
| 2569 | + }, | |
| 2570 | + { | |
| 2571 | + "filter": "design", | |
| 2572 | + "category": "design_furniture" | |
| 2573 | + }, | |
| 2574 | + { | |
| 2575 | + "filter": "photograph", | |
| 2576 | + "category": "photography" | |
| 2577 | + }, | |
| 2578 | + { | |
| 2579 | + "filter": "contemporary art", | |
| 2580 | + "category": "contemporary_art" | |
| 2581 | + }, | |
| 2582 | + { | |
| 2583 | + "filter": "jewels", | |
| 2584 | + "category": "jewelry" | |
| 2585 | + }, | |
| 2586 | + { | |
| 2587 | + "filter": "handbags", | |
| 2588 | + "category": "luxury_handbags" | |
| 2589 | + } | |
| 2590 | + ] | |
| 2591 | + } | |
| 2592 | + }, | |
| 2593 | + { | |
| 2594 | + "id": "phillips-watches", | |
| 2595 | + "displayName": "Phillips Watches (auction results)", | |
| 2596 | + "sourceId": "phillips", | |
| 2597 | + "sourceName": "Phillips", | |
| 2598 | + "sourceType": "auction_house", | |
| 2599 | + "sourceUrl": "https://www.phillips.com", | |
| 2600 | + "module": "firecrawl/phillips-watches", | |
| 2601 | + "enginePriority": [ | |
| 2602 | + "firecrawl", | |
| 2603 | + "scrapfly" | |
| 2604 | + ], | |
| 2605 | + "categories": [ | |
| 2606 | + "watches", | |
| 2607 | + "rolex", | |
| 2608 | + "patek_philippe", | |
| 2609 | + "audemars_piguet", | |
| 2610 | + "omega", | |
| 2611 | + "other_watches" | |
| 2612 | + ], | |
| 2613 | + "regions": [ | |
| 2614 | + "CH", | |
| 2615 | + "US", | |
| 2616 | + "HK", | |
| 2617 | + "GB" | |
| 2618 | + ], | |
| 2619 | + "languages": [ | |
| 2620 | + "en" | |
| 2621 | + ], | |
| 2622 | + "currency": [ | |
| 2623 | + "CHF", | |
| 2624 | + "USD", | |
| 2625 | + "HKD", | |
| 2626 | + "GBP", | |
| 2627 | + "EUR" | |
| 2628 | + ], | |
| 2629 | + "supportsListings": false, | |
| 2630 | + "supportsSold": true, | |
| 2631 | + "supportsAuctions": true, | |
| 2632 | + "supportsImages": true, | |
| 2633 | + "supportsCatalog": false, | |
| 2634 | + "supportsPopulation": false, | |
| 2635 | + "supportsLookup": false, | |
| 2636 | + "refreshFrequencyMinutes": 10080, | |
| 2637 | + "priority": "high", | |
| 2638 | + "trustScore": 0.95, | |
| 2639 | + "attributionRequired": true, | |
| 2640 | + "termsUrl": "https://www.phillips.com/about/terms", | |
| 2641 | + "accessNotes": "Past watch sales are discovered from the public /auctions/past page (plain HTTPS; embedded JSON with saleNumber, title, end date, location). Each sale page is rendered through Firecrawl (JS wait ~6 s, 1 credit) to read the public lot list: lot number, maker, reference, model, estimate and 'Sold For' (Phillips publishes prices realised including buyer's premium). robots.txt disallows only /search and filter paths. No login, no bidder data. Large sales may lazy-load beyond the first ~70 lots — coverage is recorded per run.", | |
| 2642 | + "enabled": true, | |
| 2643 | + "schemaVersion": "1.0", | |
| 2644 | + "config": { | |
| 2645 | + "pastAuctionsUrl": "https://www.phillips.com/auctions/past", | |
| 2646 | + "salesPerRun": 6, | |
| 2647 | + "titleFilter": "watch" | |
| 2648 | + } | |
| 2649 | + }, | |
| 2650 | + { | |
| 2651 | + "id": "pokemonprice", | |
| 2652 | + "displayName": "PokemonPrice (graded Pokémon price guide)", | |
| 2653 | + "sourceId": "pokemonprice", | |
| 2654 | + "sourceName": "PokemonPrice.com", | |
| 2655 | + "sourceType": "pricing_guide", | |
| 2656 | + "sourceUrl": "https://www.pokemonprice.com", | |
| 2657 | + "module": "api/pokemonprice", | |
| 2658 | + "enginePriority": [ | |
| 2659 | + "api", | |
| 2660 | + "firecrawl" | |
| 2661 | + ], | |
| 2662 | + "categories": [ | |
| 2663 | + "pokemon" | |
| 2664 | + ], | |
| 2665 | + "regions": [ | |
| 2666 | + "US" | |
| 2667 | + ], | |
| 2668 | + "languages": [ | |
| 2669 | + "en" | |
| 2670 | + ], | |
| 2671 | + "currency": [ | |
| 2672 | + "USD" | |
| 2673 | + ], | |
| 2674 | + "supportsListings": false, | |
| 2675 | + "supportsSold": false, | |
| 2676 | + "supportsAuctions": false, | |
| 2677 | + "supportsImages": true, | |
| 2678 | + "supportsCatalog": true, | |
| 2679 | + "supportsPopulation": false, | |
| 2680 | + "supportsLookup": true, | |
| 2681 | + "refreshFrequencyMinutes": 2880, | |
| 2682 | + "priority": "medium", | |
| 2683 | + "trustScore": 0.7, | |
| 2684 | + "attributionRequired": true, | |
| 2685 | + "termsUrl": "https://www.pokemonprice.com/about-us", | |
| 2686 | + "accessNotes": "Public server-rendered card pages (robots.txt allows '/' for generic agents; Content-Signal ai-train=no respected). Each card page embeds, in its React Server Components payload, a per-grade price model (Raw, PSA 1–10, BGS, CGC: fair/low/high price, model confidence, last sale date) derived from eBay and other public sold listings, plus monthly transaction counts. Emitted as price_observations 'guide_value' (+ low/high) per grade with the model confidence (capped 0.8) and the monthly transaction count as sampleSize — not transactions. Set naming follows pokemontcg ('Base Set', 'Jungle'…); edition suffixes (1st Edition, Shadowless) become the variant so assets merge with the API catalogs. ~167 set lists, ~25k card pages; politeness 1.5 s, `maxCardsPerRun` caps a run and the cursor rotates through sets.", | |
| 2687 | + "enabled": true, | |
| 2688 | + "schemaVersion": "1.0", | |
| 2689 | + "config": { | |
| 2690 | + "seeds": [], | |
| 2691 | + "maxCardsPerRun": 300, | |
| 2692 | + "priceKinds": [ | |
| 2693 | + "guide_value", | |
| 2694 | + "low", | |
| 2695 | + "high" | |
| 2696 | + ] | |
| 2697 | + } | |
| 2698 | + }, | |
| 2699 | + { | |
| 2700 | + "id": "pokemontcg", | |
| 2701 | + "displayName": "Pokémon TCG API (pokemontcg.io)", | |
| 2702 | + "sourceId": "pokemontcg", | |
| 2703 | + "sourceName": "Pokémon TCG API", | |
| 2704 | + "sourceType": "catalog", | |
| 2705 | + "sourceUrl": "https://pokemontcg.io", | |
| 2706 | + "module": "api/pokemontcg", | |
| 2707 | + "enginePriority": [ | |
| 2708 | + "api", | |
| 2709 | + "feed" | |
| 2710 | + ], | |
| 2711 | + "categories": [ | |
| 2712 | + "pokemon" | |
| 2713 | + ], | |
| 2714 | + "regions": [ | |
| 2715 | + "US", | |
| 2716 | + "EU" | |
| 2717 | + ], | |
| 2718 | + "languages": [ | |
| 2719 | + "en" | |
| 2720 | + ], | |
| 2721 | + "currency": [ | |
| 2722 | + "USD", | |
| 2723 | + "EUR" | |
| 2724 | + ], | |
| 2725 | + "supportsListings": false, | |
| 2726 | + "supportsSold": false, | |
| 2727 | + "supportsAuctions": false, | |
| 2728 | + "supportsImages": true, | |
| 2729 | + "supportsCatalog": true, | |
| 2730 | + "supportsPopulation": false, | |
| 2731 | + "supportsLookup": false, | |
| 2732 | + "refreshFrequencyMinutes": 1440, | |
| 2733 | + "priority": "high", | |
| 2734 | + "trustScore": 0.8, | |
| 2735 | + "attributionRequired": true, | |
| 2736 | + "termsUrl": "https://docs.pokemontcg.io/", | |
| 2737 | + "accessNotes": "Public REST API v2 (https://api.pokemontcg.io/v2). Optional X-Api-Key (POKEMONTCG_API_KEY) raises limits (20k/day) — without a key ~1k/day and 30/min. Crawl = sets, then cards per set (pageSize 250, cursor {setIndex,page}). The API is intermittently unavailable (5xx); the connector retries with backoff and falls back to the maintainers' public GitHub mirror (PokemonTCG/pokemon-tcg-data, same card schema, no prices) for the catalog when the API keeps failing. Prices are TCGplayer (USD, per printing variant: normal/holofoil/reverseHolofoil/1stEdition…) and Cardmarket (EUR) daily aggregates with their own updatedAt → stored as price_observations, never as sales. Pokémon © Nintendo/Creatures/GAME FREAK; data attributed to pokemontcg.io.", | |
| 2738 | + "enabled": true, | |
| 2739 | + "schemaVersion": "1.0", | |
| 2740 | + "config": { | |
| 2741 | + "pageSize": 250, | |
| 2742 | + "requestIntervalMs": 2100, | |
| 2743 | + "mirrorBase": "https://raw.githubusercontent.com/PokemonTCG/pokemon-tcg-data/master" | |
| 2744 | + } | |
| 2745 | + }, | |
| 2746 | + { | |
| 2747 | + "id": "pricecharting", | |
| 2748 | + "displayName": "PriceCharting", | |
| 2749 | + "sourceId": "pricecharting", | |
| 2750 | + "sourceName": "PriceCharting", | |
| 2751 | + "sourceType": "pricing_guide", | |
| 2752 | + "sourceUrl": "https://www.pricecharting.com", | |
| 2753 | + "module": "api/pricecharting", | |
| 2754 | + "enginePriority": [ | |
| 2755 | + "api", | |
| 2756 | + "firecrawl", | |
| 2757 | + "scrapfly" | |
| 2758 | + ], | |
| 2759 | + "categories": [ | |
| 2760 | + "video_games", | |
| 2761 | + "nintendo_games", | |
| 2762 | + "sega_games", | |
| 2763 | + "playstation_games", | |
| 2764 | + "xbox_games", | |
| 2765 | + "atari_retro_games", | |
| 2766 | + "pc_games", | |
| 2767 | + "lego_sets", | |
| 2768 | + "funko", | |
| 2769 | + "comics", | |
| 2770 | + "marvel_comics", | |
| 2771 | + "dc_comics", | |
| 2772 | + "independent_comics", | |
| 2773 | + "trading_cards", | |
| 2774 | + "pokemon", | |
| 2775 | + "magic_the_gathering", | |
| 2776 | + "yugioh" | |
| 2777 | + ], | |
| 2778 | + "regions": [ | |
| 2779 | + "US" | |
| 2780 | + ], | |
| 2781 | + "languages": [ | |
| 2782 | + "en" | |
| 2783 | + ], | |
| 2784 | + "currency": [ | |
| 2785 | + "USD" | |
| 2786 | + ], | |
| 2787 | + "supportsListings": false, | |
| 2788 | + "supportsSold": true, | |
| 2789 | + "supportsAuctions": false, | |
| 2790 | + "supportsImages": true, | |
| 2791 | + "supportsCatalog": true, | |
| 2792 | + "supportsPopulation": false, | |
| 2793 | + "supportsLookup": true, | |
| 2794 | + "refreshFrequencyMinutes": 1440, | |
| 2795 | + "priority": "high", | |
| 2796 | + "trustScore": 0.7, | |
| 2797 | + "attributionRequired": true, | |
| 2798 | + "termsUrl": "https://www.pricecharting.com/page/terms-of-service", | |
| 2799 | + "accessNotes": "Public product pages fetched over plain HTTPS with the RareIndex user agent (robots.txt only disallows /buy, /publish-offer, /stripe-connect). Each product page exposes PriceCharting guide values per condition and a table of recently completed eBay sales (date, title, price, eBay item id) — we store those as sales with the PriceCharting page as source URL. No account, no API key, no photos (TimeWarp is paid and not used). Console listings paginate through the public ?format=json cursor endpoint. ~1 request per product, 1.5 s politeness delay. Trading-card consoles (Pokémon, Magic, Yu-Gi-Oh!) are discovered from the public /category/<game>-cards pages; card pages expose guide values and eBay sales per grade (Ungraded, Grade 1–9.5, TAG/ACE/SGC/CGC/PSA/BGS 10, BGS 10 Black, CGC 10 Pristine). Graded rows without a named company use the generic grader 'graded' unless the eBay title names it. Pokémon sets are mapped to pokemontcg ids/codes through the maintainers' GitHub mirror and Magic sets/cards to Scryfall (one exact-name API lookup per Magic product, ≤ 10 req/s) so sales attach to the API catalogs' canonical assets.", | |
| 2800 | + "enabled": true, | |
| 2801 | + "schemaVersion": "2.0", | |
| 2802 | + "config": { | |
| 2803 | + "seeds": [ | |
| 2804 | + "nintendo-64", | |
| 2805 | + "nes", | |
| 2806 | + "super-nintendo", | |
| 2807 | + "gamecube", | |
| 2808 | + "gameboy", | |
| 2809 | + "gameboy-color", | |
| 2810 | + "gameboy-advance", | |
| 2811 | + "nintendo-ds", | |
| 2812 | + "nintendo-switch", | |
| 2813 | + "wii", | |
| 2814 | + "playstation", | |
| 2815 | + "playstation-2", | |
| 2816 | + "playstation-3", | |
| 2817 | + "playstation-4", | |
| 2818 | + "playstation-5", | |
| 1303 | 2819 | "psp", |
| 1304 | 2820 | "xbox", |
| 1305 | 2821 | "xbox-360", |
@@ -1374,6 +2890,300 @@ | ||
| 1374 | 2890 | "maxConsolesPerCategory": 30 |
| 1375 | 2891 | } |
| 1376 | 2892 | }, |
| 2893 | + { | |
| 2894 | + "id": "rebag", | |
| 2895 | + "displayName": "Rebag (pre-owned luxury bags, watches & jewelry listings)", | |
| 2896 | + "sourceId": "rebag", | |
| 2897 | + "sourceName": "Rebag", | |
| 2898 | + "sourceType": "marketplace", | |
| 2899 | + "sourceUrl": "https://shop.rebag.com", | |
| 2900 | + "module": "api/rebag", | |
| 2901 | + "enginePriority": [ | |
| 2902 | + "api" | |
| 2903 | + ], | |
| 2904 | + "categories": [ | |
| 2905 | + "luxury_handbags", | |
| 2906 | + "watches", | |
| 2907 | + "rolex", | |
| 2908 | + "omega", | |
| 2909 | + "other_watches", | |
| 2910 | + "jewelry", | |
| 2911 | + "fashion_streetwear" | |
| 2912 | + ], | |
| 2913 | + "regions": [ | |
| 2914 | + "US" | |
| 2915 | + ], | |
| 2916 | + "languages": [ | |
| 2917 | + "en" | |
| 2918 | + ], | |
| 2919 | + "currency": [ | |
| 2920 | + "USD" | |
| 2921 | + ], | |
| 2922 | + "supportsListings": true, | |
| 2923 | + "supportsSold": false, | |
| 2924 | + "supportsAuctions": false, | |
| 2925 | + "supportsImages": true, | |
| 2926 | + "supportsCatalog": false, | |
| 2927 | + "supportsPopulation": false, | |
| 2928 | + "supportsLookup": true, | |
| 2929 | + "refreshFrequencyMinutes": 720, | |
| 2930 | + "priority": "medium", | |
| 2931 | + "trustScore": 0.8, | |
| 2932 | + "attributionRequired": true, | |
| 2933 | + "termsUrl": "https://www.rebag.com/terms-of-service/", | |
| 2934 | + "accessNotes": "Public Shopify storefront feed (shop.rebag.com/collections/<handle>/products.json) over plain HTTPS with the RareIndex user agent; rebag.com robots.txt disallows nothing relevant (only /digital_certificate/). Each product is one authenticated pre-owned item in USD; Rebag's condition grade (Pristine/Excellent/Great/Good/Fair) is the first token of the variant title and the estimated retail price comes from the description. 1.5 s between requests.", | |
| 2935 | + "enabled": true, | |
| 2936 | + "schemaVersion": "1.0", | |
| 2937 | + "config": { | |
| 2938 | + "seeds": [ | |
| 2939 | + "hermes", | |
| 2940 | + "chanel", | |
| 2941 | + "louis-vuitton", | |
| 2942 | + "dior", | |
| 2943 | + "gucci", | |
| 2944 | + "goyard", | |
| 2945 | + "fendi", | |
| 2946 | + "bottega-veneta", | |
| 2947 | + "prada", | |
| 2948 | + "celine", | |
| 2949 | + "loewe", | |
| 2950 | + "saint-laurent", | |
| 2951 | + "all-watches", | |
| 2952 | + "rolex", | |
| 2953 | + "omega", | |
| 2954 | + "cartier", | |
| 2955 | + "van-cleef-arpels" | |
| 2956 | + ], | |
| 2957 | + "pagesPerSeed": 2 | |
| 2958 | + } | |
| 2959 | + }, | |
| 2960 | + { | |
| 2961 | + "id": "rm-sothebys", | |
| 2962 | + "displayName": "RM Sotheby's (results)", | |
| 2963 | + "sourceId": "rm-sothebys", | |
| 2964 | + "sourceName": "RM Sotheby's", | |
| 2965 | + "sourceType": "auction_house", | |
| 2966 | + "sourceUrl": "https://rmsothebys.com", | |
| 2967 | + "module": "firecrawl/rm-sothebys", | |
| 2968 | + "enginePriority": [ | |
| 2969 | + "firecrawl" | |
| 2970 | + ], | |
| 2971 | + "categories": [ | |
| 2972 | + "automobiles", | |
| 2973 | + "motorcycles", | |
| 2974 | + "automotive_memorabilia" | |
| 2975 | + ], | |
| 2976 | + "regions": [ | |
| 2977 | + "US", | |
| 2978 | + "EU", | |
| 2979 | + "GB", | |
| 2980 | + "AE" | |
| 2981 | + ], | |
| 2982 | + "languages": [ | |
| 2983 | + "en" | |
| 2984 | + ], | |
| 2985 | + "currency": [ | |
| 2986 | + "USD", | |
| 2987 | + "EUR", | |
| 2988 | + "GBP", | |
| 2989 | + "CHF", | |
| 2990 | + "AED" | |
| 2991 | + ], | |
| 2992 | + "supportsListings": false, | |
| 2993 | + "supportsSold": true, | |
| 2994 | + "supportsAuctions": false, | |
| 2995 | + "supportsImages": true, | |
| 2996 | + "supportsCatalog": false, | |
| 2997 | + "supportsPopulation": false, | |
| 2998 | + "supportsLookup": false, | |
| 2999 | + "refreshFrequencyMinutes": 1440, | |
| 3000 | + "priority": "medium", | |
| 3001 | + "trustScore": 0.9, | |
| 3002 | + "attributionRequired": true, | |
| 3003 | + "termsUrl": "https://rmsothebys.com/terms-and-conditions/", | |
| 3004 | + "accessNotes": "Public results: /results/ (auction list per year with date ranges) and each auction's lot grid (/auctions/<code>/lots/) rendered through Firecrawl because RM's site is an Angular app (no robots.txt is served; nothing is login-gated). Limitation: the lot grid paginates client-side (40 lots per view) and does not expose a page URL, so only the first 40 lots of each auction are captured per run — partial coverage, stated in the data. Lots marked 'Sold' with a price become sales; 'Not Sold' lots are skipped. RM publishes results inclusive of buyer's premium → buyer_premium_included=true. Sale date = first day of the auction (range in metadata). 1 credit per page, 2 s politeness delay.", | |
| 3005 | + "enabled": true, | |
| 3006 | + "schemaVersion": "1.0", | |
| 3007 | + "config": { | |
| 3008 | + "auctionsPerRun": 3 | |
| 3009 | + } | |
| 3010 | + }, | |
| 3011 | + { | |
| 3012 | + "id": "rr-auction", | |
| 3013 | + "displayName": "RR Auction (past auctions)", | |
| 3014 | + "sourceId": "rr-auction", | |
| 3015 | + "sourceName": "RR Auction", | |
| 3016 | + "sourceType": "auction_house", | |
| 3017 | + "sourceUrl": "https://www.rrauction.com", | |
| 3018 | + "module": "firecrawl/rr-auction", | |
| 3019 | + "enginePriority": [ | |
| 3020 | + "firecrawl" | |
| 3021 | + ], | |
| 3022 | + "categories": [ | |
| 3023 | + "space", | |
| 3024 | + "autographs", | |
| 3025 | + "historical_documents", | |
| 3026 | + "apple_collectibles", | |
| 3027 | + "music_memorabilia", | |
| 3028 | + "sports_memorabilia", | |
| 3029 | + "movie_memorabilia", | |
| 3030 | + "meteorites", | |
| 3031 | + "vintage_computers", | |
| 3032 | + "animation_art", | |
| 3033 | + "aviation" | |
| 3034 | + ], | |
| 3035 | + "regions": [ | |
| 3036 | + "US" | |
| 3037 | + ], | |
| 3038 | + "languages": [ | |
| 3039 | + "en" | |
| 3040 | + ], | |
| 3041 | + "currency": [ | |
| 3042 | + "USD" | |
| 3043 | + ], | |
| 3044 | + "supportsListings": false, | |
| 3045 | + "supportsSold": true, | |
| 3046 | + "supportsAuctions": false, | |
| 3047 | + "supportsImages": true, | |
| 3048 | + "supportsCatalog": false, | |
| 3049 | + "supportsPopulation": false, | |
| 3050 | + "supportsLookup": false, | |
| 3051 | + "refreshFrequencyMinutes": 720, | |
| 3052 | + "priority": "high", | |
| 3053 | + "trustScore": 0.9, | |
| 3054 | + "attributionRequired": true, | |
| 3055 | + "termsUrl": "https://www.rrauction.com/terms-and-conditions", | |
| 3056 | + "accessNotes": "Two public pages rendered through Firecrawl (plain HTTPS gets a Cloudflare interstitial; not bypassed; robots.txt allows everything except /admin): the past-auction calendar (/auctions/auction-calendar/cron/past/?page=N: auction id, title, date, realized total) and each auction's lot gallery (/auctions/auction-details/<id>?page=N&itemQty=96&view=gallery&sort=time&cat=0) whose cards show 'Sold For: $X (w/BP)', estimate and the auction date. RR's published prices explicitly include the buyer's premium → buyer_premium_included=true. Categories come from the auction title and lot title keywords. 1 credit per page (96 lots), 2 s politeness delay.", | |
| 3057 | + "enabled": true, | |
| 3058 | + "schemaVersion": "1.0", | |
| 3059 | + "config": { | |
| 3060 | + "auctionsPerRun": 2, | |
| 3061 | + "lotPagesPerAuction": 6, | |
| 3062 | + "calendarPagesPerRun": 1 | |
| 3063 | + } | |
| 3064 | + }, | |
| 3065 | + { | |
| 3066 | + "id": "saatchi-art", | |
| 3067 | + "displayName": "Saatchi Art (original artworks for sale)", | |
| 3068 | + "sourceId": "saatchi-art", | |
| 3069 | + "sourceName": "Saatchi Art", | |
| 3070 | + "sourceType": "marketplace", | |
| 3071 | + "sourceUrl": "https://www.saatchiart.com", | |
| 3072 | + "module": "api/saatchi-art", | |
| 3073 | + "enginePriority": [ | |
| 3074 | + "api", | |
| 3075 | + "firecrawl", | |
| 3076 | + "scrapfly" | |
| 3077 | + ], | |
| 3078 | + "categories": [ | |
| 3079 | + "art", | |
| 3080 | + "contemporary_art", | |
| 3081 | + "photography" | |
| 3082 | + ], | |
| 3083 | + "regions": [ | |
| 3084 | + "US", | |
| 3085 | + "GB", | |
| 3086 | + "EU" | |
| 3087 | + ], | |
| 3088 | + "languages": [ | |
| 3089 | + "en" | |
| 3090 | + ], | |
| 3091 | + "currency": [ | |
| 3092 | + "USD" | |
| 3093 | + ], | |
| 3094 | + "supportsListings": true, | |
| 3095 | + "supportsSold": false, | |
| 3096 | + "supportsAuctions": false, | |
| 3097 | + "supportsImages": true, | |
| 3098 | + "supportsCatalog": true, | |
| 3099 | + "supportsPopulation": false, | |
| 3100 | + "supportsLookup": true, | |
| 3101 | + "refreshFrequencyMinutes": 1440, | |
| 3102 | + "priority": "medium", | |
| 3103 | + "trustScore": 0.7, | |
| 3104 | + "attributionRequired": true, | |
| 3105 | + "termsUrl": "https://www.saatchiart.com/termsofservice", | |
| 3106 | + "accessNotes": "Public category browse pages (/paintings, /photography, /sculpture, /prints … with ?page=N) served as Next.js pages whose __NEXT_DATA__ carries 25 artworks per page: title, artist, category, mediums, materials, dimensions (cm), list price (USD cents), availability status (avail/sold), SKU and image. Plain HTTPS with the RareIndex user agent. robots.txt only disallows filter/sort query parameters (?category=, ?sort= …) which we do not use — pagination via ?page= is allowed. Listing asks are NOT transactions; sold works keep their last list price as a listing marked sold (no sale date is published, so no sale record is created). 2 s politeness delay.", | |
| 3107 | + "enabled": true, | |
| 3108 | + "schemaVersion": "1.0", | |
| 3109 | + "config": { | |
| 3110 | + "seeds": [ | |
| 3111 | + { | |
| 3112 | + "path": "/paintings", | |
| 3113 | + "category": "contemporary_art" | |
| 3114 | + }, | |
| 3115 | + { | |
| 3116 | + "path": "/photography", | |
| 3117 | + "category": "photography" | |
| 3118 | + }, | |
| 3119 | + { | |
| 3120 | + "path": "/sculpture", | |
| 3121 | + "category": "contemporary_art" | |
| 3122 | + }, | |
| 3123 | + { | |
| 3124 | + "path": "/prints", | |
| 3125 | + "category": "contemporary_art" | |
| 3126 | + }, | |
| 3127 | + { | |
| 3128 | + "path": "/drawings", | |
| 3129 | + "category": "contemporary_art" | |
| 3130 | + }, | |
| 3131 | + { | |
| 3132 | + "path": "/collage", | |
| 3133 | + "category": "contemporary_art" | |
| 3134 | + } | |
| 3135 | + ], | |
| 3136 | + "pagesPerSeed": 4 | |
| 3137 | + } | |
| 3138 | + }, | |
| 3139 | + { | |
| 3140 | + "id": "scotch-whisky-auctions", | |
| 3141 | + "displayName": "Scotch Whisky Auctions (results)", | |
| 3142 | + "sourceId": "scotch-whisky-auctions", | |
| 3143 | + "sourceName": "Scotch Whisky Auctions", | |
| 3144 | + "sourceType": "auction_house", | |
| 3145 | + "sourceUrl": "https://www.scotchwhiskyauctions.com", | |
| 3146 | + "module": "api/scotch-whisky-auctions", | |
| 3147 | + "enginePriority": [ | |
| 3148 | + "api", | |
| 3149 | + "firecrawl", | |
| 3150 | + "scrapfly" | |
| 3151 | + ], | |
| 3152 | + "categories": [ | |
| 3153 | + "whisky", | |
| 3154 | + "rum", | |
| 3155 | + "cognac" | |
| 3156 | + ], | |
| 3157 | + "regions": [ | |
| 3158 | + "GB" | |
| 3159 | + ], | |
| 3160 | + "languages": [ | |
| 3161 | + "en" | |
| 3162 | + ], | |
| 3163 | + "currency": [ | |
| 3164 | + "GBP" | |
| 3165 | + ], | |
| 3166 | + "supportsListings": false, | |
| 3167 | + "supportsSold": true, | |
| 3168 | + "supportsAuctions": true, | |
| 3169 | + "supportsImages": true, | |
| 3170 | + "supportsCatalog": false, | |
| 3171 | + "supportsPopulation": false, | |
| 3172 | + "supportsLookup": true, | |
| 3173 | + "refreshFrequencyMinutes": 1440, | |
| 3174 | + "priority": "high", | |
| 3175 | + "trustScore": 0.9, | |
| 3176 | + "attributionRequired": true, | |
| 3177 | + "termsUrl": "https://www.scotchwhiskyauctions.com/terms/", | |
| 3178 | + "accessNotes": "Public monthly auction archive (Glasgow; ~4,500–5,000 lots per auction, 20 lots per page, plain HTTPS with the RareIndex user agent, robots.txt disallows only /cmsplus/). Each lot card exposes title, lot number and 'Sold for £X in <month year>' — this is the hammer price; the buyer's commission (SWA charges a separate buyer's premium plus VAT) is NOT included, so buyer_premium_included=false. Sale date = the auction end date shown on the auction page. Unsold lots are skipped. 2 s politeness delay; pagesPerRun caps each incremental run (backfill mode walks older auctions).", | |
| 3179 | + "enabled": true, | |
| 3180 | + "schemaVersion": "1.0", | |
| 3181 | + "config": { | |
| 3182 | + "auctionsUrl": "https://www.scotchwhiskyauctions.com/auctions/", | |
| 3183 | + "auctionsPerRun": 2, | |
| 3184 | + "pagesPerRun": 12 | |
| 3185 | + } | |
| 3186 | + }, | |
| 1377 | 3187 | { |
| 1378 | 3188 | "id": "scryfall", |
| 1379 | 3189 | "displayName": "Scryfall (Magic: The Gathering)", |
@@ -1419,6 +3229,45 @@ | ||
| 1419 | 3229 | "requestIntervalMs": 100 |
| 1420 | 3230 | } |
| 1421 | 3231 | }, |
| 3232 | + { | |
| 3233 | + "id": "sorcery-tcg", | |
| 3234 | + "displayName": "Sorcery: Contested Realm (official card API)", | |
| 3235 | + "sourceId": "sorcery-tcg", | |
| 3236 | + "sourceName": "Sorcery TCG (Erik's Curiosa)", | |
| 3237 | + "sourceType": "manufacturer", | |
| 3238 | + "sourceUrl": "https://sorcerytcg.com", | |
| 3239 | + "module": "api/sorcery-tcg", | |
| 3240 | + "enginePriority": [ | |
| 3241 | + "api" | |
| 3242 | + ], | |
| 3243 | + "categories": [ | |
| 3244 | + "other_tcg" | |
| 3245 | + ], | |
| 3246 | + "regions": [ | |
| 3247 | + "US", | |
| 3248 | + "EU" | |
| 3249 | + ], | |
| 3250 | + "languages": [ | |
| 3251 | + "en" | |
| 3252 | + ], | |
| 3253 | + "currency": [], | |
| 3254 | + "supportsListings": false, | |
| 3255 | + "supportsSold": false, | |
| 3256 | + "supportsAuctions": false, | |
| 3257 | + "supportsImages": false, | |
| 3258 | + "supportsCatalog": true, | |
| 3259 | + "supportsPopulation": false, | |
| 3260 | + "supportsLookup": false, | |
| 3261 | + "refreshFrequencyMinutes": 10080, | |
| 3262 | + "priority": "low", | |
| 3263 | + "trustScore": 0.85, | |
| 3264 | + "attributionRequired": true, | |
| 3265 | + "termsUrl": "https://sorcerytcg.com", | |
| 3266 | + "accessNotes": "Official public JSON (https://api.sorcerytcg.com/api/cards, one call ≈1,100 cards with every printing: set code/name, finish Standard/Foil, product Booster/Precon/Promo, print date). Catalog only, no prices or images; TCGCSV supplies Sorcery prices via TCGplayer product names. One catalog_item per printing (finish → 'Foil' variant; non-booster product noted in metadata).", | |
| 3267 | + "enabled": true, | |
| 3268 | + "schemaVersion": "1.0", | |
| 3269 | + "config": {} | |
| 3270 | + }, | |
| 1422 | 3271 | { |
| 1423 | 3272 | "id": "sothebys", |
| 1424 | 3273 | "displayName": "Sotheby's (auction results & upcoming lots)", |
@@ -1476,69 +3325,324 @@ | ||
| 1476 | 3325 | "vintage_toys" |
| 1477 | 3326 | ], |
| 1478 | 3327 | "regions": [ |
| 1479 | − "US", | |
| 1480 | − "GB", | |
| 1481 | − "HK", | |
| 1482 | − "FR", | |
| 1483 | − "CH" | |
| 3328 | + "US", | |
| 3329 | + "GB", | |
| 3330 | + "HK", | |
| 3331 | + "FR", | |
| 3332 | + "CH" | |
| 3333 | + ], | |
| 3334 | + "languages": [ | |
| 3335 | + "en" | |
| 3336 | + ], | |
| 3337 | + "currency": [ | |
| 3338 | + "USD", | |
| 3339 | + "GBP", | |
| 3340 | + "HKD", | |
| 3341 | + "EUR", | |
| 3342 | + "CHF" | |
| 3343 | + ], | |
| 3344 | + "supportsListings": false, | |
| 3345 | + "supportsSold": true, | |
| 3346 | + "supportsAuctions": true, | |
| 3347 | + "supportsImages": true, | |
| 3348 | + "supportsCatalog": false, | |
| 3349 | + "supportsPopulation": false, | |
| 3350 | + "supportsLookup": true, | |
| 3351 | + "refreshFrequencyMinutes": 360, | |
| 3352 | + "priority": "high", | |
| 3353 | + "trustScore": 0.9, | |
| 3354 | + "attributionRequired": true, | |
| 3355 | + "termsUrl": "https://www.sothebys.com/en/terms-conditions", | |
| 3356 | + "accessNotes": "Auction pages https://www.sothebys.com/en/buy/auction/<year>/<slug> are server-rendered (__NEXT_DATA__ Apollo cache) with auction metadata, department names, currency, dates and the first 48 lot cards including estimates and, for closed sales, the visible result (BidState.sold.premiums.finalPriceV2 = price including buyer's premium, currentBidV2 = hammer). Remaining lots are paged with the same public GraphQL endpoint the page uses (clientapi.prod.sothelabs.com/graphql, query LotCardsFilterByPaginated, no authentication; we request only public lot-card fields). Auction discovery: links on the public /en/results and /en/calendar pages (rendered through Firecrawl, 1 credit each), plus config.seeds auction URLs; auctions seen while open are re-checked after they close so their results are captured. robots.txt disallows /bsp-api/* and PDFs — not used. Prices: finalPriceV2 (buyer's premium included) → buyerPremiumIncluded=true, hammer kept in metadata; sale date = lot closingTime or the auction's closed timestamp. Condition reports are behind login and are not fetched.", | |
| 3357 | + "enabled": true, | |
| 3358 | + "schemaVersion": "1.0", | |
| 3359 | + "config": { | |
| 3360 | + "seeds": [], | |
| 3361 | + "discoveryPages": [ | |
| 3362 | + "https://www.sothebys.com/en/results", | |
| 3363 | + "https://www.sothebys.com/en/calendar" | |
| 3364 | + ], | |
| 3365 | + "departments": [], | |
| 3366 | + "maxAuctionsPerRun": 25, | |
| 3367 | + "pageSize": 48 | |
| 3368 | + } | |
| 3369 | + }, | |
| 3370 | + { | |
| 3371 | + "id": "sportscardspro", | |
| 3372 | + "displayName": "SportsCardsPro", | |
| 3373 | + "sourceId": "sportscardspro", | |
| 3374 | + "sourceName": "SportsCardsPro (PriceCharting)", | |
| 3375 | + "sourceType": "pricing_guide", | |
| 3376 | + "sourceUrl": "https://www.sportscardspro.com", | |
| 3377 | + "module": "api/sportscardspro", | |
| 3378 | + "enginePriority": [ | |
| 3379 | + "firecrawl", | |
| 3380 | + "scrapfly" | |
| 3381 | + ], | |
| 3382 | + "categories": [ | |
| 3383 | + "sports_cards", | |
| 3384 | + "baseball_cards", | |
| 3385 | + "basketball_cards", | |
| 3386 | + "football_cards", | |
| 3387 | + "hockey_cards", | |
| 3388 | + "soccer_cards", | |
| 3389 | + "f1_cards", | |
| 3390 | + "other_sports_cards" | |
| 3391 | + ], | |
| 3392 | + "regions": [ | |
| 3393 | + "US" | |
| 3394 | + ], | |
| 3395 | + "languages": [ | |
| 3396 | + "en" | |
| 3397 | + ], | |
| 3398 | + "currency": [ | |
| 3399 | + "USD" | |
| 3400 | + ], | |
| 3401 | + "supportsListings": false, | |
| 3402 | + "supportsSold": true, | |
| 3403 | + "supportsAuctions": false, | |
| 3404 | + "supportsImages": true, | |
| 3405 | + "supportsCatalog": true, | |
| 3406 | + "supportsPopulation": false, | |
| 3407 | + "supportsLookup": true, | |
| 3408 | + "refreshFrequencyMinutes": 1440, | |
| 3409 | + "priority": "high", | |
| 3410 | + "trustScore": 0.7, | |
| 3411 | + "attributionRequired": true, | |
| 3412 | + "termsUrl": "https://www.sportscardspro.com/page/terms-of-service", | |
| 3413 | + "accessNotes": "Public product pages of PriceCharting's sports-card site. robots.txt only disallows /buy, /publish-offer, /stripe-connect. Plain HTTP with our user agent gets a Cloudflare interstitial (HTTP 403), so pages are fetched through Firecrawl (1 credit per page, ~1 MB each) — no login, no CAPTCHA solving, no account. Each page exposes guide values per grade (Ungraded, Grade 1–9.5, TAG/ACE/SGC/CGC/PSA/BGS 10, BGS 10 Black, CGC 10 Pristine) and recently completed eBay sales per grade tab; graded rows without a named grading company are stored with the generic grader 'graded' unless the eBay title names the company. Set lists come from the public /category/<sport>-cards pages; console listings use the ?format=json cursor endpoint.", | |
| 3414 | + "enabled": true, | |
| 3415 | + "schemaVersion": "2.0", | |
| 3416 | + "config": { | |
| 3417 | + "seeds": [ | |
| 3418 | + "basketball-cards-1986-fleer", | |
| 3419 | + "basketball-cards-2003-topps-chrome", | |
| 3420 | + "basketball-cards-2018-panini-prizm", | |
| 3421 | + "basketball-cards-2019-panini-prizm", | |
| 3422 | + "basketball-cards-1996-topps-chrome", | |
| 3423 | + "baseball-cards-1952-topps", | |
| 3424 | + "baseball-cards-1989-upper-deck", | |
| 3425 | + "baseball-cards-2011-topps-update", | |
| 3426 | + "baseball-cards-2018-topps-update", | |
| 3427 | + "baseball-cards-1993-sp", | |
| 3428 | + "football-cards-2000-playoff-contenders", | |
| 3429 | + "football-cards-2017-panini-prizm", | |
| 3430 | + "football-cards-2020-panini-prizm", | |
| 3431 | + "football-cards-1957-topps", | |
| 3432 | + "hockey-cards-1979-o-pee-chee", | |
| 3433 | + "hockey-cards-2005-upper-deck", | |
| 3434 | + "hockey-cards-2015-upper-deck", | |
| 3435 | + "soccer-cards-2018-panini-prizm-world-cup", | |
| 3436 | + "soccer-cards-2004-panini-mega-cracks" | |
| 3437 | + ], | |
| 3438 | + "cardCategories": [ | |
| 3439 | + "basketball-cards", | |
| 3440 | + "baseball-cards", | |
| 3441 | + "football-cards", | |
| 3442 | + "hockey-cards", | |
| 3443 | + "soccer-cards" | |
| 3444 | + ], | |
| 3445 | + "maxConsolesPerCategory": 25, | |
| 3446 | + "productsPerConsole": 120, | |
| 3447 | + "sort": "popularity" | |
| 3448 | + } | |
| 3449 | + }, | |
| 3450 | + { | |
| 3451 | + "id": "subdial", | |
| 3452 | + "displayName": "Subdial (UK pre-owned watch listings)", | |
| 3453 | + "sourceId": "subdial", | |
| 3454 | + "sourceName": "Subdial", | |
| 3455 | + "sourceType": "marketplace", | |
| 3456 | + "sourceUrl": "https://subdial.com", | |
| 3457 | + "module": "api/subdial", | |
| 3458 | + "enginePriority": [ | |
| 3459 | + "api", | |
| 3460 | + "firecrawl" | |
| 3461 | + ], | |
| 3462 | + "categories": [ | |
| 3463 | + "watches", | |
| 3464 | + "rolex", | |
| 3465 | + "patek_philippe", | |
| 3466 | + "audemars_piguet", | |
| 3467 | + "omega", | |
| 3468 | + "other_watches" | |
| 3469 | + ], | |
| 3470 | + "regions": [ | |
| 3471 | + "GB" | |
| 3472 | + ], | |
| 3473 | + "languages": [ | |
| 3474 | + "en" | |
| 3475 | + ], | |
| 3476 | + "currency": [ | |
| 3477 | + "GBP" | |
| 3478 | + ], | |
| 3479 | + "supportsListings": true, | |
| 3480 | + "supportsSold": false, | |
| 3481 | + "supportsAuctions": false, | |
| 3482 | + "supportsImages": true, | |
| 3483 | + "supportsCatalog": false, | |
| 3484 | + "supportsPopulation": false, | |
| 3485 | + "supportsLookup": true, | |
| 3486 | + "refreshFrequencyMinutes": 720, | |
| 3487 | + "priority": "medium", | |
| 3488 | + "trustScore": 0.8, | |
| 3489 | + "attributionRequired": true, | |
| 3490 | + "termsUrl": "https://subdial.com/terms", | |
| 3491 | + "accessNotes": "Listing URLs come from the public sitemap (subdial.com/sitemap-listing.xml); each listing page embeds a schema.org Product (brand, mpn = reference, sku = Subdial id, GBP price, availability) plus a specification table (reference, year, condition, box, papers). robots.txt allows / for generic agents (it blocks a list of named AI crawlers, which we are not). Plain HTTPS with the RareIndex user agent, 1.5 s between pages, capped per run; the crawl budget skips URLs already fetched recently.", | |
| 3492 | + "enabled": true, | |
| 3493 | + "schemaVersion": "1.0", | |
| 3494 | + "config": { | |
| 3495 | + "maxListingsPerRun": 250 | |
| 3496 | + } | |
| 3497 | + }, | |
| 3498 | + { | |
| 3499 | + "id": "surugaya", | |
| 3500 | + "displayName": "Suruga-ya (Japanese second-hand hobby retailer, JPY)", | |
| 3501 | + "sourceId": "surugaya", | |
| 3502 | + "sourceName": "Suruga-ya", | |
| 3503 | + "sourceType": "marketplace", | |
| 3504 | + "sourceUrl": "https://www.suruga-ya.jp", | |
| 3505 | + "module": "firecrawl/surugaya", | |
| 3506 | + "enginePriority": [ | |
| 3507 | + "firecrawl" | |
| 3508 | + ], | |
| 3509 | + "categories": [ | |
| 3510 | + "pokemon", | |
| 3511 | + "yugioh", | |
| 3512 | + "one_piece_card_game", | |
| 3513 | + "gundam", | |
| 3514 | + "action_figures", | |
| 3515 | + "nintendo_games", | |
| 3516 | + "sega_games", | |
| 3517 | + "playstation_games", | |
| 3518 | + "designer_toys" | |
| 3519 | + ], | |
| 3520 | + "regions": [ | |
| 3521 | + "JP" | |
| 3522 | + ], | |
| 3523 | + "languages": [ | |
| 3524 | + "ja" | |
| 3525 | + ], | |
| 3526 | + "currency": [ | |
| 3527 | + "JPY" | |
| 3528 | + ], | |
| 3529 | + "supportsListings": true, | |
| 3530 | + "supportsSold": false, | |
| 3531 | + "supportsAuctions": false, | |
| 3532 | + "supportsImages": true, | |
| 3533 | + "supportsCatalog": true, | |
| 3534 | + "supportsPopulation": false, | |
| 3535 | + "supportsLookup": false, | |
| 3536 | + "refreshFrequencyMinutes": 720, | |
| 3537 | + "priority": "medium", | |
| 3538 | + "trustScore": 0.75, | |
| 3539 | + "attributionRequired": true, | |
| 3540 | + "termsUrl": "https://www.suruga-ya.jp/man/index.html", | |
| 3541 | + "accessNotes": "Public search result pages (suruga-ya.jp/search?...; robots.txt: 'Allow: /' for generic agents with Content-Signal search=yes, ai-train=no, use=reference — we only index prices and never train on content). Plain HTTPS returns 403 from data-centre IPs, so pages are fetched through Firecrawl (1 credit per page, ~24 items). Parsed per card: product id (shinaban), title, condition/type label, release date, brand, list price (定価), Suruga-ya price or 品切れ (sold out), and the marketplace (マケプレ) lowest price when Suruga-ya itself is out of stock. JPY. Emits catalog items + fixed-price listings; no sold history is available publicly.", | |
| 3542 | + "enabled": true, | |
| 3543 | + "schemaVersion": "1.0", | |
| 3544 | + "config": { | |
| 3545 | + "seeds": [ | |
| 3546 | + { | |
| 3547 | + "query": "category=5&search_word=%E3%83%9D%E3%82%B1%E3%83%A2%E3%83%B3%E3%82%AB%E3%83%BC%E3%83%89%20PSA", | |
| 3548 | + "categorySlug": "pokemon", | |
| 3549 | + "label": "Pokémon cards PSA" | |
| 3550 | + }, | |
| 3551 | + { | |
| 3552 | + "query": "category=5&search_word=%E3%83%9D%E3%82%B1%E3%83%A2%E3%83%B3%E3%82%AB%E3%83%BC%E3%83%89%20SAR", | |
| 3553 | + "categorySlug": "pokemon", | |
| 3554 | + "label": "Pokémon SAR" | |
| 3555 | + }, | |
| 3556 | + { | |
| 3557 | + "query": "category=5&search_word=%E9%81%8A%E6%88%AF%E7%8E%8B%20%E3%83%97%E3%83%AA%E3%82%BA%E3%83%9E", | |
| 3558 | + "categorySlug": "yugioh", | |
| 3559 | + "label": "Yu-Gi-Oh! prismatic" | |
| 3560 | + }, | |
| 3561 | + { | |
| 3562 | + "query": "category=5&search_word=%E3%82%AC%E3%83%B3%E3%83%97%E3%83%A9%20MG", | |
| 3563 | + "categorySlug": "gundam", | |
| 3564 | + "label": "Gunpla MG" | |
| 3565 | + }, | |
| 3566 | + { | |
| 3567 | + "query": "category=5&search_word=figma", | |
| 3568 | + "categorySlug": "action_figures", | |
| 3569 | + "label": "figma" | |
| 3570 | + }, | |
| 3571 | + { | |
| 3572 | + "query": "category=5&search_word=%E3%82%B9%E3%83%BC%E3%83%91%E3%83%BC%E3%83%95%E3%82%A1%E3%83%9F%E3%82%B3%E3%83%B3%20%E3%82%BD%E3%83%95%E3%83%88", | |
| 3573 | + "categorySlug": "nintendo_games", | |
| 3574 | + "label": "Super Famicom software" | |
| 3575 | + } | |
| 3576 | + ], | |
| 3577 | + "pagesPerSeed": 1 | |
| 3578 | + } | |
| 3579 | + }, | |
| 3580 | + { | |
| 3581 | + "id": "swann", | |
| 3582 | + "displayName": "Swann Auction Galleries (results)", | |
| 3583 | + "sourceId": "swann", | |
| 3584 | + "sourceName": "Swann Auction Galleries", | |
| 3585 | + "sourceType": "auction_house", | |
| 3586 | + "sourceUrl": "https://www.swanngalleries.com", | |
| 3587 | + "module": "api/swann", | |
| 3588 | + "enginePriority": [ | |
| 3589 | + "api", | |
| 3590 | + "firecrawl" | |
| 3591 | + ], | |
| 3592 | + "categories": [ | |
| 3593 | + "autographs", | |
| 3594 | + "photography", | |
| 3595 | + "movie_posters", | |
| 3596 | + "maps", | |
| 3597 | + "books", | |
| 3598 | + "art", | |
| 3599 | + "contemporary_art", | |
| 3600 | + "historical_documents", | |
| 3601 | + "comics", | |
| 3602 | + "animation_art" | |
| 3603 | + ], | |
| 3604 | + "regions": [ | |
| 3605 | + "US" | |
| 1484 | 3606 | ], |
| 1485 | 3607 | "languages": [ |
| 1486 | 3608 | "en" |
| 1487 | 3609 | ], |
| 1488 | 3610 | "currency": [ |
| 1489 | − "USD", | |
| 1490 | − "GBP", | |
| 1491 | − "HKD", | |
| 1492 | − "EUR", | |
| 1493 | − "CHF" | |
| 3611 | + "USD" | |
| 1494 | 3612 | ], |
| 1495 | 3613 | "supportsListings": false, |
| 1496 | 3614 | "supportsSold": true, |
| 1497 | − "supportsAuctions": true, | |
| 3615 | + "supportsAuctions": false, | |
| 1498 | 3616 | "supportsImages": true, |
| 1499 | 3617 | "supportsCatalog": false, |
| 1500 | 3618 | "supportsPopulation": false, |
| 1501 | − "supportsLookup": true, | |
| 1502 | − "refreshFrequencyMinutes": 360, | |
| 1503 | − "priority": "high", | |
| 3619 | + "supportsLookup": false, | |
| 3620 | + "refreshFrequencyMinutes": 1440, | |
| 3621 | + "priority": "medium", | |
| 1504 | 3622 | "trustScore": 0.9, |
| 1505 | 3623 | "attributionRequired": true, |
| 1506 | − "termsUrl": "https://www.sothebys.com/en/terms-conditions", | |
| 1507 | − "accessNotes": "Auction pages https://www.sothebys.com/en/buy/auction/<year>/<slug> are server-rendered (__NEXT_DATA__ Apollo cache) with auction metadata, department names, currency, dates and the first 48 lot cards including estimates and, for closed sales, the visible result (BidState.sold.premiums.finalPriceV2 = price including buyer's premium, currentBidV2 = hammer). Remaining lots are paged with the same public GraphQL endpoint the page uses (clientapi.prod.sothelabs.com/graphql, query LotCardsFilterByPaginated, no authentication; we request only public lot-card fields). Auction discovery: links on the public /en/results and /en/calendar pages (rendered through Firecrawl, 1 credit each), plus config.seeds auction URLs; auctions seen while open are re-checked after they close so their results are captured. robots.txt disallows /bsp-api/* and PDFs — not used. Prices: finalPriceV2 (buyer's premium included) → buyerPremiumIncluded=true, hammer kept in metadata; sale date = lot closingTime or the auction's closed timestamp. Condition reports are behind login and are not fetched.", | |
| 3624 | + "termsUrl": "https://www.swanngalleries.com/conditions-of-sale/", | |
| 3625 | + "accessNotes": "Public pages over plain HTTPS with the RareIndex user agent (robots.txt: Disallow empty): the past-auctions list (/auctions/past-auctions/: date, sale number, department, catalog link) and archived catalogs (/auction-catalog/<slug>?algoliaParam=archive_lotNumber_asc_prod[page]=N, 20 lots per page) whose server-rendered cards show lot number, title, estimate and 'Sold: $X' with the note 'Sold price includes buyer's premium' → buyer_premium_included=true; 'Passed' lots are skipped. Category from the sale's department. 1.5 s politeness delay.", | |
| 1508 | 3626 | "enabled": true, |
| 1509 | 3627 | "schemaVersion": "1.0", |
| 1510 | 3628 | "config": { |
| 1511 | − "seeds": [], | |
| 1512 | − "discoveryPages": [ | |
| 1513 | − "https://www.sothebys.com/en/results", | |
| 1514 | − "https://www.sothebys.com/en/calendar" | |
| 1515 | − ], | |
| 1516 | − "departments": [], | |
| 1517 | − "maxAuctionsPerRun": 25, | |
| 1518 | − "pageSize": 48 | |
| 3629 | + "auctionsPerRun": 2, | |
| 3630 | + "pagesPerAuction": 15 | |
| 1519 | 3631 | } |
| 1520 | 3632 | }, |
| 1521 | 3633 | { |
| 1522 | − "id": "sportscardspro", | |
| 1523 | − "displayName": "SportsCardsPro", | |
| 1524 | − "sourceId": "sportscardspro", | |
| 1525 | − "sourceName": "SportsCardsPro (PriceCharting)", | |
| 1526 | − "sourceType": "pricing_guide", | |
| 1527 | − "sourceUrl": "https://www.sportscardspro.com", | |
| 1528 | − "module": "api/sportscardspro", | |
| 3634 | + "id": "swu-db", | |
| 3635 | + "displayName": "SWU-DB (Star Wars: Unlimited)", | |
| 3636 | + "sourceId": "swu-db", | |
| 3637 | + "sourceName": "SWU-DB", | |
| 3638 | + "sourceType": "catalog", | |
| 3639 | + "sourceUrl": "https://www.swu-db.com", | |
| 3640 | + "module": "api/swu-db", | |
| 1529 | 3641 | "enginePriority": [ |
| 1530 | − "firecrawl", | |
| 1531 | − "scrapfly" | |
| 3642 | + "api" | |
| 1532 | 3643 | ], |
| 1533 | 3644 | "categories": [ |
| 1534 | − "sports_cards", | |
| 1535 | − "baseball_cards", | |
| 1536 | − "basketball_cards", | |
| 1537 | − "football_cards", | |
| 1538 | − "hockey_cards", | |
| 1539 | − "soccer_cards", | |
| 1540 | − "f1_cards", | |
| 1541 | − "other_sports_cards" | |
| 3645 | + "star_wars_tcg" | |
| 1542 | 3646 | ], |
| 1543 | 3647 | "regions": [ |
| 1544 | 3648 | "US" |
@@ -1550,52 +3654,218 @@ | ||
| 1550 | 3654 | "USD" |
| 1551 | 3655 | ], |
| 1552 | 3656 | "supportsListings": false, |
| 1553 | − "supportsSold": true, | |
| 3657 | + "supportsSold": false, | |
| 1554 | 3658 | "supportsAuctions": false, |
| 1555 | 3659 | "supportsImages": true, |
| 1556 | 3660 | "supportsCatalog": true, |
| 1557 | 3661 | "supportsPopulation": false, |
| 1558 | 3662 | "supportsLookup": true, |
| 1559 | 3663 | "refreshFrequencyMinutes": 1440, |
| 1560 | − "priority": "high", | |
| 3664 | + "priority": "medium", | |
| 1561 | 3665 | "trustScore": 0.7, |
| 1562 | 3666 | "attributionRequired": true, |
| 1563 | − "termsUrl": "https://www.sportscardspro.com/page/terms-of-service", | |
| 1564 | − "accessNotes": "Public product pages of PriceCharting's sports-card site. robots.txt only disallows /buy, /publish-offer, /stripe-connect. Plain HTTP with our user agent gets a Cloudflare interstitial (HTTP 403), so pages are fetched through Firecrawl (1 credit per page, ~1 MB each) — no login, no CAPTCHA solving, no account. Each page exposes guide values per grade (Ungraded, Grade 1–9.5, TAG/ACE/SGC/CGC/PSA/BGS 10, BGS 10 Black, CGC 10 Pristine) and recently completed eBay sales per grade tab; graded rows without a named grading company are stored with the generic grader 'graded' unless the eBay title names the company. Set lists come from the public /category/<sport>-cards pages; console listings use the ?format=json cursor endpoint.", | |
| 3667 | + "termsUrl": "https://www.swu-db.com/api", | |
| 3668 | + "accessNotes": "Free community API (https://api.swu-db.com/cards/<set>, no key). Each card carries TCGplayer ids and USD MarketPrice/LowPrice (+ FoilPrice/LowFoilPrice) relayed from TCGplayer without a timestamp → observations dated by the fetch day, confidence 0.7. Set codes are configured (the /catalog/sets endpoint needs an API key); VariantType (Normal, Hyperspace, Showcase…) becomes the variant, foil prices create a 'Foil' variant.", | |
| 1565 | 3669 | "enabled": true, |
| 1566 | − "schemaVersion": "2.0", | |
| 3670 | + "schemaVersion": "1.0", | |
| 1567 | 3671 | "config": { |
| 1568 | − "seeds": [ | |
| 1569 | − "basketball-cards-1986-fleer", | |
| 1570 | − "basketball-cards-2003-topps-chrome", | |
| 1571 | − "basketball-cards-2018-panini-prizm", | |
| 1572 | − "basketball-cards-2019-panini-prizm", | |
| 1573 | − "basketball-cards-1996-topps-chrome", | |
| 1574 | − "baseball-cards-1952-topps", | |
| 1575 | − "baseball-cards-1989-upper-deck", | |
| 1576 | − "baseball-cards-2011-topps-update", | |
| 1577 | − "baseball-cards-2018-topps-update", | |
| 1578 | − "baseball-cards-1993-sp", | |
| 1579 | − "football-cards-2000-playoff-contenders", | |
| 1580 | − "football-cards-2017-panini-prizm", | |
| 1581 | − "football-cards-2020-panini-prizm", | |
| 1582 | − "football-cards-1957-topps", | |
| 1583 | − "hockey-cards-1979-o-pee-chee", | |
| 1584 | − "hockey-cards-2005-upper-deck", | |
| 1585 | − "hockey-cards-2015-upper-deck", | |
| 1586 | − "soccer-cards-2018-panini-prizm-world-cup", | |
| 1587 | − "soccer-cards-2004-panini-mega-cracks" | |
| 1588 | − ], | |
| 1589 | − "cardCategories": [ | |
| 1590 | − "basketball-cards", | |
| 1591 | − "baseball-cards", | |
| 1592 | − "football-cards", | |
| 1593 | − "hockey-cards", | |
| 1594 | − "soccer-cards" | |
| 1595 | − ], | |
| 1596 | − "maxConsolesPerCategory": 25, | |
| 1597 | − "productsPerConsole": 120, | |
| 1598 | − "sort": "popularity" | |
| 3672 | + "sets": { | |
| 3673 | + "SOR": "Spark of Rebellion", | |
| 3674 | + "SHD": "Shadows of the Galaxy", | |
| 3675 | + "TWI": "Twilight of the Republic", | |
| 3676 | + "JTL": "Jump to Lightspeed", | |
| 3677 | + "LOF": "Legends of the Force", | |
| 3678 | + "SEC": "Secrets of Power" | |
| 3679 | + } | |
| 3680 | + } | |
| 3681 | + }, | |
| 3682 | + { | |
| 3683 | + "id": "tcgcsv", | |
| 3684 | + "displayName": "TCGCSV (TCGplayer catalog & prices)", | |
| 3685 | + "sourceId": "tcgcsv", | |
| 3686 | + "sourceName": "TCGCSV", | |
| 3687 | + "sourceType": "pricing_guide", | |
| 3688 | + "sourceUrl": "https://tcgcsv.com", | |
| 3689 | + "module": "api/tcgcsv", | |
| 3690 | + "enginePriority": [ | |
| 3691 | + "api" | |
| 3692 | + ], | |
| 3693 | + "categories": [ | |
| 3694 | + "trading_cards", | |
| 3695 | + "pokemon", | |
| 3696 | + "magic_the_gathering", | |
| 3697 | + "yugioh", | |
| 3698 | + "disney_lorcana", | |
| 3699 | + "one_piece_card_game", | |
| 3700 | + "digimon_tcg", | |
| 3701 | + "flesh_and_blood", | |
| 3702 | + "star_wars_tcg", | |
| 3703 | + "dragon_ball_tcg", | |
| 3704 | + "weiss_schwarz", | |
| 3705 | + "final_fantasy_tcg", | |
| 3706 | + "other_tcg", | |
| 3707 | + "funko" | |
| 3708 | + ], | |
| 3709 | + "regions": [ | |
| 3710 | + "US" | |
| 3711 | + ], | |
| 3712 | + "languages": [ | |
| 3713 | + "en", | |
| 3714 | + "ja" | |
| 3715 | + ], | |
| 3716 | + "currency": [ | |
| 3717 | + "USD" | |
| 3718 | + ], | |
| 3719 | + "supportsListings": false, | |
| 3720 | + "supportsSold": false, | |
| 3721 | + "supportsAuctions": false, | |
| 3722 | + "supportsImages": true, | |
| 3723 | + "supportsCatalog": true, | |
| 3724 | + "supportsPopulation": false, | |
| 3725 | + "supportsLookup": false, | |
| 3726 | + "refreshFrequencyMinutes": 1440, | |
| 3727 | + "priority": "high", | |
| 3728 | + "trustScore": 0.8, | |
| 3729 | + "attributionRequired": true, | |
| 3730 | + "termsUrl": "https://tcgcsv.com/faq", | |
| 3731 | + "accessNotes": "Public daily mirror of TCGplayer's catalog and price API (JSON endpoints /tcgplayer/{categoryId}/groups, /{groupId}/products, /{groupId}/prices; robots.txt allows everything and the FAQ explicitly invites programmatic access with an identifying User-Agent and ~4 req/s). Prices (low/mid/high/market per printing subtype) are TCGplayer market data, not transactions: stored as price_observations dated with the mirror's last-updated timestamp (/last-updated.txt), confidence 0.75. Categories are mapped to taxonomy slugs in config; incremental runs cover the newest `maxGroupsPerCategory` sets per category, backfill covers all. Identifiers: tcgplayer_id (product) + tcgplayer_group_id. Historical price archives (2024-02-08 →) exist as daily 7z files and can be backfilled later.", | |
| 3732 | + "enabled": true, | |
| 3733 | + "schemaVersion": "1.0", | |
| 3734 | + "config": { | |
| 3735 | + "categories": { | |
| 3736 | + "1": { | |
| 3737 | + "slug": "magic_the_gathering", | |
| 3738 | + "franchise": "Magic: The Gathering", | |
| 3739 | + "brand": "Wizards of the Coast" | |
| 3740 | + }, | |
| 3741 | + "2": { | |
| 3742 | + "slug": "yugioh", | |
| 3743 | + "franchise": "Yu-Gi-Oh!", | |
| 3744 | + "brand": "Konami" | |
| 3745 | + }, | |
| 3746 | + "3": { | |
| 3747 | + "slug": "pokemon", | |
| 3748 | + "franchise": "Pokémon", | |
| 3749 | + "brand": "The Pokémon Company", | |
| 3750 | + "language": "English" | |
| 3751 | + }, | |
| 3752 | + "16": { | |
| 3753 | + "slug": "other_tcg", | |
| 3754 | + "franchise": "Cardfight!! Vanguard", | |
| 3755 | + "brand": "Bushiroad" | |
| 3756 | + }, | |
| 3757 | + "17": { | |
| 3758 | + "slug": "other_tcg", | |
| 3759 | + "franchise": "Force of Will", | |
| 3760 | + "brand": "Force of Will Co." | |
| 3761 | + }, | |
| 3762 | + "20": { | |
| 3763 | + "slug": "weiss_schwarz", | |
| 3764 | + "franchise": "Weiß Schwarz", | |
| 3765 | + "brand": "Bushiroad" | |
| 3766 | + }, | |
| 3767 | + "24": { | |
| 3768 | + "slug": "final_fantasy_tcg", | |
| 3769 | + "franchise": "Final Fantasy TCG", | |
| 3770 | + "brand": "Square Enix" | |
| 3771 | + }, | |
| 3772 | + "25": { | |
| 3773 | + "slug": "other_tcg", | |
| 3774 | + "franchise": "UniVersus", | |
| 3775 | + "brand": "UVS Games" | |
| 3776 | + }, | |
| 3777 | + "27": { | |
| 3778 | + "slug": "dragon_ball_tcg", | |
| 3779 | + "franchise": "Dragon Ball Super Card Game", | |
| 3780 | + "brand": "Bandai" | |
| 3781 | + }, | |
| 3782 | + "29": { | |
| 3783 | + "slug": "funko", | |
| 3784 | + "franchise": null, | |
| 3785 | + "brand": "Funko" | |
| 3786 | + }, | |
| 3787 | + "59": { | |
| 3788 | + "slug": "other_tcg", | |
| 3789 | + "franchise": "KeyForge", | |
| 3790 | + "brand": "Ghost Galaxy" | |
| 3791 | + }, | |
| 3792 | + "62": { | |
| 3793 | + "slug": "flesh_and_blood", | |
| 3794 | + "franchise": "Flesh and Blood", | |
| 3795 | + "brand": "Legend Story Studios" | |
| 3796 | + }, | |
| 3797 | + "63": { | |
| 3798 | + "slug": "digimon_tcg", | |
| 3799 | + "franchise": "Digimon", | |
| 3800 | + "brand": "Bandai" | |
| 3801 | + }, | |
| 3802 | + "66": { | |
| 3803 | + "slug": "other_tcg", | |
| 3804 | + "franchise": "MetaZoo", | |
| 3805 | + "brand": "MetaZoo Games" | |
| 3806 | + }, | |
| 3807 | + "68": { | |
| 3808 | + "slug": "one_piece_card_game", | |
| 3809 | + "franchise": "One Piece", | |
| 3810 | + "brand": "Bandai" | |
| 3811 | + }, | |
| 3812 | + "71": { | |
| 3813 | + "slug": "disney_lorcana", | |
| 3814 | + "franchise": "Disney Lorcana", | |
| 3815 | + "brand": "Ravensburger" | |
| 3816 | + }, | |
| 3817 | + "72": { | |
| 3818 | + "slug": "other_tcg", | |
| 3819 | + "franchise": "Battle Spirits Saga", | |
| 3820 | + "brand": "Bandai" | |
| 3821 | + }, | |
| 3822 | + "74": { | |
| 3823 | + "slug": "other_tcg", | |
| 3824 | + "franchise": "Grand Archive", | |
| 3825 | + "brand": "Weebs of the Shore" | |
| 3826 | + }, | |
| 3827 | + "77": { | |
| 3828 | + "slug": "other_tcg", | |
| 3829 | + "franchise": "Sorcery: Contested Realm", | |
| 3830 | + "brand": "Erik's Curiosa" | |
| 3831 | + }, | |
| 3832 | + "79": { | |
| 3833 | + "slug": "star_wars_tcg", | |
| 3834 | + "franchise": "Star Wars: Unlimited", | |
| 3835 | + "brand": "Fantasy Flight Games" | |
| 3836 | + }, | |
| 3837 | + "80": { | |
| 3838 | + "slug": "dragon_ball_tcg", | |
| 3839 | + "franchise": "Dragon Ball Super Fusion World", | |
| 3840 | + "brand": "Bandai" | |
| 3841 | + }, | |
| 3842 | + "81": { | |
| 3843 | + "slug": "other_tcg", | |
| 3844 | + "franchise": "Union Arena", | |
| 3845 | + "brand": "Bandai" | |
| 3846 | + }, | |
| 3847 | + "85": { | |
| 3848 | + "slug": "pokemon", | |
| 3849 | + "franchise": "Pokémon", | |
| 3850 | + "brand": "The Pokémon Company", | |
| 3851 | + "language": "Japanese" | |
| 3852 | + }, | |
| 3853 | + "86": { | |
| 3854 | + "slug": "other_tcg", | |
| 3855 | + "franchise": "Gundam Card Game", | |
| 3856 | + "brand": "Bandai" | |
| 3857 | + }, | |
| 3858 | + "89": { | |
| 3859 | + "slug": "other_tcg", | |
| 3860 | + "franchise": "Riftbound", | |
| 3861 | + "brand": "Riot Games" | |
| 3862 | + } | |
| 3863 | + }, | |
| 3864 | + "maxGroupsPerCategory": 40, | |
| 3865 | + "priceKinds": [ | |
| 3866 | + "market", | |
| 3867 | + "low" | |
| 3868 | + ] | |
| 1599 | 3869 | } |
| 1600 | 3870 | }, |
| 1601 | 3871 | { |
@@ -1652,6 +3922,143 @@ | ||
| 1652 | 3922 | ] |
| 1653 | 3923 | } |
| 1654 | 3924 | }, |
| 3925 | + { | |
| 3926 | + "id": "watchfinder", | |
| 3927 | + "displayName": "Watchfinder & Co. (UK pre-owned watch listings)", | |
| 3928 | + "sourceId": "watchfinder", | |
| 3929 | + "sourceName": "Watchfinder & Co.", | |
| 3930 | + "sourceType": "dealer", | |
| 3931 | + "sourceUrl": "https://www.watchfinder.co.uk", | |
| 3932 | + "module": "firecrawl/watchfinder", | |
| 3933 | + "enginePriority": [ | |
| 3934 | + "firecrawl", | |
| 3935 | + "scrapfly" | |
| 3936 | + ], | |
| 3937 | + "categories": [ | |
| 3938 | + "watches", | |
| 3939 | + "rolex", | |
| 3940 | + "patek_philippe", | |
| 3941 | + "audemars_piguet", | |
| 3942 | + "omega", | |
| 3943 | + "other_watches" | |
| 3944 | + ], | |
| 3945 | + "regions": [ | |
| 3946 | + "GB" | |
| 3947 | + ], | |
| 3948 | + "languages": [ | |
| 3949 | + "en" | |
| 3950 | + ], | |
| 3951 | + "currency": [ | |
| 3952 | + "GBP" | |
| 3953 | + ], | |
| 3954 | + "supportsListings": true, | |
| 3955 | + "supportsSold": false, | |
| 3956 | + "supportsAuctions": false, | |
| 3957 | + "supportsImages": true, | |
| 3958 | + "supportsCatalog": false, | |
| 3959 | + "supportsPopulation": false, | |
| 3960 | + "supportsLookup": false, | |
| 3961 | + "refreshFrequencyMinutes": 720, | |
| 3962 | + "priority": "medium", | |
| 3963 | + "trustScore": 0.85, | |
| 3964 | + "attributionRequired": true, | |
| 3965 | + "termsUrl": "https://www.watchfinder.co.uk/terms-and-conditions", | |
| 3966 | + "accessNotes": "Plain HTTPS returns 403 to non-browser agents (robots.txt is not served either); Firecrawl's standard fetch returns the public model pages (watchfinder.co.uk/watches/<brand>/<model>?p=N). Each product card carries data attributes (sku, brand, series, model reference, image), box/papers icons, year and the GBP price (data-price-amount). 1 Firecrawl credit per page (~24 watches); 2 s between pages. Listings only.", | |
| 3967 | + "enabled": true, | |
| 3968 | + "schemaVersion": "1.0", | |
| 3969 | + "config": { | |
| 3970 | + "seeds": [ | |
| 3971 | + "rolex/daytona", | |
| 3972 | + "rolex/submariner", | |
| 3973 | + "rolex/gmt-master-ii", | |
| 3974 | + "rolex/datejust", | |
| 3975 | + "rolex/day-date", | |
| 3976 | + "rolex/explorer", | |
| 3977 | + "patek-philippe/nautilus", | |
| 3978 | + "patek-philippe/aquanaut", | |
| 3979 | + "audemars-piguet/royal-oak", | |
| 3980 | + "audemars-piguet/royal-oak-offshore", | |
| 3981 | + "omega/speedmaster", | |
| 3982 | + "omega/seamaster", | |
| 3983 | + "cartier/santos", | |
| 3984 | + "tudor/black-bay", | |
| 3985 | + "iwc/portugieser", | |
| 3986 | + "jaeger-lecoultre/reverso" | |
| 3987 | + ], | |
| 3988 | + "pagesPerSeed": 2 | |
| 3989 | + } | |
| 3990 | + }, | |
| 3991 | + { | |
| 3992 | + "id": "winebid", | |
| 3993 | + "displayName": "WineBid (recent sales per wine)", | |
| 3994 | + "sourceId": "winebid", | |
| 3995 | + "sourceName": "WineBid", | |
| 3996 | + "sourceType": "marketplace", | |
| 3997 | + "sourceUrl": "https://www.winebid.com", | |
| 3998 | + "module": "api/winebid", | |
| 3999 | + "enginePriority": [ | |
| 4000 | + "api", | |
| 4001 | + "firecrawl", | |
| 4002 | + "scrapfly" | |
| 4003 | + ], | |
| 4004 | + "categories": [ | |
| 4005 | + "wine", | |
| 4006 | + "whisky" | |
| 4007 | + ], | |
| 4008 | + "regions": [ | |
| 4009 | + "US" | |
| 4010 | + ], | |
| 4011 | + "languages": [ | |
| 4012 | + "en" | |
| 4013 | + ], | |
| 4014 | + "currency": [ | |
| 4015 | + "USD" | |
| 4016 | + ], | |
| 4017 | + "supportsListings": true, | |
| 4018 | + "supportsSold": true, | |
| 4019 | + "supportsAuctions": true, | |
| 4020 | + "supportsImages": true, | |
| 4021 | + "supportsCatalog": false, | |
| 4022 | + "supportsPopulation": false, | |
| 4023 | + "supportsLookup": true, | |
| 4024 | + "refreshFrequencyMinutes": 1440, | |
| 4025 | + "priority": "medium", | |
| 4026 | + "trustScore": 0.85, | |
| 4027 | + "attributionRequired": true, | |
| 4028 | + "termsUrl": "https://www.winebid.com/Help/TermsAndConditions", | |
| 4029 | + "accessNotes": "US online wine auction (weekly auctions). Category browse pages (/BuyWine/Items/<region>/<id>) are rendered through Firecrawl (1 credit, plain HTTP returns 403 for list pages) to discover item URLs; each public item page (/BuyWine/Item/<id>/<slug>, plain HTTPS) shows the wine, the current auction lot (estimate / current bid) and a 'Recent sales' table of past winning bids for the same wine: item id, quantity sold, per-bottle amount (USD) and sale date. robots.txt lists rules for named search engines only (no generic group); we still avoid /Sales, /AuctionClosed and account paths. Winning bids exclude WineBid's 17% buyer's premium (buyer_premium_included=false; premium % in metadata). Sale dates come from the table (PDT). 2 s politeness delay; itemsPerRun caps each run.", | |
| 4030 | + "enabled": true, | |
| 4031 | + "schemaVersion": "1.0", | |
| 4032 | + "config": { | |
| 4033 | + "categories": [ | |
| 4034 | + { | |
| 4035 | + "path": "/BuyWine/Items/Bordeaux/18403", | |
| 4036 | + "label": "Bordeaux" | |
| 4037 | + }, | |
| 4038 | + { | |
| 4039 | + "path": "/BuyWine/Items/Burgundy/15363", | |
| 4040 | + "label": "Burgundy" | |
| 4041 | + }, | |
| 4042 | + { | |
| 4043 | + "path": "/BuyWine/Items/Napa-Valley/2910", | |
| 4044 | + "label": "Napa Valley" | |
| 4045 | + }, | |
| 4046 | + { | |
| 4047 | + "path": "/BuyWine/Items/Barolo/16367", | |
| 4048 | + "label": "Barolo" | |
| 4049 | + }, | |
| 4050 | + { | |
| 4051 | + "path": "/BuyWine/Items/Brunello-di-Montalcino/7725165", | |
| 4052 | + "label": "Brunello di Montalcino" | |
| 4053 | + }, | |
| 4054 | + { | |
| 4055 | + "path": "/BuyWine/Items/Big-Bottles/7902856", | |
| 4056 | + "label": "Large formats" | |
| 4057 | + } | |
| 4058 | + ], | |
| 4059 | + "itemsPerRun": 24 | |
| 4060 | + } | |
| 4061 | + }, | |
| 1655 | 4062 | { |
| 1656 | 4063 | "id": "ygoprodeck", |
| 1657 | 4064 | "displayName": "YGOPRODeck (Yu-Gi-Oh!)", |
added
data/fixtures/digimoncard/bt5-first.json
+46 −0
@@ -0,0 +1,46 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://digimoncard.io/card/a-blazing-storm-of-metal-bt5-103", | |
| 4 | + "externalId": "BT5-103", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "card": { | |
| 10 | + "name": "A Blazing Storm of Metal!", | |
| 11 | + "type": "Option", | |
| 12 | + "id": "BT5-103", | |
| 13 | + "color": "Black", | |
| 14 | + "color2": null, | |
| 15 | + "rarity": "U", | |
| 16 | + "stage": null, | |
| 17 | + "attribute": null, | |
| 18 | + "level": null, | |
| 19 | + "dp": null, | |
| 20 | + "artist": null, | |
| 21 | + "series": "Digimon Card Game", | |
| 22 | + "pretty_url": "a-blazing-storm-of-metal-bt5-103", | |
| 23 | + "date_added": "2025-11-25 15:16:35", | |
| 24 | + "tcgplayer_name": "A Blazing Storm of Metal!", | |
| 25 | + "tcgplayer_id": 245181, | |
| 26 | + "set_name": [ | |
| 27 | + "BT-05: Booster Battle Of Omni" | |
| 28 | + ] | |
| 29 | + }, | |
| 30 | + "alt": 1 | |
| 31 | + }, | |
| 32 | + "fetchedAt": "2026-09-07T06:26:44.856Z" | |
| 33 | + }, | |
| 34 | + "expect": { | |
| 35 | + "minCount": 1, | |
| 36 | + "kinds": [ | |
| 37 | + "catalog_item" | |
| 38 | + ], | |
| 39 | + "requiredFields": [ | |
| 40 | + "attributes.identifiers.digimoncard_id", | |
| 41 | + "attributes.number" | |
| 42 | + ] | |
| 43 | + }, | |
| 44 | + "note": "Live capture 2026-09-07 from https://digimoncard.io/card/a-blazing-storm-of-metal-bt5-103", | |
| 45 | + "capturedAt": "2026-09-07T06:26:44.857Z" | |
| 46 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/digimoncard/bt5-sixth.json
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://digimoncard.io/card/agumon-bt5-007", | |
| 4 | + "externalId": "BT5-007", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "card": { | |
| 10 | + "name": "Agumon", | |
| 11 | + "type": "Digimon", | |
| 12 | + "id": "BT5-007", | |
| 13 | + "color": "Red", | |
| 14 | + "color2": null, | |
| 15 | + "rarity": "C", | |
| 16 | + "stage": "Rookie", | |
| 17 | + "attribute": "Vaccine", | |
| 18 | + "level": 3, | |
| 19 | + "dp": 2000, | |
| 20 | + "artist": null, | |
| 21 | + "series": "Digimon Card Game", | |
| 22 | + "pretty_url": "agumon-bt5-007", | |
| 23 | + "date_added": "2025-11-25 15:16:37", | |
| 24 | + "tcgplayer_name": "Agumon", | |
| 25 | + "tcgplayer_id": 243533, | |
| 26 | + "set_name": [ | |
| 27 | + "BT-05: Booster Battle Of Omni", | |
| 28 | + "PB-13: Digimon Card Game Royal Knights Binder Set", | |
| 29 | + "Tamer Battle Pack 7", | |
| 30 | + "Tamer Party Vol.4" | |
| 31 | + ] | |
| 32 | + }, | |
| 33 | + "alt": 1 | |
| 34 | + }, | |
| 35 | + "fetchedAt": "2026-09-07T06:26:44.856Z" | |
| 36 | + }, | |
| 37 | + "expect": { | |
| 38 | + "minCount": 1, | |
| 39 | + "kinds": [ | |
| 40 | + "catalog_item" | |
| 41 | + ], | |
| 42 | + "requiredFields": [ | |
| 43 | + "attributes.identifiers.digimoncard_id", | |
| 44 | + "attributes.number" | |
| 45 | + ] | |
| 46 | + }, | |
| 47 | + "note": "Live capture 2026-09-07 from https://digimoncard.io/card/agumon-bt5-007", | |
| 48 | + "capturedAt": "2026-09-07T06:26:44.859Z" | |
| 49 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/grand-archive/page1-first.json
+64 −0
@@ -0,0 +1,64 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://index.gatcg.com/card/apotheosis-rite", | |
| 4 | + "externalId": "df594Qoszn", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "card": { | |
| 10 | + "uuid": "df594Qoszn", | |
| 11 | + "name": "Apotheosis Rite", | |
| 12 | + "slug": "apotheosis-rite", | |
| 13 | + "element": "NORM", | |
| 14 | + "types": [ | |
| 15 | + "REGALIA", | |
| 16 | + "ITEM" | |
| 17 | + ], | |
| 18 | + "classes": [ | |
| 19 | + "WARRIOR" | |
| 20 | + ], | |
| 21 | + "editions": [ | |
| 22 | + { | |
| 23 | + "uuid": "2zw7a98f7b", | |
| 24 | + "slug": "apotheosis-rite-p24-cpr", | |
| 25 | + "collector_number": "000", | |
| 26 | + "rarity": 9, | |
| 27 | + "illustrator": "十尾", | |
| 28 | + "image": "/cards/images/2zw7a98f7b.jpg", | |
| 29 | + "configuration": "default", | |
| 30 | + "set": { | |
| 31 | + "id": "muw6lmtzwg", | |
| 32 | + "name": "Promotional 2024", | |
| 33 | + "prefix": "P24", | |
| 34 | + "release_date": "2024-01-24T00:00:00", | |
| 35 | + "language": "EN" | |
| 36 | + }, | |
| 37 | + "circulations": [ | |
| 38 | + { | |
| 39 | + "uuid": "c29f57afoP", | |
| 40 | + "kind": "FOIL", | |
| 41 | + "foil": true, | |
| 42 | + "population": 1, | |
| 43 | + "population_operator": "=", | |
| 44 | + "printing": false | |
| 45 | + } | |
| 46 | + ] | |
| 47 | + } | |
| 48 | + ] | |
| 49 | + } | |
| 50 | + }, | |
| 51 | + "fetchedAt": "2026-09-07T06:26:55.815Z" | |
| 52 | + }, | |
| 53 | + "expect": { | |
| 54 | + "minCount": 1, | |
| 55 | + "kinds": [ | |
| 56 | + "catalog_item" | |
| 57 | + ], | |
| 58 | + "requiredFields": [ | |
| 59 | + "attributes.identifiers.gatcg_edition_id" | |
| 60 | + ] | |
| 61 | + }, | |
| 62 | + "note": "Live capture 2026-09-07 from https://index.gatcg.com/card/apotheosis-rite", | |
| 63 | + "capturedAt": "2026-09-07T06:26:55.818Z" | |
| 64 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/grand-archive/page1-third.json
+64 −0
@@ -0,0 +1,64 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://index.gatcg.com/card/transcendental-rite", | |
| 4 | + "externalId": "tAiiMGZJXp", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "card": { | |
| 10 | + "uuid": "tAiiMGZJXp", | |
| 11 | + "name": "Transcendental Rite", | |
| 12 | + "slug": "transcendental-rite", | |
| 13 | + "element": "NORM", | |
| 14 | + "types": [ | |
| 15 | + "REGALIA", | |
| 16 | + "ITEM" | |
| 17 | + ], | |
| 18 | + "classes": [ | |
| 19 | + "MAGE" | |
| 20 | + ], | |
| 21 | + "editions": [ | |
| 22 | + { | |
| 23 | + "uuid": "tlTsnFNlat", | |
| 24 | + "slug": "transcendental-rite-p26-cpr", | |
| 25 | + "collector_number": "000", | |
| 26 | + "rarity": 9, | |
| 27 | + "illustrator": "十尾", | |
| 28 | + "image": "/cards/images/tlTsnFNlat.jpg", | |
| 29 | + "configuration": "default", | |
| 30 | + "set": { | |
| 31 | + "id": "8a5a6c2b25", | |
| 32 | + "name": "Promotional 2026", | |
| 33 | + "prefix": "P26", | |
| 34 | + "release_date": "1970-01-01T00:00:00", | |
| 35 | + "language": "EN" | |
| 36 | + }, | |
| 37 | + "circulations": [ | |
| 38 | + { | |
| 39 | + "uuid": "lFW23Ww57k", | |
| 40 | + "kind": "FOIL", | |
| 41 | + "foil": true, | |
| 42 | + "population": 1, | |
| 43 | + "population_operator": "=", | |
| 44 | + "printing": false | |
| 45 | + } | |
| 46 | + ] | |
| 47 | + } | |
| 48 | + ] | |
| 49 | + } | |
| 50 | + }, | |
| 51 | + "fetchedAt": "2026-09-07T06:26:55.815Z" | |
| 52 | + }, | |
| 53 | + "expect": { | |
| 54 | + "minCount": 1, | |
| 55 | + "kinds": [ | |
| 56 | + "catalog_item" | |
| 57 | + ], | |
| 58 | + "requiredFields": [ | |
| 59 | + "attributes.identifiers.gatcg_edition_id" | |
| 60 | + ] | |
| 61 | + }, | |
| 62 | + "note": "Live capture 2026-09-07 from https://index.gatcg.com/card/transcendental-rite", | |
| 63 | + "capturedAt": "2026-09-07T06:26:55.819Z" | |
| 64 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/mtggoldfish/lea-air-elemental.json
+42 −0
@@ -0,0 +1,42 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://www.mtggoldfish.com/price/limited-edition-alpha/46/air-elemental", | |
| 4 | + "externalId": "019ce2ef-3ce1-7f1e-9e00-484543650f1a", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "kind": "card", | |
| 10 | + "setName": "Limited Edition Alpha Set Information", | |
| 11 | + "setSlug": "Limited+Edition+Alpha", | |
| 12 | + "card": { | |
| 13 | + "name": "Air Elemental [LEA]", | |
| 14 | + "set": "LEA", | |
| 15 | + "display_name": "Air Elemental", | |
| 16 | + "rarity": "Uncommon", | |
| 17 | + "foil": false, | |
| 18 | + "card_num": 46, | |
| 19 | + "finish": "regular", | |
| 20 | + "card_uuid": "019ce2ef-3ce1-7f1e-9e00-484543650f1a", | |
| 21 | + "paper": 156, | |
| 22 | + "online": null, | |
| 23 | + "link": "/price/limited-edition-alpha/46/air-elemental", | |
| 24 | + "image": "https://cards.mtggoldfish.com/images/019ce2ef-3ce1-7f1e-9e00-484543650f1a/variants/672/938/card_image.webp" | |
| 25 | + } | |
| 26 | + }, | |
| 27 | + "fetchedAt": "2026-09-07T06:27:11.229Z" | |
| 28 | + }, | |
| 29 | + "expect": { | |
| 30 | + "minCount": 1, | |
| 31 | + "kinds": [ | |
| 32 | + "catalog_item", | |
| 33 | + "price_observation" | |
| 34 | + ], | |
| 35 | + "requiredFields": [ | |
| 36 | + "attributes.setCode", | |
| 37 | + "attributes.number" | |
| 38 | + ] | |
| 39 | + }, | |
| 40 | + "note": "Live capture 2026-09-07 from https://www.mtggoldfish.com/price/limited-edition-alpha/46/air-elemental", | |
| 41 | + "capturedAt": "2026-09-07T06:27:11.234Z" | |
| 42 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/mtggoldfish/lea-black-lotus.json
+42 −0
@@ -0,0 +1,42 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://www.mtggoldfish.com/price/limited-edition-alpha/232/black-lotus", | |
| 4 | + "externalId": "019ce2ef-3ce1-7edb-b3c2-64761ad06557", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "kind": "card", | |
| 10 | + "setName": "Limited Edition Alpha Set Information", | |
| 11 | + "setSlug": "Limited+Edition+Alpha", | |
| 12 | + "card": { | |
| 13 | + "name": "Black Lotus [LEA]", | |
| 14 | + "set": "LEA", | |
| 15 | + "display_name": "Black Lotus", | |
| 16 | + "rarity": "Rare", | |
| 17 | + "foil": false, | |
| 18 | + "card_num": 232, | |
| 19 | + "finish": "regular", | |
| 20 | + "card_uuid": "019ce2ef-3ce1-7edb-b3c2-64761ad06557", | |
| 21 | + "paper": null, | |
| 22 | + "online": 491.06, | |
| 23 | + "link": "/price/limited-edition-alpha/232/black-lotus", | |
| 24 | + "image": "https://cards.mtggoldfish.com/images/019ce2ef-3ce1-7edb-b3c2-64761ad06557/variants/672/938/card_image.webp" | |
| 25 | + } | |
| 26 | + }, | |
| 27 | + "fetchedAt": "2026-09-07T06:27:11.229Z" | |
| 28 | + }, | |
| 29 | + "expect": { | |
| 30 | + "minCount": 1, | |
| 31 | + "kinds": [ | |
| 32 | + "catalog_item", | |
| 33 | + "price_observation" | |
| 34 | + ], | |
| 35 | + "requiredFields": [ | |
| 36 | + "attributes.setCode", | |
| 37 | + "attributes.number" | |
| 38 | + ] | |
| 39 | + }, | |
| 40 | + "note": "Live capture 2026-09-07 from https://www.mtggoldfish.com/price/limited-edition-alpha/232/black-lotus", | |
| 41 | + "capturedAt": "2026-09-07T06:31:35.665Z" | |
| 42 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/pokemonprice/base-set-1st-edition-first.json
+1360 −0
@@ -0,0 +1,1360 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://www.pokemonprice.com/base-set-1st-edition/alakazam-1-holo-1st-edition", | |
| 4 | + "externalId": "base-set-1st-edition/alakazam-1-holo-1st-edition", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "slug": "alakazam-1-holo-1st-edition", | |
| 10 | + "setSlug": "base-set-1st-edition", | |
| 11 | + "name": "Alakazam", | |
| 12 | + "tag": "Holo 1st edition", | |
| 13 | + "number": "1", | |
| 14 | + "total": "130", | |
| 15 | + "setName": "Base Set 1st Edition", | |
| 16 | + "image": "https://cdn.pokemonprice.com/cards/base4-1-alakazam.webp", | |
| 17 | + "rows": [ | |
| 18 | + { | |
| 19 | + "grade": "PSA1", | |
| 20 | + "fair_price": "30.00", | |
| 21 | + "low_price": "30.00", | |
| 22 | + "high_price": "30.00", | |
| 23 | + "confidence": "52.72", | |
| 24 | + "last_sale_date": "2020-05-30" | |
| 25 | + }, | |
| 26 | + { | |
| 27 | + "grade": "PSA10", | |
| 28 | + "fair_price": "12999.99", | |
| 29 | + "low_price": "12999.99", | |
| 30 | + "high_price": "12999.99", | |
| 31 | + "confidence": "52.72", | |
| 32 | + "last_sale_date": "2024-11-03" | |
| 33 | + }, | |
| 34 | + { | |
| 35 | + "grade": "PSA2", | |
| 36 | + "fair_price": "36.00", | |
| 37 | + "low_price": "36.00", | |
| 38 | + "high_price": "36.00", | |
| 39 | + "confidence": "52.72", | |
| 40 | + "last_sale_date": "2023-07-03" | |
| 41 | + }, | |
| 42 | + { | |
| 43 | + "grade": "PSA3", | |
| 44 | + "fair_price": "30.00", | |
| 45 | + "low_price": "30.00", | |
| 46 | + "high_price": "30.00", | |
| 47 | + "confidence": "52.72", | |
| 48 | + "last_sale_date": "2023-07-10" | |
| 49 | + }, | |
| 50 | + { | |
| 51 | + "grade": "PSA4", | |
| 52 | + "fair_price": "28.00", | |
| 53 | + "low_price": "28.00", | |
| 54 | + "high_price": "28.00", | |
| 55 | + "confidence": "52.72", | |
| 56 | + "last_sale_date": "2023-08-31" | |
| 57 | + }, | |
| 58 | + { | |
| 59 | + "grade": "PSA5", | |
| 60 | + "fair_price": "51.00", | |
| 61 | + "low_price": "51.00", | |
| 62 | + "high_price": "51.00", | |
| 63 | + "confidence": "52.72", | |
| 64 | + "last_sale_date": "2023-09-25" | |
| 65 | + }, | |
| 66 | + { | |
| 67 | + "grade": "PSA6", | |
| 68 | + "fair_price": "71.00", | |
| 69 | + "low_price": "71.00", | |
| 70 | + "high_price": "71.00", | |
| 71 | + "confidence": "52.72", | |
| 72 | + "last_sale_date": "2023-10-01" | |
| 73 | + }, | |
| 74 | + { | |
| 75 | + "grade": "PSA7", | |
| 76 | + "fair_price": "94.54", | |
| 77 | + "low_price": "94.54", | |
| 78 | + "high_price": "94.54", | |
| 79 | + "confidence": "52.72", | |
| 80 | + "last_sale_date": "2024-05-24" | |
| 81 | + }, | |
| 82 | + { | |
| 83 | + "grade": "PSA8", | |
| 84 | + "fair_price": "242.50", | |
| 85 | + "low_price": "242.50", | |
| 86 | + "high_price": "242.50", | |
| 87 | + "confidence": "52.72", | |
| 88 | + "last_sale_date": "2024-06-10" | |
| 89 | + }, | |
| 90 | + { | |
| 91 | + "grade": "PSA9", | |
| 92 | + "fair_price": "2059.80", | |
| 93 | + "low_price": "2059.80", | |
| 94 | + "high_price": "2059.80", | |
| 95 | + "confidence": "52.72", | |
| 96 | + "last_sale_date": "2024-09-14" | |
| 97 | + }, | |
| 98 | + { | |
| 99 | + "grade": "Raw", | |
| 100 | + "fair_price": "448.75", | |
| 101 | + "low_price": "397.47", | |
| 102 | + "high_price": "500.03", | |
| 103 | + "confidence": "47.78", | |
| 104 | + "last_sale_date": "2024-07-18" | |
| 105 | + } | |
| 106 | + ], | |
| 107 | + "transactions": [ | |
| 108 | + { | |
| 109 | + "month": "Sep, 2016", | |
| 110 | + "grade": "PSA9", | |
| 111 | + "count": 1 | |
| 112 | + }, | |
| 113 | + { | |
| 114 | + "month": "Nov, 2016", | |
| 115 | + "grade": "PSA9", | |
| 116 | + "count": 1 | |
| 117 | + }, | |
| 118 | + { | |
| 119 | + "month": "Dec, 2016", | |
| 120 | + "grade": "PSA6", | |
| 121 | + "count": 1 | |
| 122 | + }, | |
| 123 | + { | |
| 124 | + "month": "Jan, 2017", | |
| 125 | + "grade": "PSA8", | |
| 126 | + "count": 1 | |
| 127 | + }, | |
| 128 | + { | |
| 129 | + "month": "Jan, 2017", | |
| 130 | + "grade": "PSA7", | |
| 131 | + "count": 1 | |
| 132 | + }, | |
| 133 | + { | |
| 134 | + "month": "May, 2017", | |
| 135 | + "grade": "PSA8", | |
| 136 | + "count": 2 | |
| 137 | + }, | |
| 138 | + { | |
| 139 | + "month": "Jun, 2017", | |
| 140 | + "grade": "PSA8", | |
| 141 | + "count": 1 | |
| 142 | + }, | |
| 143 | + { | |
| 144 | + "month": "Jul, 2017", | |
| 145 | + "grade": "PSA7", | |
| 146 | + "count": 4 | |
| 147 | + }, | |
| 148 | + { | |
| 149 | + "month": "Jul, 2017", | |
| 150 | + "grade": "PSA8", | |
| 151 | + "count": 2 | |
| 152 | + }, | |
| 153 | + { | |
| 154 | + "month": "Jul, 2017", | |
| 155 | + "grade": "PSA9", | |
| 156 | + "count": 1 | |
| 157 | + }, | |
| 158 | + { | |
| 159 | + "month": "Aug, 2017", | |
| 160 | + "grade": "PSA8", | |
| 161 | + "count": 1 | |
| 162 | + }, | |
| 163 | + { | |
| 164 | + "month": "Aug, 2017", | |
| 165 | + "grade": "PSA9", | |
| 166 | + "count": 2 | |
| 167 | + }, | |
| 168 | + { | |
| 169 | + "month": "Aug, 2017", | |
| 170 | + "grade": "PSA6", | |
| 171 | + "count": 1 | |
| 172 | + }, | |
| 173 | + { | |
| 174 | + "month": "Sep, 2017", | |
| 175 | + "grade": "PSA6", | |
| 176 | + "count": 1 | |
| 177 | + }, | |
| 178 | + { | |
| 179 | + "month": "Sep, 2017", | |
| 180 | + "grade": "PSA9", | |
| 181 | + "count": 1 | |
| 182 | + }, | |
| 183 | + { | |
| 184 | + "month": "Oct, 2017", | |
| 185 | + "grade": "PSA7", | |
| 186 | + "count": 1 | |
| 187 | + }, | |
| 188 | + { | |
| 189 | + "month": "Oct, 2017", | |
| 190 | + "grade": "PSA8", | |
| 191 | + "count": 2 | |
| 192 | + }, | |
| 193 | + { | |
| 194 | + "month": "Nov, 2017", | |
| 195 | + "grade": "PSA7", | |
| 196 | + "count": 1 | |
| 197 | + }, | |
| 198 | + { | |
| 199 | + "month": "Nov, 2017", | |
| 200 | + "grade": "PSA8", | |
| 201 | + "count": 2 | |
| 202 | + }, | |
| 203 | + { | |
| 204 | + "month": "Dec, 2017", | |
| 205 | + "grade": "PSA7", | |
| 206 | + "count": 1 | |
| 207 | + }, | |
| 208 | + { | |
| 209 | + "month": "Jan, 2018", | |
| 210 | + "grade": "PSA8", | |
| 211 | + "count": 4 | |
| 212 | + }, | |
| 213 | + { | |
| 214 | + "month": "Jan, 2018", | |
| 215 | + "grade": "PSA9", | |
| 216 | + "count": 1 | |
| 217 | + }, | |
| 218 | + { | |
| 219 | + "month": "Feb, 2018", | |
| 220 | + "grade": "PSA8", | |
| 221 | + "count": 2 | |
| 222 | + }, | |
| 223 | + { | |
| 224 | + "month": "Feb, 2018", | |
| 225 | + "grade": "PSA6", | |
| 226 | + "count": 2 | |
| 227 | + }, | |
| 228 | + { | |
| 229 | + "month": "Feb, 2018", | |
| 230 | + "grade": "PSA9", | |
| 231 | + "count": 2 | |
| 232 | + }, | |
| 233 | + { | |
| 234 | + "month": "Mar, 2018", | |
| 235 | + "grade": "PSA8", | |
| 236 | + "count": 6 | |
| 237 | + }, | |
| 238 | + { | |
| 239 | + "month": "Mar, 2018", | |
| 240 | + "grade": "PSA6", | |
| 241 | + "count": 1 | |
| 242 | + }, | |
| 243 | + { | |
| 244 | + "month": "Mar, 2018", | |
| 245 | + "grade": "PSA9", | |
| 246 | + "count": 2 | |
| 247 | + }, | |
| 248 | + { | |
| 249 | + "month": "Mar, 2018", | |
| 250 | + "grade": "PSA5", | |
| 251 | + "count": 1 | |
| 252 | + }, | |
| 253 | + { | |
| 254 | + "month": "Apr, 2018", | |
| 255 | + "grade": "PSA7", | |
| 256 | + "count": 5 | |
| 257 | + }, | |
| 258 | + { | |
| 259 | + "month": "Apr, 2018", | |
| 260 | + "grade": "PSA9", | |
| 261 | + "count": 6 | |
| 262 | + }, | |
| 263 | + { | |
| 264 | + "month": "Apr, 2018", | |
| 265 | + "grade": "PSA8", | |
| 266 | + "count": 2 | |
| 267 | + }, | |
| 268 | + { | |
| 269 | + "month": "Apr, 2018", | |
| 270 | + "grade": "PSA5", | |
| 271 | + "count": 1 | |
| 272 | + }, | |
| 273 | + { | |
| 274 | + "month": "Apr, 2018", | |
| 275 | + "grade": "PSA1", | |
| 276 | + "count": 1 | |
| 277 | + }, | |
| 278 | + { | |
| 279 | + "month": "Apr, 2018", | |
| 280 | + "grade": "PSA3", | |
| 281 | + "count": 1 | |
| 282 | + }, | |
| 283 | + { | |
| 284 | + "month": "May, 2018", | |
| 285 | + "grade": "PSA8", | |
| 286 | + "count": 4 | |
| 287 | + }, | |
| 288 | + { | |
| 289 | + "month": "May, 2018", | |
| 290 | + "grade": "PSA7", | |
| 291 | + "count": 1 | |
| 292 | + }, | |
| 293 | + { | |
| 294 | + "month": "May, 2018", | |
| 295 | + "grade": "PSA10", | |
| 296 | + "count": 2 | |
| 297 | + }, | |
| 298 | + { | |
| 299 | + "month": "May, 2018", | |
| 300 | + "grade": "PSA5", | |
| 301 | + "count": 1 | |
| 302 | + }, | |
| 303 | + { | |
| 304 | + "month": "May, 2018", | |
| 305 | + "grade": "PSA6", | |
| 306 | + "count": 1 | |
| 307 | + }, | |
| 308 | + { | |
| 309 | + "month": "May, 2018", | |
| 310 | + "grade": "PSA9", | |
| 311 | + "count": 2 | |
| 312 | + }, | |
| 313 | + { | |
| 314 | + "month": "Jun, 2018", | |
| 315 | + "grade": "PSA8", | |
| 316 | + "count": 2 | |
| 317 | + }, | |
| 318 | + { | |
| 319 | + "month": "Jul, 2018", | |
| 320 | + "grade": "PSA6", | |
| 321 | + "count": 1 | |
| 322 | + }, | |
| 323 | + { | |
| 324 | + "month": "Jul, 2018", | |
| 325 | + "grade": "PSA8", | |
| 326 | + "count": 2 | |
| 327 | + }, | |
| 328 | + { | |
| 329 | + "month": "Jul, 2018", | |
| 330 | + "grade": "PSA7", | |
| 331 | + "count": 2 | |
| 332 | + }, | |
| 333 | + { | |
| 334 | + "month": "Jul, 2018", | |
| 335 | + "grade": "PSA2", | |
| 336 | + "count": 1 | |
| 337 | + }, | |
| 338 | + { | |
| 339 | + "month": "Aug, 2018", | |
| 340 | + "grade": "PSA9", | |
| 341 | + "count": 4 | |
| 342 | + }, | |
| 343 | + { | |
| 344 | + "month": "Aug, 2018", | |
| 345 | + "grade": "PSA10", | |
| 346 | + "count": 2 | |
| 347 | + }, | |
| 348 | + { | |
| 349 | + "month": "Aug, 2018", | |
| 350 | + "grade": "PSA8", | |
| 351 | + "count": 2 | |
| 352 | + }, | |
| 353 | + { | |
| 354 | + "month": "Aug, 2018", | |
| 355 | + "grade": "PSA7", | |
| 356 | + "count": 1 | |
| 357 | + }, | |
| 358 | + { | |
| 359 | + "month": "Sep, 2018", | |
| 360 | + "grade": "PSA8", | |
| 361 | + "count": 1 | |
| 362 | + }, | |
| 363 | + { | |
| 364 | + "month": "Oct, 2018", | |
| 365 | + "grade": "PSA8", | |
| 366 | + "count": 1 | |
| 367 | + }, | |
| 368 | + { | |
| 369 | + "month": "Nov, 2018", | |
| 370 | + "grade": "PSA9", | |
| 371 | + "count": 1 | |
| 372 | + }, | |
| 373 | + { | |
| 374 | + "month": "Nov, 2018", | |
| 375 | + "grade": "PSA8", | |
| 376 | + "count": 1 | |
| 377 | + }, | |
| 378 | + { | |
| 379 | + "month": "Dec, 2018", | |
| 380 | + "grade": "PSA4", | |
| 381 | + "count": 1 | |
| 382 | + }, | |
| 383 | + { | |
| 384 | + "month": "Dec, 2018", | |
| 385 | + "grade": "PSA8", | |
| 386 | + "count": 2 | |
| 387 | + }, | |
| 388 | + { | |
| 389 | + "month": "Jan, 2019", | |
| 390 | + "grade": "PSA8", | |
| 391 | + "count": 3 | |
| 392 | + }, | |
| 393 | + { | |
| 394 | + "month": "Jan, 2019", | |
| 395 | + "grade": "PSA4", | |
| 396 | + "count": 1 | |
| 397 | + }, | |
| 398 | + { | |
| 399 | + "month": "Jan, 2019", | |
| 400 | + "grade": "PSA9", | |
| 401 | + "count": 1 | |
| 402 | + }, | |
| 403 | + { | |
| 404 | + "month": "Feb, 2019", | |
| 405 | + "grade": "PSA8", | |
| 406 | + "count": 2 | |
| 407 | + }, | |
| 408 | + { | |
| 409 | + "month": "Feb, 2019", | |
| 410 | + "grade": "PSA6", | |
| 411 | + "count": 1 | |
| 412 | + }, | |
| 413 | + { | |
| 414 | + "month": "Mar, 2019", | |
| 415 | + "grade": "PSA10", | |
| 416 | + "count": 1 | |
| 417 | + }, | |
| 418 | + { | |
| 419 | + "month": "Mar, 2019", | |
| 420 | + "grade": "PSA7", | |
| 421 | + "count": 1 | |
| 422 | + }, | |
| 423 | + { | |
| 424 | + "month": "Mar, 2019", | |
| 425 | + "grade": "PSA9", | |
| 426 | + "count": 3 | |
| 427 | + }, | |
| 428 | + { | |
| 429 | + "month": "Mar, 2019", | |
| 430 | + "grade": "PSA6", | |
| 431 | + "count": 1 | |
| 432 | + }, | |
| 433 | + { | |
| 434 | + "month": "Apr, 2019", | |
| 435 | + "grade": "PSA8", | |
| 436 | + "count": 1 | |
| 437 | + }, | |
| 438 | + { | |
| 439 | + "month": "Apr, 2019", | |
| 440 | + "grade": "PSA9", | |
| 441 | + "count": 1 | |
| 442 | + }, | |
| 443 | + { | |
| 444 | + "month": "May, 2019", | |
| 445 | + "grade": "PSA9", | |
| 446 | + "count": 2 | |
| 447 | + }, | |
| 448 | + { | |
| 449 | + "month": "May, 2019", | |
| 450 | + "grade": "PSA8", | |
| 451 | + "count": 1 | |
| 452 | + }, | |
| 453 | + { | |
| 454 | + "month": "Jun, 2019", | |
| 455 | + "grade": "PSA9", | |
| 456 | + "count": 3 | |
| 457 | + }, | |
| 458 | + { | |
| 459 | + "month": "Jun, 2019", | |
| 460 | + "grade": "PSA8", | |
| 461 | + "count": 1 | |
| 462 | + }, | |
| 463 | + { | |
| 464 | + "month": "Jul, 2019", | |
| 465 | + "grade": "PSA9", | |
| 466 | + "count": 1 | |
| 467 | + }, | |
| 468 | + { | |
| 469 | + "month": "Jul, 2019", | |
| 470 | + "grade": "PSA5", | |
| 471 | + "count": 1 | |
| 472 | + }, | |
| 473 | + { | |
| 474 | + "month": "Jul, 2019", | |
| 475 | + "grade": "PSA1", | |
| 476 | + "count": 1 | |
| 477 | + }, | |
| 478 | + { | |
| 479 | + "month": "Aug, 2019", | |
| 480 | + "grade": "PSA7", | |
| 481 | + "count": 1 | |
| 482 | + }, | |
| 483 | + { | |
| 484 | + "month": "Aug, 2019", | |
| 485 | + "grade": "PSA6", | |
| 486 | + "count": 1 | |
| 487 | + }, | |
| 488 | + { | |
| 489 | + "month": "Aug, 2019", | |
| 490 | + "grade": "PSA9", | |
| 491 | + "count": 1 | |
| 492 | + }, | |
| 493 | + { | |
| 494 | + "month": "Sep, 2019", | |
| 495 | + "grade": "PSA8", | |
| 496 | + "count": 2 | |
| 497 | + }, | |
| 498 | + { | |
| 499 | + "month": "Oct, 2019", | |
| 500 | + "grade": "PSA8", | |
| 501 | + "count": 2 | |
| 502 | + }, | |
| 503 | + { | |
| 504 | + "month": "Oct, 2019", | |
| 505 | + "grade": "PSA4", | |
| 506 | + "count": 1 | |
| 507 | + }, | |
| 508 | + { | |
| 509 | + "month": "Oct, 2019", | |
| 510 | + "grade": "PSA3", | |
| 511 | + "count": 1 | |
| 512 | + }, | |
| 513 | + { | |
| 514 | + "month": "Nov, 2019", | |
| 515 | + "grade": "PSA8", | |
| 516 | + "count": 1 | |
| 517 | + }, | |
| 518 | + { | |
| 519 | + "month": "Nov, 2019", | |
| 520 | + "grade": "PSA5", | |
| 521 | + "count": 1 | |
| 522 | + }, | |
| 523 | + { | |
| 524 | + "month": "Nov, 2019", | |
| 525 | + "grade": "PSA9", | |
| 526 | + "count": 1 | |
| 527 | + }, | |
| 528 | + { | |
| 529 | + "month": "Nov, 2019", | |
| 530 | + "grade": "PSA7", | |
| 531 | + "count": 1 | |
| 532 | + }, | |
| 533 | + { | |
| 534 | + "month": "Dec, 2019", | |
| 535 | + "grade": "PSA2", | |
| 536 | + "count": 2 | |
| 537 | + }, | |
| 538 | + { | |
| 539 | + "month": "Dec, 2019", | |
| 540 | + "grade": "PSA9", | |
| 541 | + "count": 2 | |
| 542 | + }, | |
| 543 | + { | |
| 544 | + "month": "Dec, 2019", | |
| 545 | + "grade": "PSA8", | |
| 546 | + "count": 1 | |
| 547 | + }, | |
| 548 | + { | |
| 549 | + "month": "Dec, 2019", | |
| 550 | + "grade": "PSA4", | |
| 551 | + "count": 1 | |
| 552 | + }, | |
| 553 | + { | |
| 554 | + "month": "Jan, 2020", | |
| 555 | + "grade": "PSA8", | |
| 556 | + "count": 1 | |
| 557 | + }, | |
| 558 | + { | |
| 559 | + "month": "Feb, 2020", | |
| 560 | + "grade": "PSA7", | |
| 561 | + "count": 1 | |
| 562 | + }, | |
| 563 | + { | |
| 564 | + "month": "Feb, 2020", | |
| 565 | + "grade": "PSA6", | |
| 566 | + "count": 1 | |
| 567 | + }, | |
| 568 | + { | |
| 569 | + "month": "Feb, 2020", | |
| 570 | + "grade": "PSA9", | |
| 571 | + "count": 1 | |
| 572 | + }, | |
| 573 | + { | |
| 574 | + "month": "Mar, 2020", | |
| 575 | + "grade": "PSA9", | |
| 576 | + "count": 2 | |
| 577 | + }, | |
| 578 | + { | |
| 579 | + "month": "Mar, 2020", | |
| 580 | + "grade": "PSA6", | |
| 581 | + "count": 1 | |
| 582 | + }, | |
| 583 | + { | |
| 584 | + "month": "Mar, 2020", | |
| 585 | + "grade": "PSA4", | |
| 586 | + "count": 1 | |
| 587 | + }, | |
| 588 | + { | |
| 589 | + "month": "Mar, 2020", | |
| 590 | + "grade": "PSA8", | |
| 591 | + "count": 1 | |
| 592 | + }, | |
| 593 | + { | |
| 594 | + "month": "Mar, 2020", | |
| 595 | + "grade": "PSA7", | |
| 596 | + "count": 1 | |
| 597 | + }, | |
| 598 | + { | |
| 599 | + "month": "Apr, 2020", | |
| 600 | + "grade": "PSA9", | |
| 601 | + "count": 3 | |
| 602 | + }, | |
| 603 | + { | |
| 604 | + "month": "Apr, 2020", | |
| 605 | + "grade": "PSA8", | |
| 606 | + "count": 5 | |
| 607 | + }, | |
| 608 | + { | |
| 609 | + "month": "Apr, 2020", | |
| 610 | + "grade": "PSA6", | |
| 611 | + "count": 1 | |
| 612 | + }, | |
| 613 | + { | |
| 614 | + "month": "May, 2020", | |
| 615 | + "grade": "PSA6", | |
| 616 | + "count": 2 | |
| 617 | + }, | |
| 618 | + { | |
| 619 | + "month": "May, 2020", | |
| 620 | + "grade": "PSA5", | |
| 621 | + "count": 2 | |
| 622 | + }, | |
| 623 | + { | |
| 624 | + "month": "May, 2020", | |
| 625 | + "grade": "PSA7", | |
| 626 | + "count": 1 | |
| 627 | + }, | |
| 628 | + { | |
| 629 | + "month": "May, 2020", | |
| 630 | + "grade": "PSA8", | |
| 631 | + "count": 1 | |
| 632 | + }, | |
| 633 | + { | |
| 634 | + "month": "May, 2020", | |
| 635 | + "grade": "PSA1", | |
| 636 | + "count": 1 | |
| 637 | + }, | |
| 638 | + { | |
| 639 | + "month": "Jun, 2020", | |
| 640 | + "grade": "PSA7", | |
| 641 | + "count": 2 | |
| 642 | + }, | |
| 643 | + { | |
| 644 | + "month": "Jun, 2020", | |
| 645 | + "grade": "PSA8", | |
| 646 | + "count": 3 | |
| 647 | + }, | |
| 648 | + { | |
| 649 | + "month": "Jun, 2020", | |
| 650 | + "grade": "PSA9", | |
| 651 | + "count": 3 | |
| 652 | + }, | |
| 653 | + { | |
| 654 | + "month": "Jun, 2020", | |
| 655 | + "grade": "PSA4", | |
| 656 | + "count": 1 | |
| 657 | + }, | |
| 658 | + { | |
| 659 | + "month": "Jun, 2020", | |
| 660 | + "grade": "PSA5", | |
| 661 | + "count": 1 | |
| 662 | + }, | |
| 663 | + { | |
| 664 | + "month": "Jul, 2020", | |
| 665 | + "grade": "PSA6", | |
| 666 | + "count": 2 | |
| 667 | + }, | |
| 668 | + { | |
| 669 | + "month": "Jul, 2020", | |
| 670 | + "grade": "PSA9", | |
| 671 | + "count": 3 | |
| 672 | + }, | |
| 673 | + { | |
| 674 | + "month": "Jul, 2020", | |
| 675 | + "grade": "PSA4", | |
| 676 | + "count": 1 | |
| 677 | + }, | |
| 678 | + { | |
| 679 | + "month": "Jul, 2020", | |
| 680 | + "grade": "PSA8", | |
| 681 | + "count": 3 | |
| 682 | + }, | |
| 683 | + { | |
| 684 | + "month": "Aug, 2020", | |
| 685 | + "grade": "PSA5", | |
| 686 | + "count": 1 | |
| 687 | + }, | |
| 688 | + { | |
| 689 | + "month": "Aug, 2020", | |
| 690 | + "grade": "PSA6", | |
| 691 | + "count": 2 | |
| 692 | + }, | |
| 693 | + { | |
| 694 | + "month": "Aug, 2020", | |
| 695 | + "grade": "PSA8", | |
| 696 | + "count": 7 | |
| 697 | + }, | |
| 698 | + { | |
| 699 | + "month": "Aug, 2020", | |
| 700 | + "grade": "PSA7", | |
| 701 | + "count": 1 | |
| 702 | + }, | |
| 703 | + { | |
| 704 | + "month": "Aug, 2020", | |
| 705 | + "grade": "PSA9", | |
| 706 | + "count": 1 | |
| 707 | + }, | |
| 708 | + { | |
| 709 | + "month": "Sep, 2020", | |
| 710 | + "grade": "PSA9", | |
| 711 | + "count": 3 | |
| 712 | + }, | |
| 713 | + { | |
| 714 | + "month": "Sep, 2020", | |
| 715 | + "grade": "PSA7", | |
| 716 | + "count": 2 | |
| 717 | + }, | |
| 718 | + { | |
| 719 | + "month": "Sep, 2020", | |
| 720 | + "grade": "PSA8", | |
| 721 | + "count": 2 | |
| 722 | + }, | |
| 723 | + { | |
| 724 | + "month": "Sep, 2020", | |
| 725 | + "grade": "PSA6", | |
| 726 | + "count": 1 | |
| 727 | + }, | |
| 728 | + { | |
| 729 | + "month": "Sep, 2020", | |
| 730 | + "grade": "PSA3", | |
| 731 | + "count": 1 | |
| 732 | + }, | |
| 733 | + { | |
| 734 | + "month": "Oct, 2020", | |
| 735 | + "grade": "PSA8", | |
| 736 | + "count": 6 | |
| 737 | + }, | |
| 738 | + { | |
| 739 | + "month": "Oct, 2020", | |
| 740 | + "grade": "PSA4", | |
| 741 | + "count": 1 | |
| 742 | + }, | |
| 743 | + { | |
| 744 | + "month": "Oct, 2020", | |
| 745 | + "grade": "PSA9", | |
| 746 | + "count": 5 | |
| 747 | + }, | |
| 748 | + { | |
| 749 | + "month": "Oct, 2020", | |
| 750 | + "grade": "PSA7", | |
| 751 | + "count": 7 | |
| 752 | + }, | |
| 753 | + { | |
| 754 | + "month": "Oct, 2020", | |
| 755 | + "grade": "PSA6", | |
| 756 | + "count": 2 | |
| 757 | + }, | |
| 758 | + { | |
| 759 | + "month": "Oct, 2020", | |
| 760 | + "grade": "PSA5", | |
| 761 | + "count": 2 | |
| 762 | + }, | |
| 763 | + { | |
| 764 | + "month": "Nov, 2020", | |
| 765 | + "grade": "PSA3", | |
| 766 | + "count": 1 | |
| 767 | + }, | |
| 768 | + { | |
| 769 | + "month": "Nov, 2020", | |
| 770 | + "grade": "PSA8", | |
| 771 | + "count": 4 | |
| 772 | + }, | |
| 773 | + { | |
| 774 | + "month": "Nov, 2020", | |
| 775 | + "grade": "PSA6", | |
| 776 | + "count": 2 | |
| 777 | + }, | |
| 778 | + { | |
| 779 | + "month": "Nov, 2020", | |
| 780 | + "grade": "PSA9", | |
| 781 | + "count": 5 | |
| 782 | + }, | |
| 783 | + { | |
| 784 | + "month": "Nov, 2020", | |
| 785 | + "grade": "PSA7", | |
| 786 | + "count": 2 | |
| 787 | + }, | |
| 788 | + { | |
| 789 | + "month": "Dec, 2020", | |
| 790 | + "grade": "PSA6", | |
| 791 | + "count": 2 | |
| 792 | + }, | |
| 793 | + { | |
| 794 | + "month": "Dec, 2020", | |
| 795 | + "grade": "PSA9", | |
| 796 | + "count": 11 | |
| 797 | + }, | |
| 798 | + { | |
| 799 | + "month": "Dec, 2020", | |
| 800 | + "grade": "PSA8", | |
| 801 | + "count": 9 | |
| 802 | + }, | |
| 803 | + { | |
| 804 | + "month": "Dec, 2020", | |
| 805 | + "grade": "PSA5", | |
| 806 | + "count": 2 | |
| 807 | + }, | |
| 808 | + { | |
| 809 | + "month": "Dec, 2020", | |
| 810 | + "grade": "PSA4", | |
| 811 | + "count": 1 | |
| 812 | + }, | |
| 813 | + { | |
| 814 | + "month": "Dec, 2020", | |
| 815 | + "grade": "PSA7", | |
| 816 | + "count": 1 | |
| 817 | + }, | |
| 818 | + { | |
| 819 | + "month": "Jan, 2021", | |
| 820 | + "grade": "PSA9", | |
| 821 | + "count": 8 | |
| 822 | + }, | |
| 823 | + { | |
| 824 | + "month": "Jan, 2021", | |
| 825 | + "grade": "PSA7", | |
| 826 | + "count": 3 | |
| 827 | + }, | |
| 828 | + { | |
| 829 | + "month": "Jan, 2021", | |
| 830 | + "grade": "PSA8", | |
| 831 | + "count": 5 | |
| 832 | + }, | |
| 833 | + { | |
| 834 | + "month": "Jan, 2021", | |
| 835 | + "grade": "PSA6", | |
| 836 | + "count": 3 | |
| 837 | + }, | |
| 838 | + { | |
| 839 | + "month": "Jan, 2021", | |
| 840 | + "grade": "PSA4", | |
| 841 | + "count": 1 | |
| 842 | + }, | |
| 843 | + { | |
| 844 | + "month": "Feb, 2021", | |
| 845 | + "grade": "PSA5", | |
| 846 | + "count": 1 | |
| 847 | + }, | |
| 848 | + { | |
| 849 | + "month": "Feb, 2021", | |
| 850 | + "grade": "PSA9", | |
| 851 | + "count": 1 | |
| 852 | + }, | |
| 853 | + { | |
| 854 | + "month": "Feb, 2021", | |
| 855 | + "grade": "PSA8", | |
| 856 | + "count": 7 | |
| 857 | + }, | |
| 858 | + { | |
| 859 | + "month": "Feb, 2021", | |
| 860 | + "grade": "PSA7", | |
| 861 | + "count": 1 | |
| 862 | + }, | |
| 863 | + { | |
| 864 | + "month": "Feb, 2021", | |
| 865 | + "grade": "PSA6", | |
| 866 | + "count": 2 | |
| 867 | + }, | |
| 868 | + { | |
| 869 | + "month": "Mar, 2021", | |
| 870 | + "grade": "PSA8", | |
| 871 | + "count": 6 | |
| 872 | + }, | |
| 873 | + { | |
| 874 | + "month": "Mar, 2021", | |
| 875 | + "grade": "PSA6", | |
| 876 | + "count": 1 | |
| 877 | + }, | |
| 878 | + { | |
| 879 | + "month": "Mar, 2021", | |
| 880 | + "grade": "PSA5", | |
| 881 | + "count": 2 | |
| 882 | + }, | |
| 883 | + { | |
| 884 | + "month": "Mar, 2021", | |
| 885 | + "grade": "PSA7", | |
| 886 | + "count": 1 | |
| 887 | + }, | |
| 888 | + { | |
| 889 | + "month": "Apr, 2021", | |
| 890 | + "grade": "PSA6", | |
| 891 | + "count": 5 | |
| 892 | + }, | |
| 893 | + { | |
| 894 | + "month": "Apr, 2021", | |
| 895 | + "grade": "PSA9", | |
| 896 | + "count": 6 | |
| 897 | + }, | |
| 898 | + { | |
| 899 | + "month": "Apr, 2021", | |
| 900 | + "grade": "PSA8", | |
| 901 | + "count": 4 | |
| 902 | + }, | |
| 903 | + { | |
| 904 | + "month": "Apr, 2021", | |
| 905 | + "grade": "PSA3", | |
| 906 | + "count": 1 | |
| 907 | + }, | |
| 908 | + { | |
| 909 | + "month": "May, 2021", | |
| 910 | + "grade": "PSA2", | |
| 911 | + "count": 1 | |
| 912 | + }, | |
| 913 | + { | |
| 914 | + "month": "May, 2021", | |
| 915 | + "grade": "PSA9", | |
| 916 | + "count": 1 | |
| 917 | + }, | |
| 918 | + { | |
| 919 | + "month": "May, 2021", | |
| 920 | + "grade": "PSA6", | |
| 921 | + "count": 1 | |
| 922 | + }, | |
| 923 | + { | |
| 924 | + "month": "May, 2021", | |
| 925 | + "grade": "PSA5", | |
| 926 | + "count": 1 | |
| 927 | + }, | |
| 928 | + { | |
| 929 | + "month": "May, 2021", | |
| 930 | + "grade": "PSA7", | |
| 931 | + "count": 2 | |
| 932 | + }, | |
| 933 | + { | |
| 934 | + "month": "Jun, 2021", | |
| 935 | + "grade": "PSA8", | |
| 936 | + "count": 1 | |
| 937 | + }, | |
| 938 | + { | |
| 939 | + "month": "Jul, 2021", | |
| 940 | + "grade": "PSA8", | |
| 941 | + "count": 1 | |
| 942 | + }, | |
| 943 | + { | |
| 944 | + "month": "Jul, 2021", | |
| 945 | + "grade": "PSA9", | |
| 946 | + "count": 2 | |
| 947 | + }, | |
| 948 | + { | |
| 949 | + "month": "Jul, 2021", | |
| 950 | + "grade": "PSA6", | |
| 951 | + "count": 1 | |
| 952 | + }, | |
| 953 | + { | |
| 954 | + "month": "Jul, 2021", | |
| 955 | + "grade": "PSA7", | |
| 956 | + "count": 1 | |
| 957 | + }, | |
| 958 | + { | |
| 959 | + "month": "Jul, 2021", | |
| 960 | + "grade": "PSA5", | |
| 961 | + "count": 1 | |
| 962 | + }, | |
| 963 | + { | |
| 964 | + "month": "Aug, 2021", | |
| 965 | + "grade": "PSA6", | |
| 966 | + "count": 1 | |
| 967 | + }, | |
| 968 | + { | |
| 969 | + "month": "Aug, 2021", | |
| 970 | + "grade": "PSA9", | |
| 971 | + "count": 1 | |
| 972 | + }, | |
| 973 | + { | |
| 974 | + "month": "Aug, 2021", | |
| 975 | + "grade": "PSA8", | |
| 976 | + "count": 2 | |
| 977 | + }, | |
| 978 | + { | |
| 979 | + "month": "Aug, 2021", | |
| 980 | + "grade": "PSA7", | |
| 981 | + "count": 1 | |
| 982 | + }, | |
| 983 | + { | |
| 984 | + "month": "Sep, 2021", | |
| 985 | + "grade": "PSA4", | |
| 986 | + "count": 1 | |
| 987 | + }, | |
| 988 | + { | |
| 989 | + "month": "Sep, 2021", | |
| 990 | + "grade": "PSA6", | |
| 991 | + "count": 2 | |
| 992 | + }, | |
| 993 | + { | |
| 994 | + "month": "Sep, 2021", | |
| 995 | + "grade": "PSA8", | |
| 996 | + "count": 1 | |
| 997 | + }, | |
| 998 | + { | |
| 999 | + "month": "Sep, 2021", | |
| 1000 | + "grade": "PSA5", | |
| 1001 | + "count": 1 | |
| 1002 | + }, | |
| 1003 | + { | |
| 1004 | + "month": "Oct, 2021", | |
| 1005 | + "grade": "PSA6", | |
| 1006 | + "count": 3 | |
| 1007 | + }, | |
| 1008 | + { | |
| 1009 | + "month": "Oct, 2021", | |
| 1010 | + "grade": "PSA2", | |
| 1011 | + "count": 1 | |
| 1012 | + }, | |
| 1013 | + { | |
| 1014 | + "month": "Oct, 2021", | |
| 1015 | + "grade": "PSA8", | |
| 1016 | + "count": 1 | |
| 1017 | + }, | |
| 1018 | + { | |
| 1019 | + "month": "Nov, 2021", | |
| 1020 | + "grade": "PSA6", | |
| 1021 | + "count": 2 | |
| 1022 | + }, | |
| 1023 | + { | |
| 1024 | + "month": "Nov, 2021", | |
| 1025 | + "grade": "PSA5", | |
| 1026 | + "count": 2 | |
| 1027 | + }, | |
| 1028 | + { | |
| 1029 | + "month": "Nov, 2021", | |
| 1030 | + "grade": "PSA7", | |
| 1031 | + "count": 1 | |
| 1032 | + }, | |
| 1033 | + { | |
| 1034 | + "month": "Dec, 2021", | |
| 1035 | + "grade": "PSA5", | |
| 1036 | + "count": 2 | |
| 1037 | + }, | |
| 1038 | + { | |
| 1039 | + "month": "Dec, 2021", | |
| 1040 | + "grade": "PSA7", | |
| 1041 | + "count": 1 | |
| 1042 | + }, | |
| 1043 | + { | |
| 1044 | + "month": "Dec, 2021", | |
| 1045 | + "grade": "PSA8", | |
| 1046 | + "count": 2 | |
| 1047 | + }, | |
| 1048 | + { | |
| 1049 | + "month": "Dec, 2021", | |
| 1050 | + "grade": "PSA4", | |
| 1051 | + "count": 1 | |
| 1052 | + }, | |
| 1053 | + { | |
| 1054 | + "month": "Dec, 2021", | |
| 1055 | + "grade": "PSA6", | |
| 1056 | + "count": 1 | |
| 1057 | + }, | |
| 1058 | + { | |
| 1059 | + "month": "Dec, 2021", | |
| 1060 | + "grade": "PSA9", | |
| 1061 | + "count": 1 | |
| 1062 | + }, | |
| 1063 | + { | |
| 1064 | + "month": "Jan, 2022", | |
| 1065 | + "grade": "PSA8", | |
| 1066 | + "count": 1 | |
| 1067 | + }, | |
| 1068 | + { | |
| 1069 | + "month": "Jan, 2022", | |
| 1070 | + "grade": "PSA7", | |
| 1071 | + "count": 1 | |
| 1072 | + }, | |
| 1073 | + { | |
| 1074 | + "month": "Jan, 2022", | |
| 1075 | + "grade": "PSA3", | |
| 1076 | + "count": 1 | |
| 1077 | + }, | |
| 1078 | + { | |
| 1079 | + "month": "Feb, 2022", | |
| 1080 | + "grade": "PSA9", | |
| 1081 | + "count": 1 | |
| 1082 | + }, | |
| 1083 | + { | |
| 1084 | + "month": "Mar, 2022", | |
| 1085 | + "grade": "PSA4", | |
| 1086 | + "count": 1 | |
| 1087 | + }, | |
| 1088 | + { | |
| 1089 | + "month": "May, 2022", | |
| 1090 | + "grade": "PSA5", | |
| 1091 | + "count": 1 | |
| 1092 | + }, | |
| 1093 | + { | |
| 1094 | + "month": "May, 2022", | |
| 1095 | + "grade": "PSA4", | |
| 1096 | + "count": 2 | |
| 1097 | + }, | |
| 1098 | + { | |
| 1099 | + "month": "Jun, 2022", | |
| 1100 | + "grade": "PSA5", | |
| 1101 | + "count": 1 | |
| 1102 | + }, | |
| 1103 | + { | |
| 1104 | + "month": "Jun, 2022", | |
| 1105 | + "grade": "PSA8", | |
| 1106 | + "count": 1 | |
| 1107 | + }, | |
| 1108 | + { | |
| 1109 | + "month": "Oct, 2022", | |
| 1110 | + "grade": "PSA7", | |
| 1111 | + "count": 2 | |
| 1112 | + }, | |
| 1113 | + { | |
| 1114 | + "month": "Oct, 2022", | |
| 1115 | + "grade": "PSA4", | |
| 1116 | + "count": 1 | |
| 1117 | + }, | |
| 1118 | + { | |
| 1119 | + "month": "Dec, 2022", | |
| 1120 | + "grade": "PSA5", | |
| 1121 | + "count": 2 | |
| 1122 | + }, | |
| 1123 | + { | |
| 1124 | + "month": "Dec, 2022", | |
| 1125 | + "grade": "PSA9", | |
| 1126 | + "count": 1 | |
| 1127 | + }, | |
| 1128 | + { | |
| 1129 | + "month": "Dec, 2022", | |
| 1130 | + "grade": "PSA4", | |
| 1131 | + "count": 1 | |
| 1132 | + }, | |
| 1133 | + { | |
| 1134 | + "month": "Dec, 2022", | |
| 1135 | + "grade": "PSA7", | |
| 1136 | + "count": 1 | |
| 1137 | + }, | |
| 1138 | + { | |
| 1139 | + "month": "Dec, 2022", | |
| 1140 | + "grade": "PSA6", | |
| 1141 | + "count": 2 | |
| 1142 | + }, | |
| 1143 | + { | |
| 1144 | + "month": "Dec, 2022", | |
| 1145 | + "grade": "PSA8", | |
| 1146 | + "count": 1 | |
| 1147 | + }, | |
| 1148 | + { | |
| 1149 | + "month": "Jan, 2023", | |
| 1150 | + "grade": "PSA6", | |
| 1151 | + "count": 2 | |
| 1152 | + }, | |
| 1153 | + { | |
| 1154 | + "month": "Jan, 2023", | |
| 1155 | + "grade": "PSA8", | |
| 1156 | + "count": 3 | |
| 1157 | + }, | |
| 1158 | + { | |
| 1159 | + "month": "Feb, 2023", | |
| 1160 | + "grade": "PSA7", | |
| 1161 | + "count": 4 | |
| 1162 | + }, | |
| 1163 | + { | |
| 1164 | + "month": "Feb, 2023", | |
| 1165 | + "grade": "PSA8", | |
| 1166 | + "count": 4 | |
| 1167 | + }, | |
| 1168 | + { | |
| 1169 | + "month": "Feb, 2023", | |
| 1170 | + "grade": "PSA5", | |
| 1171 | + "count": 1 | |
| 1172 | + }, | |
| 1173 | + { | |
| 1174 | + "month": "Feb, 2023", | |
| 1175 | + "grade": "PSA6", | |
| 1176 | + "count": 1 | |
| 1177 | + }, | |
| 1178 | + { | |
| 1179 | + "month": "Mar, 2023", | |
| 1180 | + "grade": "PSA8", | |
| 1181 | + "count": 2 | |
| 1182 | + }, | |
| 1183 | + { | |
| 1184 | + "month": "Mar, 2023", | |
| 1185 | + "grade": "PSA6", | |
| 1186 | + "count": 3 | |
| 1187 | + }, | |
| 1188 | + { | |
| 1189 | + "month": "Mar, 2023", | |
| 1190 | + "grade": "PSA7", | |
| 1191 | + "count": 3 | |
| 1192 | + }, | |
| 1193 | + { | |
| 1194 | + "month": "Mar, 2023", | |
| 1195 | + "grade": "PSA5", | |
| 1196 | + "count": 2 | |
| 1197 | + }, | |
| 1198 | + { | |
| 1199 | + "month": "Apr, 2023", | |
| 1200 | + "grade": "PSA8", | |
| 1201 | + "count": 1 | |
| 1202 | + }, | |
| 1203 | + { | |
| 1204 | + "month": "May, 2023", | |
| 1205 | + "grade": "PSA7", | |
| 1206 | + "count": 1 | |
| 1207 | + }, | |
| 1208 | + { | |
| 1209 | + "month": "Jun, 2023", | |
| 1210 | + "grade": "PSA5", | |
| 1211 | + "count": 1 | |
| 1212 | + }, | |
| 1213 | + { | |
| 1214 | + "month": "Jul, 2023", | |
| 1215 | + "grade": "PSA2", | |
| 1216 | + "count": 1 | |
| 1217 | + }, | |
| 1218 | + { | |
| 1219 | + "month": "Jul, 2023", | |
| 1220 | + "grade": "PSA3", | |
| 1221 | + "count": 1 | |
| 1222 | + }, | |
| 1223 | + { | |
| 1224 | + "month": "Aug, 2023", | |
| 1225 | + "grade": "PSA6", | |
| 1226 | + "count": 3 | |
| 1227 | + }, | |
| 1228 | + { | |
| 1229 | + "month": "Aug, 2023", | |
| 1230 | + "grade": "PSA5", | |
| 1231 | + "count": 1 | |
| 1232 | + }, | |
| 1233 | + { | |
| 1234 | + "month": "Aug, 2023", | |
| 1235 | + "grade": "PSA8", | |
| 1236 | + "count": 1 | |
| 1237 | + }, | |
| 1238 | + { | |
| 1239 | + "month": "Aug, 2023", | |
| 1240 | + "grade": "PSA4", | |
| 1241 | + "count": 1 | |
| 1242 | + }, | |
| 1243 | + { | |
| 1244 | + "month": "Sep, 2023", | |
| 1245 | + "grade": "PSA6", | |
| 1246 | + "count": 2 | |
| 1247 | + }, | |
| 1248 | + { | |
| 1249 | + "month": "Sep, 2023", | |
| 1250 | + "grade": "PSA8", | |
| 1251 | + "count": 1 | |
| 1252 | + }, | |
| 1253 | + { | |
| 1254 | + "month": "Sep, 2023", | |
| 1255 | + "grade": "PSA5", | |
| 1256 | + "count": 1 | |
| 1257 | + }, | |
| 1258 | + { | |
| 1259 | + "month": "Oct, 2023", | |
| 1260 | + "grade": "PSA6", | |
| 1261 | + "count": 1 | |
| 1262 | + }, | |
| 1263 | + { | |
| 1264 | + "month": "Oct, 2023", | |
| 1265 | + "grade": "PSA7", | |
| 1266 | + "count": 1 | |
| 1267 | + }, | |
| 1268 | + { | |
| 1269 | + "month": "Oct, 2023", | |
| 1270 | + "grade": "PSA8", | |
| 1271 | + "count": 1 | |
| 1272 | + }, | |
| 1273 | + { | |
| 1274 | + "month": "Jan, 2024", | |
| 1275 | + "grade": "Raw", | |
| 1276 | + "count": 2 | |
| 1277 | + }, | |
| 1278 | + { | |
| 1279 | + "month": "Feb, 2024", | |
| 1280 | + "grade": "Raw", | |
| 1281 | + "count": 3 | |
| 1282 | + }, | |
| 1283 | + { | |
| 1284 | + "month": "Mar, 2024", | |
| 1285 | + "grade": "Raw", | |
| 1286 | + "count": 5 | |
| 1287 | + }, | |
| 1288 | + { | |
| 1289 | + "month": "Mar, 2024", | |
| 1290 | + "grade": "PSA9", | |
| 1291 | + "count": 1 | |
| 1292 | + }, | |
| 1293 | + { | |
| 1294 | + "month": "Apr, 2024", | |
| 1295 | + "grade": "PSA7", | |
| 1296 | + "count": 1 | |
| 1297 | + }, | |
| 1298 | + { | |
| 1299 | + "month": "Apr, 2024", | |
| 1300 | + "grade": "Raw", | |
| 1301 | + "count": 1 | |
| 1302 | + }, | |
| 1303 | + { | |
| 1304 | + "month": "May, 2024", | |
| 1305 | + "grade": "Raw", | |
| 1306 | + "count": 2 | |
| 1307 | + }, | |
| 1308 | + { | |
| 1309 | + "month": "May, 2024", | |
| 1310 | + "grade": "PSA9", | |
| 1311 | + "count": 3 | |
| 1312 | + }, | |
| 1313 | + { | |
| 1314 | + "month": "May, 2024", | |
| 1315 | + "grade": "PSA7", | |
| 1316 | + "count": 1 | |
| 1317 | + }, | |
| 1318 | + { | |
| 1319 | + "month": "Jun, 2024", | |
| 1320 | + "grade": "PSA8", | |
| 1321 | + "count": 1 | |
| 1322 | + }, | |
| 1323 | + { | |
| 1324 | + "month": "Jun, 2024", | |
| 1325 | + "grade": "Raw", | |
| 1326 | + "count": 1 | |
| 1327 | + }, | |
| 1328 | + { | |
| 1329 | + "month": "Jul, 2024", | |
| 1330 | + "grade": "Raw", | |
| 1331 | + "count": 4 | |
| 1332 | + }, | |
| 1333 | + { | |
| 1334 | + "month": "Sep, 2024", | |
| 1335 | + "grade": "PSA9", | |
| 1336 | + "count": 1 | |
| 1337 | + }, | |
| 1338 | + { | |
| 1339 | + "month": "Nov, 2024", | |
| 1340 | + "grade": "PSA10", | |
| 1341 | + "count": 1 | |
| 1342 | + } | |
| 1343 | + ] | |
| 1344 | + }, | |
| 1345 | + "fetchedAt": "2026-09-07T06:27:03.677Z" | |
| 1346 | + }, | |
| 1347 | + "expect": { | |
| 1348 | + "minCount": 1, | |
| 1349 | + "kinds": [ | |
| 1350 | + "catalog_item", | |
| 1351 | + "price_observation" | |
| 1352 | + ], | |
| 1353 | + "requiredFields": [ | |
| 1354 | + "attributes.set", | |
| 1355 | + "attributes.number" | |
| 1356 | + ] | |
| 1357 | + }, | |
| 1358 | + "note": "Live capture 2026-09-07 from https://www.pokemonprice.com/base-set-1st-edition/alakazam-1-holo-1st-edition", | |
| 1359 | + "capturedAt": "2026-09-07T06:27:03.691Z" | |
| 1360 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/pokemonprice/base-set-1st-edition-third.json
+915 −0
@@ -0,0 +1,915 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://www.pokemonprice.com/base-set-1st-edition/lightning-energy-100-1st-edition", | |
| 4 | + "externalId": "base-set-1st-edition/lightning-energy-100-1st-edition", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "slug": "lightning-energy-100-1st-edition", | |
| 10 | + "setSlug": "base-set-1st-edition", | |
| 11 | + "name": "Lightning Energy", | |
| 12 | + "tag": "1st edition", | |
| 13 | + "number": "100", | |
| 14 | + "total": "102", | |
| 15 | + "setName": "Base Set 1st Edition", | |
| 16 | + "image": "https://cdn.pokemonprice.com/cards/base1-100-lightning-energy.webp", | |
| 17 | + "rows": [ | |
| 18 | + { | |
| 19 | + "grade": "PSA10", | |
| 20 | + "fair_price": "102.00", | |
| 21 | + "low_price": "102.00", | |
| 22 | + "high_price": "102.00", | |
| 23 | + "confidence": "52.72", | |
| 24 | + "last_sale_date": "2024-08-25" | |
| 25 | + }, | |
| 26 | + { | |
| 27 | + "grade": "PSA6", | |
| 28 | + "fair_price": "8.00", | |
| 29 | + "low_price": "8.00", | |
| 30 | + "high_price": "8.00", | |
| 31 | + "confidence": "52.72", | |
| 32 | + "last_sale_date": "2020-04-30" | |
| 33 | + }, | |
| 34 | + { | |
| 35 | + "grade": "PSA7", | |
| 36 | + "fair_price": "10.50", | |
| 37 | + "low_price": "10.50", | |
| 38 | + "high_price": "10.50", | |
| 39 | + "confidence": "52.72", | |
| 40 | + "last_sale_date": "2024-06-24" | |
| 41 | + }, | |
| 42 | + { | |
| 43 | + "grade": "PSA8", | |
| 44 | + "fair_price": "15.99", | |
| 45 | + "low_price": "15.99", | |
| 46 | + "high_price": "15.99", | |
| 47 | + "confidence": "52.72", | |
| 48 | + "last_sale_date": "2024-03-31" | |
| 49 | + }, | |
| 50 | + { | |
| 51 | + "grade": "PSA9", | |
| 52 | + "fair_price": "22.50", | |
| 53 | + "low_price": "22.50", | |
| 54 | + "high_price": "22.50", | |
| 55 | + "confidence": "52.72", | |
| 56 | + "last_sale_date": "2024-10-17" | |
| 57 | + }, | |
| 58 | + { | |
| 59 | + "grade": "Raw", | |
| 60 | + "fair_price": "7.25", | |
| 61 | + "low_price": "7.25", | |
| 62 | + "high_price": "7.25", | |
| 63 | + "confidence": "52.72", | |
| 64 | + "last_sale_date": "2024-10-04" | |
| 65 | + } | |
| 66 | + ], | |
| 67 | + "transactions": [ | |
| 68 | + { | |
| 69 | + "month": "Sep, 2016", | |
| 70 | + "grade": "PSA10", | |
| 71 | + "count": 1 | |
| 72 | + }, | |
| 73 | + { | |
| 74 | + "month": "Sep, 2016", | |
| 75 | + "grade": "PSA9", | |
| 76 | + "count": 1 | |
| 77 | + }, | |
| 78 | + { | |
| 79 | + "month": "Oct, 2016", | |
| 80 | + "grade": "PSA9", | |
| 81 | + "count": 1 | |
| 82 | + }, | |
| 83 | + { | |
| 84 | + "month": "Dec, 2016", | |
| 85 | + "grade": "PSA9", | |
| 86 | + "count": 1 | |
| 87 | + }, | |
| 88 | + { | |
| 89 | + "month": "Jan, 2017", | |
| 90 | + "grade": "PSA10", | |
| 91 | + "count": 1 | |
| 92 | + }, | |
| 93 | + { | |
| 94 | + "month": "Feb, 2017", | |
| 95 | + "grade": "PSA10", | |
| 96 | + "count": 1 | |
| 97 | + }, | |
| 98 | + { | |
| 99 | + "month": "May, 2017", | |
| 100 | + "grade": "PSA10", | |
| 101 | + "count": 1 | |
| 102 | + }, | |
| 103 | + { | |
| 104 | + "month": "May, 2017", | |
| 105 | + "grade": "PSA9", | |
| 106 | + "count": 1 | |
| 107 | + }, | |
| 108 | + { | |
| 109 | + "month": "Jun, 2017", | |
| 110 | + "grade": "PSA10", | |
| 111 | + "count": 1 | |
| 112 | + }, | |
| 113 | + { | |
| 114 | + "month": "Jul, 2017", | |
| 115 | + "grade": "PSA9", | |
| 116 | + "count": 3 | |
| 117 | + }, | |
| 118 | + { | |
| 119 | + "month": "Jul, 2017", | |
| 120 | + "grade": "PSA10", | |
| 121 | + "count": 1 | |
| 122 | + }, | |
| 123 | + { | |
| 124 | + "month": "Sep, 2017", | |
| 125 | + "grade": "PSA9", | |
| 126 | + "count": 5 | |
| 127 | + }, | |
| 128 | + { | |
| 129 | + "month": "Sep, 2017", | |
| 130 | + "grade": "PSA10", | |
| 131 | + "count": 2 | |
| 132 | + }, | |
| 133 | + { | |
| 134 | + "month": "Oct, 2017", | |
| 135 | + "grade": "PSA9", | |
| 136 | + "count": 2 | |
| 137 | + }, | |
| 138 | + { | |
| 139 | + "month": "Oct, 2017", | |
| 140 | + "grade": "PSA8", | |
| 141 | + "count": 1 | |
| 142 | + }, | |
| 143 | + { | |
| 144 | + "month": "Oct, 2017", | |
| 145 | + "grade": "PSA10", | |
| 146 | + "count": 3 | |
| 147 | + }, | |
| 148 | + { | |
| 149 | + "month": "Nov, 2017", | |
| 150 | + "grade": "PSA10", | |
| 151 | + "count": 2 | |
| 152 | + }, | |
| 153 | + { | |
| 154 | + "month": "Nov, 2017", | |
| 155 | + "grade": "PSA9", | |
| 156 | + "count": 3 | |
| 157 | + }, | |
| 158 | + { | |
| 159 | + "month": "Dec, 2017", | |
| 160 | + "grade": "PSA10", | |
| 161 | + "count": 4 | |
| 162 | + }, | |
| 163 | + { | |
| 164 | + "month": "Dec, 2017", | |
| 165 | + "grade": "PSA9", | |
| 166 | + "count": 2 | |
| 167 | + }, | |
| 168 | + { | |
| 169 | + "month": "Jan, 2018", | |
| 170 | + "grade": "PSA9", | |
| 171 | + "count": 3 | |
| 172 | + }, | |
| 173 | + { | |
| 174 | + "month": "Jan, 2018", | |
| 175 | + "grade": "PSA10", | |
| 176 | + "count": 3 | |
| 177 | + }, | |
| 178 | + { | |
| 179 | + "month": "Feb, 2018", | |
| 180 | + "grade": "PSA10", | |
| 181 | + "count": 1 | |
| 182 | + }, | |
| 183 | + { | |
| 184 | + "month": "Mar, 2018", | |
| 185 | + "grade": "PSA10", | |
| 186 | + "count": 3 | |
| 187 | + }, | |
| 188 | + { | |
| 189 | + "month": "Mar, 2018", | |
| 190 | + "grade": "PSA9", | |
| 191 | + "count": 3 | |
| 192 | + }, | |
| 193 | + { | |
| 194 | + "month": "Apr, 2018", | |
| 195 | + "grade": "PSA10", | |
| 196 | + "count": 3 | |
| 197 | + }, | |
| 198 | + { | |
| 199 | + "month": "Apr, 2018", | |
| 200 | + "grade": "PSA9", | |
| 201 | + "count": 2 | |
| 202 | + }, | |
| 203 | + { | |
| 204 | + "month": "May, 2018", | |
| 205 | + "grade": "PSA10", | |
| 206 | + "count": 5 | |
| 207 | + }, | |
| 208 | + { | |
| 209 | + "month": "May, 2018", | |
| 210 | + "grade": "PSA9", | |
| 211 | + "count": 1 | |
| 212 | + }, | |
| 213 | + { | |
| 214 | + "month": "Jun, 2018", | |
| 215 | + "grade": "PSA9", | |
| 216 | + "count": 2 | |
| 217 | + }, | |
| 218 | + { | |
| 219 | + "month": "Jun, 2018", | |
| 220 | + "grade": "PSA10", | |
| 221 | + "count": 3 | |
| 222 | + }, | |
| 223 | + { | |
| 224 | + "month": "Jul, 2018", | |
| 225 | + "grade": "PSA10", | |
| 226 | + "count": 3 | |
| 227 | + }, | |
| 228 | + { | |
| 229 | + "month": "Jul, 2018", | |
| 230 | + "grade": "PSA9", | |
| 231 | + "count": 2 | |
| 232 | + }, | |
| 233 | + { | |
| 234 | + "month": "Aug, 2018", | |
| 235 | + "grade": "PSA9", | |
| 236 | + "count": 2 | |
| 237 | + }, | |
| 238 | + { | |
| 239 | + "month": "Aug, 2018", | |
| 240 | + "grade": "PSA10", | |
| 241 | + "count": 2 | |
| 242 | + }, | |
| 243 | + { | |
| 244 | + "month": "Sep, 2018", | |
| 245 | + "grade": "PSA10", | |
| 246 | + "count": 3 | |
| 247 | + }, | |
| 248 | + { | |
| 249 | + "month": "Sep, 2018", | |
| 250 | + "grade": "PSA8", | |
| 251 | + "count": 2 | |
| 252 | + }, | |
| 253 | + { | |
| 254 | + "month": "Sep, 2018", | |
| 255 | + "grade": "PSA9", | |
| 256 | + "count": 2 | |
| 257 | + }, | |
| 258 | + { | |
| 259 | + "month": "Nov, 2018", | |
| 260 | + "grade": "PSA8", | |
| 261 | + "count": 1 | |
| 262 | + }, | |
| 263 | + { | |
| 264 | + "month": "Dec, 2018", | |
| 265 | + "grade": "PSA9", | |
| 266 | + "count": 3 | |
| 267 | + }, | |
| 268 | + { | |
| 269 | + "month": "Dec, 2018", | |
| 270 | + "grade": "PSA8", | |
| 271 | + "count": 2 | |
| 272 | + }, | |
| 273 | + { | |
| 274 | + "month": "Dec, 2018", | |
| 275 | + "grade": "PSA10", | |
| 276 | + "count": 2 | |
| 277 | + }, | |
| 278 | + { | |
| 279 | + "month": "Jan, 2019", | |
| 280 | + "grade": "PSA8", | |
| 281 | + "count": 2 | |
| 282 | + }, | |
| 283 | + { | |
| 284 | + "month": "Jan, 2019", | |
| 285 | + "grade": "PSA10", | |
| 286 | + "count": 10 | |
| 287 | + }, | |
| 288 | + { | |
| 289 | + "month": "Jan, 2019", | |
| 290 | + "grade": "PSA7", | |
| 291 | + "count": 1 | |
| 292 | + }, | |
| 293 | + { | |
| 294 | + "month": "Jan, 2019", | |
| 295 | + "grade": "PSA9", | |
| 296 | + "count": 8 | |
| 297 | + }, | |
| 298 | + { | |
| 299 | + "month": "Feb, 2019", | |
| 300 | + "grade": "PSA9", | |
| 301 | + "count": 1 | |
| 302 | + }, | |
| 303 | + { | |
| 304 | + "month": "Feb, 2019", | |
| 305 | + "grade": "PSA10", | |
| 306 | + "count": 1 | |
| 307 | + }, | |
| 308 | + { | |
| 309 | + "month": "Mar, 2019", | |
| 310 | + "grade": "PSA10", | |
| 311 | + "count": 2 | |
| 312 | + }, | |
| 313 | + { | |
| 314 | + "month": "Mar, 2019", | |
| 315 | + "grade": "PSA9", | |
| 316 | + "count": 6 | |
| 317 | + }, | |
| 318 | + { | |
| 319 | + "month": "Apr, 2019", | |
| 320 | + "grade": "PSA9", | |
| 321 | + "count": 3 | |
| 322 | + }, | |
| 323 | + { | |
| 324 | + "month": "Apr, 2019", | |
| 325 | + "grade": "PSA10", | |
| 326 | + "count": 3 | |
| 327 | + }, | |
| 328 | + { | |
| 329 | + "month": "Jun, 2019", | |
| 330 | + "grade": "PSA9", | |
| 331 | + "count": 2 | |
| 332 | + }, | |
| 333 | + { | |
| 334 | + "month": "Jul, 2019", | |
| 335 | + "grade": "PSA9", | |
| 336 | + "count": 4 | |
| 337 | + }, | |
| 338 | + { | |
| 339 | + "month": "Jul, 2019", | |
| 340 | + "grade": "PSA8", | |
| 341 | + "count": 1 | |
| 342 | + }, | |
| 343 | + { | |
| 344 | + "month": "Jul, 2019", | |
| 345 | + "grade": "PSA10", | |
| 346 | + "count": 4 | |
| 347 | + }, | |
| 348 | + { | |
| 349 | + "month": "Aug, 2019", | |
| 350 | + "grade": "PSA9", | |
| 351 | + "count": 2 | |
| 352 | + }, | |
| 353 | + { | |
| 354 | + "month": "Aug, 2019", | |
| 355 | + "grade": "PSA10", | |
| 356 | + "count": 1 | |
| 357 | + }, | |
| 358 | + { | |
| 359 | + "month": "Sep, 2019", | |
| 360 | + "grade": "PSA9", | |
| 361 | + "count": 1 | |
| 362 | + }, | |
| 363 | + { | |
| 364 | + "month": "Sep, 2019", | |
| 365 | + "grade": "PSA10", | |
| 366 | + "count": 1 | |
| 367 | + }, | |
| 368 | + { | |
| 369 | + "month": "Nov, 2019", | |
| 370 | + "grade": "PSA10", | |
| 371 | + "count": 3 | |
| 372 | + }, | |
| 373 | + { | |
| 374 | + "month": "Dec, 2019", | |
| 375 | + "grade": "PSA10", | |
| 376 | + "count": 1 | |
| 377 | + }, | |
| 378 | + { | |
| 379 | + "month": "Jan, 2020", | |
| 380 | + "grade": "PSA8", | |
| 381 | + "count": 1 | |
| 382 | + }, | |
| 383 | + { | |
| 384 | + "month": "Jan, 2020", | |
| 385 | + "grade": "PSA9", | |
| 386 | + "count": 2 | |
| 387 | + }, | |
| 388 | + { | |
| 389 | + "month": "Feb, 2020", | |
| 390 | + "grade": "PSA9", | |
| 391 | + "count": 1 | |
| 392 | + }, | |
| 393 | + { | |
| 394 | + "month": "Feb, 2020", | |
| 395 | + "grade": "PSA10", | |
| 396 | + "count": 1 | |
| 397 | + }, | |
| 398 | + { | |
| 399 | + "month": "Feb, 2020", | |
| 400 | + "grade": "PSA8", | |
| 401 | + "count": 1 | |
| 402 | + }, | |
| 403 | + { | |
| 404 | + "month": "Mar, 2020", | |
| 405 | + "grade": "PSA10", | |
| 406 | + "count": 2 | |
| 407 | + }, | |
| 408 | + { | |
| 409 | + "month": "Apr, 2020", | |
| 410 | + "grade": "PSA9", | |
| 411 | + "count": 5 | |
| 412 | + }, | |
| 413 | + { | |
| 414 | + "month": "Apr, 2020", | |
| 415 | + "grade": "PSA6", | |
| 416 | + "count": 1 | |
| 417 | + }, | |
| 418 | + { | |
| 419 | + "month": "May, 2020", | |
| 420 | + "grade": "PSA10", | |
| 421 | + "count": 6 | |
| 422 | + }, | |
| 423 | + { | |
| 424 | + "month": "May, 2020", | |
| 425 | + "grade": "PSA9", | |
| 426 | + "count": 5 | |
| 427 | + }, | |
| 428 | + { | |
| 429 | + "month": "May, 2020", | |
| 430 | + "grade": "PSA8", | |
| 431 | + "count": 1 | |
| 432 | + }, | |
| 433 | + { | |
| 434 | + "month": "May, 2020", | |
| 435 | + "grade": "PSA7", | |
| 436 | + "count": 1 | |
| 437 | + }, | |
| 438 | + { | |
| 439 | + "month": "Jun, 2020", | |
| 440 | + "grade": "PSA10", | |
| 441 | + "count": 4 | |
| 442 | + }, | |
| 443 | + { | |
| 444 | + "month": "Jun, 2020", | |
| 445 | + "grade": "PSA9", | |
| 446 | + "count": 9 | |
| 447 | + }, | |
| 448 | + { | |
| 449 | + "month": "Jul, 2020", | |
| 450 | + "grade": "PSA9", | |
| 451 | + "count": 10 | |
| 452 | + }, | |
| 453 | + { | |
| 454 | + "month": "Jul, 2020", | |
| 455 | + "grade": "PSA10", | |
| 456 | + "count": 4 | |
| 457 | + }, | |
| 458 | + { | |
| 459 | + "month": "Jul, 2020", | |
| 460 | + "grade": "PSA8", | |
| 461 | + "count": 1 | |
| 462 | + }, | |
| 463 | + { | |
| 464 | + "month": "Aug, 2020", | |
| 465 | + "grade": "PSA10", | |
| 466 | + "count": 1 | |
| 467 | + }, | |
| 468 | + { | |
| 469 | + "month": "Aug, 2020", | |
| 470 | + "grade": "PSA8", | |
| 471 | + "count": 1 | |
| 472 | + }, | |
| 473 | + { | |
| 474 | + "month": "Aug, 2020", | |
| 475 | + "grade": "PSA9", | |
| 476 | + "count": 5 | |
| 477 | + }, | |
| 478 | + { | |
| 479 | + "month": "Sep, 2020", | |
| 480 | + "grade": "PSA9", | |
| 481 | + "count": 2 | |
| 482 | + }, | |
| 483 | + { | |
| 484 | + "month": "Sep, 2020", | |
| 485 | + "grade": "PSA10", | |
| 486 | + "count": 2 | |
| 487 | + }, | |
| 488 | + { | |
| 489 | + "month": "Oct, 2020", | |
| 490 | + "grade": "PSA9", | |
| 491 | + "count": 2 | |
| 492 | + }, | |
| 493 | + { | |
| 494 | + "month": "Oct, 2020", | |
| 495 | + "grade": "PSA10", | |
| 496 | + "count": 4 | |
| 497 | + }, | |
| 498 | + { | |
| 499 | + "month": "Oct, 2020", | |
| 500 | + "grade": "PSA8", | |
| 501 | + "count": 1 | |
| 502 | + }, | |
| 503 | + { | |
| 504 | + "month": "Nov, 2020", | |
| 505 | + "grade": "PSA9", | |
| 506 | + "count": 4 | |
| 507 | + }, | |
| 508 | + { | |
| 509 | + "month": "Nov, 2020", | |
| 510 | + "grade": "PSA10", | |
| 511 | + "count": 7 | |
| 512 | + }, | |
| 513 | + { | |
| 514 | + "month": "Dec, 2020", | |
| 515 | + "grade": "PSA9", | |
| 516 | + "count": 6 | |
| 517 | + }, | |
| 518 | + { | |
| 519 | + "month": "Dec, 2020", | |
| 520 | + "grade": "PSA10", | |
| 521 | + "count": 4 | |
| 522 | + }, | |
| 523 | + { | |
| 524 | + "month": "Dec, 2020", | |
| 525 | + "grade": "PSA8", | |
| 526 | + "count": 1 | |
| 527 | + }, | |
| 528 | + { | |
| 529 | + "month": "Jan, 2021", | |
| 530 | + "grade": "PSA10", | |
| 531 | + "count": 3 | |
| 532 | + }, | |
| 533 | + { | |
| 534 | + "month": "Jan, 2021", | |
| 535 | + "grade": "PSA7", | |
| 536 | + "count": 1 | |
| 537 | + }, | |
| 538 | + { | |
| 539 | + "month": "Jan, 2021", | |
| 540 | + "grade": "PSA9", | |
| 541 | + "count": 1 | |
| 542 | + }, | |
| 543 | + { | |
| 544 | + "month": "Feb, 2021", | |
| 545 | + "grade": "PSA10", | |
| 546 | + "count": 8 | |
| 547 | + }, | |
| 548 | + { | |
| 549 | + "month": "Feb, 2021", | |
| 550 | + "grade": "PSA8", | |
| 551 | + "count": 1 | |
| 552 | + }, | |
| 553 | + { | |
| 554 | + "month": "Feb, 2021", | |
| 555 | + "grade": "PSA9", | |
| 556 | + "count": 2 | |
| 557 | + }, | |
| 558 | + { | |
| 559 | + "month": "Mar, 2021", | |
| 560 | + "grade": "PSA9", | |
| 561 | + "count": 2 | |
| 562 | + }, | |
| 563 | + { | |
| 564 | + "month": "Mar, 2021", | |
| 565 | + "grade": "PSA10", | |
| 566 | + "count": 1 | |
| 567 | + }, | |
| 568 | + { | |
| 569 | + "month": "Apr, 2021", | |
| 570 | + "grade": "PSA9", | |
| 571 | + "count": 1 | |
| 572 | + }, | |
| 573 | + { | |
| 574 | + "month": "Apr, 2021", | |
| 575 | + "grade": "PSA10", | |
| 576 | + "count": 1 | |
| 577 | + }, | |
| 578 | + { | |
| 579 | + "month": "May, 2021", | |
| 580 | + "grade": "PSA9", | |
| 581 | + "count": 5 | |
| 582 | + }, | |
| 583 | + { | |
| 584 | + "month": "May, 2021", | |
| 585 | + "grade": "PSA10", | |
| 586 | + "count": 3 | |
| 587 | + }, | |
| 588 | + { | |
| 589 | + "month": "Jun, 2021", | |
| 590 | + "grade": "PSA9", | |
| 591 | + "count": 2 | |
| 592 | + }, | |
| 593 | + { | |
| 594 | + "month": "Jul, 2021", | |
| 595 | + "grade": "PSA10", | |
| 596 | + "count": 2 | |
| 597 | + }, | |
| 598 | + { | |
| 599 | + "month": "Jul, 2021", | |
| 600 | + "grade": "PSA8", | |
| 601 | + "count": 1 | |
| 602 | + }, | |
| 603 | + { | |
| 604 | + "month": "Jul, 2021", | |
| 605 | + "grade": "PSA9", | |
| 606 | + "count": 3 | |
| 607 | + }, | |
| 608 | + { | |
| 609 | + "month": "Aug, 2021", | |
| 610 | + "grade": "PSA10", | |
| 611 | + "count": 2 | |
| 612 | + }, | |
| 613 | + { | |
| 614 | + "month": "Aug, 2021", | |
| 615 | + "grade": "PSA9", | |
| 616 | + "count": 1 | |
| 617 | + }, | |
| 618 | + { | |
| 619 | + "month": "Sep, 2021", | |
| 620 | + "grade": "PSA8", | |
| 621 | + "count": 1 | |
| 622 | + }, | |
| 623 | + { | |
| 624 | + "month": "Sep, 2021", | |
| 625 | + "grade": "PSA7", | |
| 626 | + "count": 1 | |
| 627 | + }, | |
| 628 | + { | |
| 629 | + "month": "Sep, 2021", | |
| 630 | + "grade": "PSA9", | |
| 631 | + "count": 3 | |
| 632 | + }, | |
| 633 | + { | |
| 634 | + "month": "Sep, 2021", | |
| 635 | + "grade": "PSA10", | |
| 636 | + "count": 1 | |
| 637 | + }, | |
| 638 | + { | |
| 639 | + "month": "Oct, 2021", | |
| 640 | + "grade": "PSA10", | |
| 641 | + "count": 1 | |
| 642 | + }, | |
| 643 | + { | |
| 644 | + "month": "Oct, 2021", | |
| 645 | + "grade": "PSA9", | |
| 646 | + "count": 6 | |
| 647 | + }, | |
| 648 | + { | |
| 649 | + "month": "Oct, 2021", | |
| 650 | + "grade": "PSA8", | |
| 651 | + "count": 2 | |
| 652 | + }, | |
| 653 | + { | |
| 654 | + "month": "Nov, 2021", | |
| 655 | + "grade": "PSA8", | |
| 656 | + "count": 1 | |
| 657 | + }, | |
| 658 | + { | |
| 659 | + "month": "Nov, 2021", | |
| 660 | + "grade": "PSA10", | |
| 661 | + "count": 2 | |
| 662 | + }, | |
| 663 | + { | |
| 664 | + "month": "Dec, 2021", | |
| 665 | + "grade": "PSA10", | |
| 666 | + "count": 7 | |
| 667 | + }, | |
| 668 | + { | |
| 669 | + "month": "Dec, 2021", | |
| 670 | + "grade": "PSA8", | |
| 671 | + "count": 3 | |
| 672 | + }, | |
| 673 | + { | |
| 674 | + "month": "Dec, 2021", | |
| 675 | + "grade": "PSA9", | |
| 676 | + "count": 7 | |
| 677 | + }, | |
| 678 | + { | |
| 679 | + "month": "Jan, 2022", | |
| 680 | + "grade": "PSA9", | |
| 681 | + "count": 6 | |
| 682 | + }, | |
| 683 | + { | |
| 684 | + "month": "Jan, 2022", | |
| 685 | + "grade": "PSA8", | |
| 686 | + "count": 1 | |
| 687 | + }, | |
| 688 | + { | |
| 689 | + "month": "Mar, 2022", | |
| 690 | + "grade": "PSA10", | |
| 691 | + "count": 1 | |
| 692 | + }, | |
| 693 | + { | |
| 694 | + "month": "May, 2022", | |
| 695 | + "grade": "PSA8", | |
| 696 | + "count": 2 | |
| 697 | + }, | |
| 698 | + { | |
| 699 | + "month": "May, 2022", | |
| 700 | + "grade": "PSA9", | |
| 701 | + "count": 1 | |
| 702 | + }, | |
| 703 | + { | |
| 704 | + "month": "Sep, 2022", | |
| 705 | + "grade": "PSA9", | |
| 706 | + "count": 1 | |
| 707 | + }, | |
| 708 | + { | |
| 709 | + "month": "Sep, 2022", | |
| 710 | + "grade": "PSA10", | |
| 711 | + "count": 2 | |
| 712 | + }, | |
| 713 | + { | |
| 714 | + "month": "Sep, 2022", | |
| 715 | + "grade": "PSA8", | |
| 716 | + "count": 1 | |
| 717 | + }, | |
| 718 | + { | |
| 719 | + "month": "Oct, 2022", | |
| 720 | + "grade": "PSA10", | |
| 721 | + "count": 4 | |
| 722 | + }, | |
| 723 | + { | |
| 724 | + "month": "Oct, 2022", | |
| 725 | + "grade": "PSA9", | |
| 726 | + "count": 1 | |
| 727 | + }, | |
| 728 | + { | |
| 729 | + "month": "Dec, 2022", | |
| 730 | + "grade": "PSA8", | |
| 731 | + "count": 1 | |
| 732 | + }, | |
| 733 | + { | |
| 734 | + "month": "Dec, 2022", | |
| 735 | + "grade": "PSA9", | |
| 736 | + "count": 2 | |
| 737 | + }, | |
| 738 | + { | |
| 739 | + "month": "Dec, 2022", | |
| 740 | + "grade": "PSA10", | |
| 741 | + "count": 1 | |
| 742 | + }, | |
| 743 | + { | |
| 744 | + "month": "Jan, 2023", | |
| 745 | + "grade": "PSA8", | |
| 746 | + "count": 1 | |
| 747 | + }, | |
| 748 | + { | |
| 749 | + "month": "Jan, 2023", | |
| 750 | + "grade": "PSA9", | |
| 751 | + "count": 4 | |
| 752 | + }, | |
| 753 | + { | |
| 754 | + "month": "Jan, 2023", | |
| 755 | + "grade": "PSA10", | |
| 756 | + "count": 1 | |
| 757 | + }, | |
| 758 | + { | |
| 759 | + "month": "Feb, 2023", | |
| 760 | + "grade": "PSA10", | |
| 761 | + "count": 3 | |
| 762 | + }, | |
| 763 | + { | |
| 764 | + "month": "Feb, 2023", | |
| 765 | + "grade": "PSA9", | |
| 766 | + "count": 6 | |
| 767 | + }, | |
| 768 | + { | |
| 769 | + "month": "Mar, 2023", | |
| 770 | + "grade": "PSA9", | |
| 771 | + "count": 6 | |
| 772 | + }, | |
| 773 | + { | |
| 774 | + "month": "Mar, 2023", | |
| 775 | + "grade": "PSA10", | |
| 776 | + "count": 4 | |
| 777 | + }, | |
| 778 | + { | |
| 779 | + "month": "Mar, 2023", | |
| 780 | + "grade": "PSA8", | |
| 781 | + "count": 2 | |
| 782 | + }, | |
| 783 | + { | |
| 784 | + "month": "Mar, 2023", | |
| 785 | + "grade": "PSA7", | |
| 786 | + "count": 4 | |
| 787 | + }, | |
| 788 | + { | |
| 789 | + "month": "Jul, 2023", | |
| 790 | + "grade": "PSA10", | |
| 791 | + "count": 1 | |
| 792 | + }, | |
| 793 | + { | |
| 794 | + "month": "Aug, 2023", | |
| 795 | + "grade": "PSA9", | |
| 796 | + "count": 1 | |
| 797 | + }, | |
| 798 | + { | |
| 799 | + "month": "Sep, 2023", | |
| 800 | + "grade": "PSA7", | |
| 801 | + "count": 1 | |
| 802 | + }, | |
| 803 | + { | |
| 804 | + "month": "Sep, 2023", | |
| 805 | + "grade": "PSA8", | |
| 806 | + "count": 1 | |
| 807 | + }, | |
| 808 | + { | |
| 809 | + "month": "Oct, 2023", | |
| 810 | + "grade": "PSA9", | |
| 811 | + "count": 2 | |
| 812 | + }, | |
| 813 | + { | |
| 814 | + "month": "Dec, 2023", | |
| 815 | + "grade": "Raw", | |
| 816 | + "count": 1 | |
| 817 | + }, | |
| 818 | + { | |
| 819 | + "month": "Dec, 2023", | |
| 820 | + "grade": "PSA10", | |
| 821 | + "count": 1 | |
| 822 | + }, | |
| 823 | + { | |
| 824 | + "month": "Dec, 2023", | |
| 825 | + "grade": "PSA8", | |
| 826 | + "count": 1 | |
| 827 | + }, | |
| 828 | + { | |
| 829 | + "month": "Jan, 2024", | |
| 830 | + "grade": "PSA9", | |
| 831 | + "count": 2 | |
| 832 | + }, | |
| 833 | + { | |
| 834 | + "month": "Mar, 2024", | |
| 835 | + "grade": "Raw", | |
| 836 | + "count": 5 | |
| 837 | + }, | |
| 838 | + { | |
| 839 | + "month": "Mar, 2024", | |
| 840 | + "grade": "PSA8", | |
| 841 | + "count": 1 | |
| 842 | + }, | |
| 843 | + { | |
| 844 | + "month": "Apr, 2024", | |
| 845 | + "grade": "Raw", | |
| 846 | + "count": 3 | |
| 847 | + }, | |
| 848 | + { | |
| 849 | + "month": "Apr, 2024", | |
| 850 | + "grade": "PSA10", | |
| 851 | + "count": 1 | |
| 852 | + }, | |
| 853 | + { | |
| 854 | + "month": "May, 2024", | |
| 855 | + "grade": "Raw", | |
| 856 | + "count": 1 | |
| 857 | + }, | |
| 858 | + { | |
| 859 | + "month": "Jun, 2024", | |
| 860 | + "grade": "PSA9", | |
| 861 | + "count": 1 | |
| 862 | + }, | |
| 863 | + { | |
| 864 | + "month": "Jun, 2024", | |
| 865 | + "grade": "PSA7", | |
| 866 | + "count": 1 | |
| 867 | + }, | |
| 868 | + { | |
| 869 | + "month": "Aug, 2024", | |
| 870 | + "grade": "Raw", | |
| 871 | + "count": 1 | |
| 872 | + }, | |
| 873 | + { | |
| 874 | + "month": "Aug, 2024", | |
| 875 | + "grade": "PSA10", | |
| 876 | + "count": 1 | |
| 877 | + }, | |
| 878 | + { | |
| 879 | + "month": "Sep, 2024", | |
| 880 | + "grade": "Raw", | |
| 881 | + "count": 3 | |
| 882 | + }, | |
| 883 | + { | |
| 884 | + "month": "Sep, 2024", | |
| 885 | + "grade": "PSA9", | |
| 886 | + "count": 1 | |
| 887 | + }, | |
| 888 | + { | |
| 889 | + "month": "Oct, 2024", | |
| 890 | + "grade": "Raw", | |
| 891 | + "count": 1 | |
| 892 | + }, | |
| 893 | + { | |
| 894 | + "month": "Oct, 2024", | |
| 895 | + "grade": "PSA9", | |
| 896 | + "count": 1 | |
| 897 | + } | |
| 898 | + ] | |
| 899 | + }, | |
| 900 | + "fetchedAt": "2026-09-07T06:27:06.261Z" | |
| 901 | + }, | |
| 902 | + "expect": { | |
| 903 | + "minCount": 1, | |
| 904 | + "kinds": [ | |
| 905 | + "catalog_item", | |
| 906 | + "price_observation" | |
| 907 | + ], | |
| 908 | + "requiredFields": [ | |
| 909 | + "attributes.set", | |
| 910 | + "attributes.number" | |
| 911 | + ] | |
| 912 | + }, | |
| 913 | + "note": "Live capture 2026-09-07 from https://www.pokemonprice.com/base-set-1st-edition/lightning-energy-100-1st-edition", | |
| 914 | + "capturedAt": "2026-09-07T06:27:06.264Z" | |
| 915 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/sorcery-tcg/first.json
+78 −0
@@ -0,0 +1,78 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://curiosa.io/cards/13_treasures_of_britain", | |
| 4 | + "externalId": "cmt865vva04mqnwcyrl8c2h8l", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "card": { | |
| 10 | + "id": "cmt865vva04mqnwcyrl8c2h8l", | |
| 11 | + "name": "13 Treasures of Britain", | |
| 12 | + "slug": "13_treasures_of_britain", | |
| 13 | + "engine": { | |
| 14 | + "type": "Artifact", | |
| 15 | + "category": "Spell", | |
| 16 | + "rarity": "Unique", | |
| 17 | + "elements": [ | |
| 18 | + "None" | |
| 19 | + ], | |
| 20 | + "subtypes": [ | |
| 21 | + "Relic" | |
| 22 | + ] | |
| 23 | + }, | |
| 24 | + "printings": [ | |
| 25 | + { | |
| 26 | + "id": "cmt865x7l04ninwcyyju3668s", | |
| 27 | + "slug": "004-13_treasures_of_britain-b-s", | |
| 28 | + "printedAt": "2024-10-04T07:00:00.000Z", | |
| 29 | + "set": { | |
| 30 | + "name": "Arthurian Legends", | |
| 31 | + "code": "004", | |
| 32 | + "releasedAt": "2026-08-25T04:32:18.603Z" | |
| 33 | + }, | |
| 34 | + "meta": { | |
| 35 | + "finish": "Standard", | |
| 36 | + "product": "Booster", | |
| 37 | + "artist": { | |
| 38 | + "name": "Jeff A. Menges" | |
| 39 | + }, | |
| 40 | + "typeline": "A Unique collection of priceless Relics" | |
| 41 | + } | |
| 42 | + }, | |
| 43 | + { | |
| 44 | + "id": "cmt865wcd04n1nwcy6l8uye2o", | |
| 45 | + "slug": "004-13_treasures_of_britain-b-f", | |
| 46 | + "printedAt": "2024-10-04T07:00:00.000Z", | |
| 47 | + "set": { | |
| 48 | + "name": "Arthurian Legends", | |
| 49 | + "code": "004", | |
| 50 | + "releasedAt": "2026-08-25T04:32:18.603Z" | |
| 51 | + }, | |
| 52 | + "meta": { | |
| 53 | + "finish": "Foil", | |
| 54 | + "product": "Booster", | |
| 55 | + "artist": { | |
| 56 | + "name": "Jeff A. Menges" | |
| 57 | + }, | |
| 58 | + "typeline": "A Unique collection of priceless Relics" | |
| 59 | + } | |
| 60 | + } | |
| 61 | + ] | |
| 62 | + } | |
| 63 | + }, | |
| 64 | + "fetchedAt": "2026-09-07T06:26:56.600Z" | |
| 65 | + }, | |
| 66 | + "expect": { | |
| 67 | + "minCount": 1, | |
| 68 | + "kinds": [ | |
| 69 | + "catalog_item" | |
| 70 | + ], | |
| 71 | + "requiredFields": [ | |
| 72 | + "attributes.identifiers.sorcery_printing_id", | |
| 73 | + "attributes.set" | |
| 74 | + ] | |
| 75 | + }, | |
| 76 | + "note": "Live capture 2026-09-07 from https://curiosa.io/cards/13_treasures_of_britain", | |
| 77 | + "capturedAt": "2026-09-07T06:26:56.603Z" | |
| 78 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/sorcery-tcg/sixth.json
+114 −0
@@ -0,0 +1,114 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://curiosa.io/cards/accursed_albatross", | |
| 4 | + "externalId": "cmt85x1nl0010nwcyghalcg62", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "card": { | |
| 10 | + "id": "cmt85x1nl0010nwcyghalcg62", | |
| 11 | + "name": "Accursed Albatross", | |
| 12 | + "slug": "accursed_albatross", | |
| 13 | + "engine": { | |
| 14 | + "type": "Minion", | |
| 15 | + "category": "Spell", | |
| 16 | + "rarity": "Exceptional", | |
| 17 | + "elements": [ | |
| 18 | + "Water" | |
| 19 | + ], | |
| 20 | + "subtypes": [ | |
| 21 | + "Beast" | |
| 22 | + ] | |
| 23 | + }, | |
| 24 | + "printings": [ | |
| 25 | + { | |
| 26 | + "id": "cmt8613ul02a2nwcykgiqva2v", | |
| 27 | + "slug": "002-accursed_albatross-b-s", | |
| 28 | + "printedAt": "2023-10-06T07:00:00.000Z", | |
| 29 | + "set": { | |
| 30 | + "name": "Beta", | |
| 31 | + "code": "002", | |
| 32 | + "releasedAt": "2026-08-25T04:29:12.082Z" | |
| 33 | + }, | |
| 34 | + "meta": { | |
| 35 | + "finish": "Standard", | |
| 36 | + "product": "Booster", | |
| 37 | + "artist": { | |
| 38 | + "name": "Vincent Pompetti" | |
| 39 | + }, | |
| 40 | + "typeline": "An Exceptional Beast of deadly portent" | |
| 41 | + } | |
| 42 | + }, | |
| 43 | + { | |
| 44 | + "id": "cmt8612yp029nnwcy7j41qo56", | |
| 45 | + "slug": "002-accursed_albatross-b-f", | |
| 46 | + "printedAt": "2023-10-06T07:00:00.000Z", | |
| 47 | + "set": { | |
| 48 | + "name": "Beta", | |
| 49 | + "code": "002", | |
| 50 | + "releasedAt": "2026-08-25T04:29:12.082Z" | |
| 51 | + }, | |
| 52 | + "meta": { | |
| 53 | + "finish": "Foil", | |
| 54 | + "product": "Booster", | |
| 55 | + "artist": { | |
| 56 | + "name": "Vincent Pompetti" | |
| 57 | + }, | |
| 58 | + "typeline": "An Exceptional Beast of deadly portent" | |
| 59 | + } | |
| 60 | + }, | |
| 61 | + { | |
| 62 | + "id": "cmt85x2yc001tnwcyn5568x55", | |
| 63 | + "slug": "001-accursed_albatross-b-s", | |
| 64 | + "printedAt": "2023-06-22T07:00:00.000Z", | |
| 65 | + "set": { | |
| 66 | + "name": "Alpha", | |
| 67 | + "code": "001", | |
| 68 | + "releasedAt": "2026-08-25T04:21:27.405Z" | |
| 69 | + }, | |
| 70 | + "meta": { | |
| 71 | + "finish": "Standard", | |
| 72 | + "product": "Booster", | |
| 73 | + "artist": { | |
| 74 | + "name": "Vincent Pompetti" | |
| 75 | + }, | |
| 76 | + "typeline": "An Exceptional Beast of deadly portent" | |
| 77 | + } | |
| 78 | + }, | |
| 79 | + { | |
| 80 | + "id": "cmt85x237001anwcy399fyuld", | |
| 81 | + "slug": "001-accursed_albatross-b-f", | |
| 82 | + "printedAt": "2023-06-22T07:00:00.000Z", | |
| 83 | + "set": { | |
| 84 | + "name": "Alpha", | |
| 85 | + "code": "001", | |
| 86 | + "releasedAt": "2026-08-25T04:21:27.405Z" | |
| 87 | + }, | |
| 88 | + "meta": { | |
| 89 | + "finish": "Foil", | |
| 90 | + "product": "Booster", | |
| 91 | + "artist": { | |
| 92 | + "name": "Vincent Pompetti" | |
| 93 | + }, | |
| 94 | + "typeline": "An Exceptional Beast of deadly portent" | |
| 95 | + } | |
| 96 | + } | |
| 97 | + ] | |
| 98 | + } | |
| 99 | + }, | |
| 100 | + "fetchedAt": "2026-09-07T06:26:56.600Z" | |
| 101 | + }, | |
| 102 | + "expect": { | |
| 103 | + "minCount": 1, | |
| 104 | + "kinds": [ | |
| 105 | + "catalog_item" | |
| 106 | + ], | |
| 107 | + "requiredFields": [ | |
| 108 | + "attributes.identifiers.sorcery_printing_id", | |
| 109 | + "attributes.set" | |
| 110 | + ] | |
| 111 | + }, | |
| 112 | + "note": "Live capture 2026-09-07 from https://curiosa.io/cards/accursed_albatross", | |
| 113 | + "capturedAt": "2026-09-07T06:26:56.604Z" | |
| 114 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/swu-db/sor-first.json
+48 −0
@@ -0,0 +1,48 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://www.swu-db.com/card/SOR/059", | |
| 4 | + "externalId": "SOR-059", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "card": { | |
| 10 | + "Set": "SOR", | |
| 11 | + "Number": "059", | |
| 12 | + "Name": "2-1B Surgical Droid", | |
| 13 | + "Type": "Unit", | |
| 14 | + "Rarity": "Common", | |
| 15 | + "VariantType": "Normal", | |
| 16 | + "Unique": false, | |
| 17 | + "Artist": "Hoan Nguyen", | |
| 18 | + "cid": "5449704164", | |
| 19 | + "tcgplayerId": "540180", | |
| 20 | + "MarketPrice": "0.05", | |
| 21 | + "LowPrice": "0.01", | |
| 22 | + "FoilPrice": "0.10", | |
| 23 | + "LowFoilPrice": "0.05", | |
| 24 | + "FrontArt": "https://cdn.swu-db.com/images/cards/SOR/059.png", | |
| 25 | + "Aspects": [ | |
| 26 | + { | |
| 27 | + "S": "Vigilance" | |
| 28 | + } | |
| 29 | + ] | |
| 30 | + }, | |
| 31 | + "setName": "Spark of Rebellion" | |
| 32 | + }, | |
| 33 | + "fetchedAt": "2026-09-07T06:26:53.964Z" | |
| 34 | + }, | |
| 35 | + "expect": { | |
| 36 | + "minCount": 1, | |
| 37 | + "kinds": [ | |
| 38 | + "catalog_item", | |
| 39 | + "price_observation" | |
| 40 | + ], | |
| 41 | + "requiredFields": [ | |
| 42 | + "attributes.setCode", | |
| 43 | + "attributes.number" | |
| 44 | + ] | |
| 45 | + }, | |
| 46 | + "note": "Live capture 2026-09-07 from https://www.swu-db.com/card/SOR/059", | |
| 47 | + "capturedAt": "2026-09-07T06:26:53.965Z" | |
| 48 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/swu-db/sor-fourth.json
+47 −0
@@ -0,0 +1,47 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://www.swu-db.com/card/SOR/324F", | |
| 4 | + "externalId": "SOR-324F", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "card": { | |
| 10 | + "Set": "SOR", | |
| 11 | + "Number": "324F", | |
| 12 | + "Name": "2-1B Surgical Droid", | |
| 13 | + "Type": "Unit", | |
| 14 | + "Rarity": "Common", | |
| 15 | + "VariantType": "Hyperspace Foil", | |
| 16 | + "Unique": false, | |
| 17 | + "Artist": "Hoan Nguyen", | |
| 18 | + "tcgplayerId": "540446", | |
| 19 | + "MarketPrice": "0.23", | |
| 20 | + "LowPrice": "0.01", | |
| 21 | + "FoilPrice": "0.28", | |
| 22 | + "LowFoilPrice": "0.20", | |
| 23 | + "FrontArt": "https://cdn.swu-db.com/images/cards/SOR/324F.png", | |
| 24 | + "Aspects": [ | |
| 25 | + { | |
| 26 | + "S": "Vigilance" | |
| 27 | + } | |
| 28 | + ] | |
| 29 | + }, | |
| 30 | + "setName": "Spark of Rebellion" | |
| 31 | + }, | |
| 32 | + "fetchedAt": "2026-09-07T06:26:53.964Z" | |
| 33 | + }, | |
| 34 | + "expect": { | |
| 35 | + "minCount": 1, | |
| 36 | + "kinds": [ | |
| 37 | + "catalog_item", | |
| 38 | + "price_observation" | |
| 39 | + ], | |
| 40 | + "requiredFields": [ | |
| 41 | + "attributes.setCode", | |
| 42 | + "attributes.number" | |
| 43 | + ] | |
| 44 | + }, | |
| 45 | + "note": "Live capture 2026-09-07 from https://www.swu-db.com/card/SOR/324F", | |
| 46 | + "capturedAt": "2026-09-07T06:26:53.966Z" | |
| 47 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/tcgcsv/pokemon-base-charizard.json
+112 −0
@@ -0,0 +1,112 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://www.tcgplayer.com/product/42382/pokemon-base-set-charizard", | |
| 4 | + "externalId": "42382", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "fetchedAt": "2026-09-07T06:38:25.908Z", | |
| 8 | + "payload": { | |
| 9 | + "categoryId": 3, | |
| 10 | + "category": { | |
| 11 | + "slug": "pokemon", | |
| 12 | + "franchise": "Pokémon", | |
| 13 | + "brand": "The Pokémon Company", | |
| 14 | + "language": "English" | |
| 15 | + }, | |
| 16 | + "group": { | |
| 17 | + "groupId": 604, | |
| 18 | + "name": "Base Set", | |
| 19 | + "abbreviation": "BS", | |
| 20 | + "isSupplemental": false, | |
| 21 | + "publishedOn": "1999-01-09T00:00:00", | |
| 22 | + "categoryId": 3 | |
| 23 | + }, | |
| 24 | + "product": { | |
| 25 | + "productId": 42382, | |
| 26 | + "name": "Charizard", | |
| 27 | + "cleanName": "Charizard", | |
| 28 | + "imageUrl": "https://tcgplayer-cdn.tcgplayer.com/product/42382_200w.jpg", | |
| 29 | + "url": "https://www.tcgplayer.com/product/42382/pokemon-base-set-charizard", | |
| 30 | + "modifiedOn": "2026-06-30T22:37:51.91", | |
| 31 | + "extendedData": [ | |
| 32 | + { | |
| 33 | + "name": "Number", | |
| 34 | + "displayName": "Card Number", | |
| 35 | + "value": "004/102" | |
| 36 | + }, | |
| 37 | + { | |
| 38 | + "name": "Rarity", | |
| 39 | + "displayName": "Rarity", | |
| 40 | + "value": "Holo Rare" | |
| 41 | + }, | |
| 42 | + { | |
| 43 | + "name": "Card Type", | |
| 44 | + "displayName": "Card Type", | |
| 45 | + "value": "Fire" | |
| 46 | + }, | |
| 47 | + { | |
| 48 | + "name": "HP", | |
| 49 | + "displayName": "HP", | |
| 50 | + "value": "120" | |
| 51 | + }, | |
| 52 | + { | |
| 53 | + "name": "Stage", | |
| 54 | + "displayName": "Stage", | |
| 55 | + "value": "Stage 2" | |
| 56 | + }, | |
| 57 | + { | |
| 58 | + "name": "CardText", | |
| 59 | + "displayName": "Card Text", | |
| 60 | + "value": "<strong>Pokémon Power: Energy Burn</strong> As often as you like during your turn <em>(before your attack)</em>, you may turn all Energy attached to Charizard into Fire Energy for the rest of the turn. This power can't be used if Charizard is Asleep, Confused, or Paralyzed." | |
| 61 | + }, | |
| 62 | + { | |
| 63 | + "name": "Attack 1", | |
| 64 | + "displayName": "Attack 1", | |
| 65 | + "value": "[RRRR] Fire Spin (100)\r\n<br>Discard 2 Energy cards attached to Charizard in order to use this attack." | |
| 66 | + }, | |
| 67 | + { | |
| 68 | + "name": "Weakness", | |
| 69 | + "displayName": "Weakness", | |
| 70 | + "value": "W" | |
| 71 | + }, | |
| 72 | + { | |
| 73 | + "name": "Resistance", | |
| 74 | + "displayName": "Resistance", | |
| 75 | + "value": "F-30" | |
| 76 | + }, | |
| 77 | + { | |
| 78 | + "name": "RetreatCost", | |
| 79 | + "displayName": "Retreat Cost", | |
| 80 | + "value": "3" | |
| 81 | + } | |
| 82 | + ] | |
| 83 | + }, | |
| 84 | + "prices": [ | |
| 85 | + { | |
| 86 | + "productId": 42382, | |
| 87 | + "lowPrice": 449.99, | |
| 88 | + "midPrice": 999, | |
| 89 | + "highPrice": 4761.9, | |
| 90 | + "marketPrice": 897.19, | |
| 91 | + "directLowPrice": null, | |
| 92 | + "subTypeName": "Holofoil" | |
| 93 | + } | |
| 94 | + ], | |
| 95 | + "updatedAt": "2026-09-06T20:05:30+0000" | |
| 96 | + } | |
| 97 | + }, | |
| 98 | + "expect": { | |
| 99 | + "minCount": 2, | |
| 100 | + "kinds": [ | |
| 101 | + "catalog_item", | |
| 102 | + "price_observation" | |
| 103 | + ], | |
| 104 | + "requiredFields": [ | |
| 105 | + "attributes.identifiers.tcgplayer_id", | |
| 106 | + "attributes.set", | |
| 107 | + "attributes.number" | |
| 108 | + ] | |
| 109 | + }, | |
| 110 | + "note": "Live capture 2026-09-07 — tcgcsv.com/tcgplayer/3/604 (Base Set), subtypes Holofoil", | |
| 111 | + "capturedAt": "2026-09-07T06:38:25.908Z" | |
| 112 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/tcgcsv/pokemon-base-energy.json
+77 −0
@@ -0,0 +1,77 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://www.tcgplayer.com/product/42349/pokemon-base-set-psychic-energy", | |
| 4 | + "externalId": "42349", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "fetchedAt": "2026-09-07T06:38:25.908Z", | |
| 8 | + "payload": { | |
| 9 | + "categoryId": 3, | |
| 10 | + "category": { | |
| 11 | + "slug": "pokemon", | |
| 12 | + "franchise": "Pokémon", | |
| 13 | + "brand": "The Pokémon Company", | |
| 14 | + "language": "English" | |
| 15 | + }, | |
| 16 | + "group": { | |
| 17 | + "groupId": 604, | |
| 18 | + "name": "Base Set", | |
| 19 | + "abbreviation": "BS", | |
| 20 | + "isSupplemental": false, | |
| 21 | + "publishedOn": "1999-01-09T00:00:00", | |
| 22 | + "categoryId": 3 | |
| 23 | + }, | |
| 24 | + "product": { | |
| 25 | + "productId": 42349, | |
| 26 | + "name": "Psychic Energy", | |
| 27 | + "cleanName": "Psychic Energy", | |
| 28 | + "imageUrl": "https://tcgplayer-cdn.tcgplayer.com/product/42349_200w.jpg", | |
| 29 | + "url": "https://www.tcgplayer.com/product/42349/pokemon-base-set-psychic-energy", | |
| 30 | + "modifiedOn": "2026-08-05T23:02:16.687", | |
| 31 | + "extendedData": [ | |
| 32 | + { | |
| 33 | + "name": "Number", | |
| 34 | + "displayName": "Card Number", | |
| 35 | + "value": "101/102" | |
| 36 | + }, | |
| 37 | + { | |
| 38 | + "name": "Rarity", | |
| 39 | + "displayName": "Rarity", | |
| 40 | + "value": "Common" | |
| 41 | + }, | |
| 42 | + { | |
| 43 | + "name": "Card Type", | |
| 44 | + "displayName": "Card Type", | |
| 45 | + "value": "Basic Energy" | |
| 46 | + } | |
| 47 | + ] | |
| 48 | + }, | |
| 49 | + "prices": [ | |
| 50 | + { | |
| 51 | + "productId": 42349, | |
| 52 | + "lowPrice": 0.1, | |
| 53 | + "midPrice": 0.4, | |
| 54 | + "highPrice": 18.9, | |
| 55 | + "marketPrice": 0.79, | |
| 56 | + "directLowPrice": 1.25, | |
| 57 | + "subTypeName": "Normal" | |
| 58 | + } | |
| 59 | + ], | |
| 60 | + "updatedAt": "2026-09-06T20:05:30+0000" | |
| 61 | + } | |
| 62 | + }, | |
| 63 | + "expect": { | |
| 64 | + "minCount": 2, | |
| 65 | + "kinds": [ | |
| 66 | + "catalog_item", | |
| 67 | + "price_observation" | |
| 68 | + ], | |
| 69 | + "requiredFields": [ | |
| 70 | + "attributes.identifiers.tcgplayer_id", | |
| 71 | + "attributes.set", | |
| 72 | + "attributes.number" | |
| 73 | + ] | |
| 74 | + }, | |
| 75 | + "note": "Live capture 2026-09-07 — tcgcsv.com/tcgplayer/3/604 (Base Set), subtypes Normal", | |
| 76 | + "capturedAt": "2026-09-07T06:38:25.908Z" | |
| 77 | +} | |
| \ No newline at end of file | ||
added
data/fixtures/tcgcsv/pokemon-first-product.json
+67 −0
@@ -0,0 +1,67 @@ | ||
| 1 | +{ | |
| 2 | + "raw": { | |
| 3 | + "url": "https://www.tcgplayer.com/product/712093/pokemon-me06-delta-reign-delta-reign-3-pack-blister-seel", | |
| 4 | + "externalId": "712093", | |
| 5 | + "kind": "catalog_item", | |
| 6 | + "engine": "api", | |
| 7 | + "httpStatus": 200, | |
| 8 | + "payload": { | |
| 9 | + "categoryId": 3, | |
| 10 | + "category": { | |
| 11 | + "slug": "pokemon", | |
| 12 | + "franchise": "Pokémon", | |
| 13 | + "brand": "The Pokémon Company", | |
| 14 | + "language": "English" | |
| 15 | + }, | |
| 16 | + "group": { | |
| 17 | + "groupId": 24831, | |
| 18 | + "name": "ME06: Delta Reign", | |
| 19 | + "abbreviation": "DLR", | |
| 20 | + "isSupplemental": false, | |
| 21 | + "publishedOn": "2026-11-06T00:00:00", | |
| 22 | + "categoryId": 3 | |
| 23 | + }, | |
| 24 | + "product": { | |
| 25 | + "productId": 712093, | |
| 26 | + "name": "Delta Reign 3 Pack Blister [Seel]", | |
| 27 | + "cleanName": "Delta Reign 3 Pack Blister Seel", | |
| 28 | + "imageUrl": "https://tcgplayer-cdn.tcgplayer.com/product/712093_200w.jpg", | |
| 29 | + "url": "https://www.tcgplayer.com/product/712093/pokemon-me06-delta-reign-delta-reign-3-pack-blister-seel", | |
| 30 | + "modifiedOn": "2026-08-21T19:13:09.38", | |
| 31 | + "extendedData": [ | |
| 32 | + { | |
| 33 | + "name": "CardText", | |
| 34 | + "displayName": "Card Text", | |
| 35 | + "value": "A Legendary Storm Is Stirring!<br>\r\nA thunderous roar from above heralds the descent of a renowned ruler of the sky! Mega Rayquaza ex brings unparallelled strength to battle, inspiring Mega Golurk ex, Mega Malamar ex, and Mega Golisopod ex to come forth and put their own skills to the test. Prevailing winds lead to legendary places and emerald-colored storms in the Pokémon TCG: Mega Evolution—Delta Reign expansion! <br>\r\nEach blister comes with 3 booster packs and a promotional card." | |
| 36 | + } | |
| 37 | + ] | |
| 38 | + }, | |
| 39 | + "prices": [ | |
| 40 | + { | |
| 41 | + "productId": 712093, | |
| 42 | + "lowPrice": 34.99, | |
| 43 | + "midPrice": 52.24, | |
| 44 | + "highPrice": 52.58, | |
| 45 | + "marketPrice": 52.59, | |
| 46 | + "directLowPrice": null, | |
| 47 | + "subTypeName": "Normal" | |
| 48 | + } | |
| 49 | + ], | |
| 50 | + "updatedAt": "2026-09-06T20:05:30+0000" | |
| 51 | + }, | |
| 52 | + "fetchedAt": "2026-09-07T06:26:43.883Z" | |
| 53 | + }, | |
| 54 | + "expect": { | |
| 55 | + "minCount": 1, | |
| 56 | + "kinds": [ | |
| 57 | + "catalog_item", | |
| 58 | + "price_observation" | |
| 59 | + ], | |
| 60 | + "requiredFields": [ | |
| 61 | + "attributes.identifiers.tcgplayer_id", | |
| 62 | + "attributes.set" | |
| 63 | + ] | |
| 64 | + }, | |
| 65 | + "note": "Live capture 2026-09-07 from https://www.tcgplayer.com/product/712093/pokemon-me06-delta-reign-delta-reign-3-pack-blister-seel", | |
| 66 | + "capturedAt": "2026-09-07T06:26:44.118Z" | |
| 67 | +} | |
| \ No newline at end of file | ||
modified
workers/entity-resolution/canonical-key.ts
+2 −1
@@ -13,7 +13,8 @@ const num = (s: string | null | undefined): string => (s ? s.toLowerCase().repla | ||
| 13 | 13 | /** Identifier keys considered deterministic enough to match on their own. */ |
| 14 | 14 | export const DETERMINISTIC_IDS = [ |
| 15 | 15 | 'scryfall_id', 'oracle_id', 'tcgplayer_id', 'cardmarket_id', 'pokemontcg_id', 'ygo_id', 'ygo_set_code', 'psa_spec_id', 'psa_item_id', |
| 16 | − 'style_code', 'sku', 'upc', 'ean', 'isbn', 'lego_set_number', 'bricklink_id', 'brickset_id', 'pricecharting_id', 'reference', 'goldin_item_id', 'stockx_id', 'chrono24_id', 'comic_id', 'gocollect_id', 'hobbydb_id', 'funko_id', | |
| 16 | + 'style_code', 'sku', 'upc', 'ean', 'isbn', 'jan', 'lego_set_number', 'bricklink_id', 'brickset_id', 'pricecharting_id', 'reference', 'goldin_item_id', 'stockx_id', 'chrono24_id', 'comic_id', 'gocollect_id', 'hobbydb_id', 'funko_id', | |
| 17 | + 'tcgdex_id', 'lorcast_id', 'mtggoldfish_uuid', 'gatcg_edition_id', 'swudb_id', 'digimoncard_id', 'sorcery_printing_id', 'discogs_release_id', 'pcgs_number', 'mfc_id', 'comics_org_id', 'ebay_epid', 'asin', 'vin', 'bgg_id', 'amiami_gcode', 'fossilera_item', 'ppg_id', | |
| 17 | 18 | ] as const; |
| 18 | 19 | |
| 19 | 20 | export function deterministicIdentifiers(ids: Record<string, string> | undefined): Record<string, string> { |
| 20 | 21 | |