TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import type { ReactNode } from 'react';4import { ASK_ANOMALY_HIGH_RATIO, ASK_ANOMALY_LOW_RATIO, ASK_MIN_CONFIDENCE, ASK_MIN_MATCH_CONFIDENCE, ASK_MIN_SAMPLE, DEAL_REVIEW_THRESHOLD, DEAL_THRESHOLD, FEE_SCHEDULES, FEE_SCHEDULE_AS_OF, MAX_PLAUSIBLE_CHANGE, PREMIUM_THRESHOLD } from '@rareindex/valuation';5import { FeeConfidenceBadge, feeScheduleSummary } from '@/components/market/fee-schedule';6import { PageHeader } from '@/components/ui/page-header';7import { Badge, Card } from '@/components/ui/primitives';8import { cn } from '@/lib/format';910export const metadata: Metadata = {11 title: 'Methodology Center',12 description: 'How every RareIndex number is produced: RareIndex Valuation (RIV), confidence, value range, comparables, the ask-vs-RIV anomaly gate, liquidity, rarity, momentum, outliers, sales verification, indices, FX and data-honesty rules.',13};14export const revalidate = 3600;1516/*17 * §203 Methodology Center. Constants that gate numbers on the site are imported from @rareindex/valuation so18 * this page cannot drift from the implementation. Constants that live as literals in the engine files are19 * quoted with their file so a reader can verify them (valuation.ts, scores.ts, outliers.ts, premiums.ts,20 * indices/chain.ts, workers/indices/run.ts).21 */2223const pct = (v: number, digits = 0) => `${(v * 100).toFixed(digits)} %`;2425const SECTIONS: Array<{ id: string; title: string }> = [26 { id: 'principles', title: 'Data honesty' },27 { id: 'riv', title: 'RIV — RareIndex Valuation' },28 { id: 'confidence', title: 'Confidence' },29 { id: 'range', title: 'Value range' },30 { id: 'distribution', title: 'Price distribution' },31 { id: 'comparables', title: 'Comparables & grade premiums' },32 { id: 'ask-vs-riv', title: 'Ask vs RIV & the anomaly gate' },33 { id: 'liquidity', title: 'Liquidity Score' },34 { id: 'rarity', title: 'Rarity Score' },35 { id: 'momentum', title: 'Momentum & Trending' },36 { id: 'outliers', title: 'Outliers' },37 { id: 'verification', title: 'Sales verification' },38 { id: 'indices', title: 'Indices' },39 { id: 'fx', title: 'Currencies' },40 { id: 'freshness', title: 'Freshness & limits' },41 { id: 'fees', title: 'Auction fees & all-in cost' },42];4344export default function MethodologyPage() {45 return (46 <div>47 <PageHeader48 kicker="Transparency"49 title="Methodology Center"50 description="Every number on RareIndex is either observed data, a model estimate, user data or an external asking price — and is labelled as such. This page documents how each estimate is produced, what evidence it rests on and the exact thresholds that gate it. Thresholds are read from the valuation engine, not copied by hand."51 meta={52 <>53 <Link href="/data" className="hover:text-fg">54 Coverage & sources →55 </Link>56 <Link href="/rareindex" className="hover:text-fg">57 Indices →58 </Link>59 <Link href="/api-docs" className="hover:text-fg">60 API →61 </Link>62 </>63 }64 />65 <div className="grid gap-6 lg:grid-cols-[220px_minmax(0,1fr)] lg:items-start">66 <nav aria-label="Methodology sections" className="hidden lg:block lg:sticky lg:top-[calc(var(--ri-header-h)+16px)]">67 <ol className="border-l border-border text-[12px]">68 {SECTIONS.map((s, i) => (69 <li key={s.id}>70 <a href={`#${s.id}`} className="-ml-px flex items-baseline gap-2 border-l border-transparent py-1 pl-3 text-muted hover:border-fg hover:text-fg">71 <span className="num w-4 shrink-0 text-[10px] text-subtle">{i + 1}</span>72 {s.title}73 </a>74 </li>75 ))}76 </ol>77 </nav>78 <div className="grid gap-4">79 <div className="lg:hidden">80 <details className="card p-3 text-[12px]">81 <summary className="cursor-pointer font-medium text-fg">Sections</summary>82 <ol className="mt-2 grid gap-1 pl-4 text-muted">83 {SECTIONS.map((s) => (84 <li key={s.id}>85 <a href={`#${s.id}`} className="hover:text-fg">86 {s.title}87 </a>88 </li>89 ))}90 </ol>91 </details>92 </div>9394 <Section id="principles" n={1} title="Data honesty" lead="Four kinds of numbers appear on the platform. They are never mixed, and a missing number is shown as such (§205–§206).">95 <div className="grid gap-2 sm:grid-cols-2">96 <Kind tone="gain" name="Observed data" desc="A sale, a listing, an auction lot, a population count or a guide price captured from a public source, stored immutably with its URL, capture time, connector and parser version. Shown with its source link." />97 <Kind tone="index" name="Model estimate" desc="RIV, its range and confidence, scores (liquidity, rarity, momentum, trending, deal), indices and returns. Always displayed with sample size, confidence and update time." />98 <Kind tone="rarity" name="User data" desc="Purchase prices, quantities, manual valuations and notes in collections and watchlists. Private by default; never used to compute market estimates." />99 <Kind tone="alert" name="External ask" desc="An asking price on a marketplace. Never a market value: it is compared with RIV only through the gate in §7 and labelled as an ask everywhere." />100 </div>101 <P>102 RareIndex never fabricates transactions, valuations, bids, populations, sales, historical prices or certification data. Where evidence is missing the interface shows <em>Data unavailable</em> or <em>Not enough data</em>. Valuations are estimates from observed public sales, not offers or appraisals; RareIndex does not authenticate items.103 </P>104 </Section>105106 <Section id="riv" n={2} title="RIV — RareIndex Valuation" lead="RIV estimates the current fair market value of a variant (an asset in one grade/condition) from verified transactions. It is never an arithmetic mean alone.">107 <H>Evidence and window</H>108 <P>109 Inputs are valid transactions of the variant: status <Code>valid</Code>, price > 0, single item (bundles and multi-quantity lots excluded). The default window is <strong>365 days</strong>; when fewer than five recent sales exist and older ones do, it is extended to <strong>3 years</strong> and confidence is capped at 70 %.110 </P>111 <H>Estimators</H>112 <Table113 head={['Estimator', 'Definition']}114 rows={[115 ['Latest', 'most recent transaction in the window'],116 ['Median 5 / 10 / 20', 'median of the last 5, 10 and 20 transactions'],117 ['Trimmed mean', '10 % trimmed each tail; requires ≥ 5 sales'],118 ['EW mean', 'exponentially weighted mean, 90-day half-life'],119 ['Weighted mean', 'weight = source trust × identification confidence × 0.5^(age / 90 d)'],120 ['Comps-adjusted', 'median of other-grade sales × empirical grade premium (≥ 3 comps) — see §6'],121 ['Guide', 'median of the 5 most recent price-guide observations ≤ 60 days old (market / mid / trend kinds preferred)'],122 ]}123 />124 <H>Basis ladder</H>125 <Table126 head={['Basis', 'When', 'RIV', 'Confidence']}127 rows={[128 ['transactions', '≥ 3 sales in window', 'median of {EW mean, median 10 (or 5), weighted mean, trimmed mean (or median 5)}', 'computed (§3)'],129 ['transactions', '1–2 sales + comps or guide', 'median of {latest, comps or guide}', '0.45 (comps) · 0.40 (guide)'],130 ['transactions', '1–2 sales only', 'median of the two, or the single sale', '0.30 · 0.20'],131 ['comps', 'no sales, ≥ 3 grade-adjusted comps', 'comps-adjusted median', '0.25 + 0.03 × comps, ≤ 0.50'],132 ['guide', 'no sales, guide observations only', 'guide median', '0.20 + 0.05 × obs, ≤ 0.45'],133 ['none', 'nothing usable', 'Data unavailable', '—'],134 ]}135 />136 <P>137 A <strong>guide</strong> value is a price published by a price guide or marketplace (e.g. a “market price” cell). It is an observation, not a transaction; in the interface it appears as <em>Guide · source</em>, never as RIV, and guide-based RIVs are labelled with a capped confidence. The headline RIV of an asset is the valuation of its representative variant: the default (raw / base) variant when it has a transaction-based RIV with ≥ {ASK_MIN_SAMPLE} sales and confidence ≥ {pct(ASK_MIN_CONFIDENCE)}, otherwise the qualifying variant with the most sales. Period changes are measured on that variant's own series so a change of representative never shows as a price move.138 </P>139 <Ref>packages/valuation/src/valuation.ts · workers/valuation/run.ts</Ref>140 </Section>141142 <Section id="confidence" n={3} title="Confidence" lead="Every RIV carries a 0–1 confidence and a label. Users must be able to see how uncertain a valuation is.">143 <Formula>confidence = 0.35 · size + 0.30 · (1 − dispersion) + 0.20 · recency + 0.15 · trust</Formula>144 <Table145 head={['Term', 'Definition', 'Scale']}146 rows={[147 ['size', 'log2(n + 1) / log2(41)', '40 transactions → 1'],148 ['dispersion', 'robust σ of log prices = 1.4826 × MAD, divided by 0.6', '60 % log-dispersion → 0'],149 ['recency', '1 − age of the latest sale / 365 d', 'a year-old latest sale → 0'],150 ['trust', 'mean of source trust × identification confidence over the sales used', '0–1'],151 ]}152 />153 <P>154 Labels: <Badge tone="gain">High ≥ 75 %</Badge> <Badge tone="index">Medium ≥ 50 %</Badge> <Badge tone="alert">Low > 20 %</Badge> <Badge>Insufficient</Badge>. Window extended to three years → capped at 70 %; comps-only ≤ 50 %; guide-only ≤ 45 %.155 </P>156 <Ref>packages/valuation/src/valuation.ts (confidence, confidenceLabel)</Ref>157 </Section>158159 <Section id="range" n={4} title="Value range — low / fair / high" lead="A point estimate is never presented as exact. Each RIV comes with an expected market range.">160 <Table161 head={['Sample', 'Low', 'High']}162 rows={[163 ['≥ 5 sales', 'min(P25 of the sales used, RIV × 0.95)', 'max(P75, RIV × 1.05)'],164 ['2–4 sales', 'RIV × e^(−max(σ, 0.1)) with σ = sd of log prices', 'RIV × e^(+max(σ, 0.1))'],165 ['1 sale / fallback', 'RIV × (1 − 20 %) — 30 % for guide-based', 'RIV × (1 + 20 %) — 30 % for guide-based'],166 ]}167 />168 <P>The band is always at least ±5 % around RIV. On asset pages it is drawn as the shaded band around the RIV line; the current RIV and latest sale are marked on the range meter.</P>169 </Section>170171 <Section id="distribution" n={5} title="Price distribution" lead="Beyond the point estimate, the dispersion of the transactions themselves is shown.">172 <P>For the variant and window, RareIndex reports count, minimum, P25, median, P75, maximum and the 10 % trimmed mean of verified sale prices in USD at historical FX. Flagged outliers (§11) are shown in sales tables for audit but excluded from these statistics.</P>173 </Section>174175 <Section id="comparables" n={6} title="Comparables & grade premiums" lead="When a variant has too few sales, sales of other grades of the same asset are used — adjusted, never mixed as-is.">176 <P>177 For each category, grader and grade, the <strong>grade premium</strong> is the median, across assets, of median(graded price) / median(raw price) of the <em>same asset</em>, from at least <strong>8 paired assets</strong>. A comparable from grade A to grade B is adjusted by premium(B) / premium(A). Premiums exist only where paired evidence exists: PSA 10, BGS 10 and CGC 10 are never assumed equivalent, and no cross-grader factor is applied without data.178 </P>179 <P>Comparable sales on asset pages carry a transparent similarity score (same set, same grade, same variant, price proximity). Grade curves (price by grade) and premiums are published on the Grades tab and on /grading.</P>180 <Ref>packages/valuation/src/premiums.ts (computeGradePremiums, adjustmentFactor)</Ref>181 </Section>182183 <Section id="ask-vs-riv" n={7} title="Ask vs RIV & the anomaly gate" lead="An asking price is compared with a valuation only when the comparison is legitimate. An implausible ask is a data or identity problem, never a deal.">184 <Formula>discount = (ask + fees − RIV) / RIV · negative = below the valuation</Formula>185 <H>Gates — all must pass</H>186 <Table187 head={['Gate', 'Threshold']}188 rows={[189 ['Same variant', 'the ask is compared with the RIV of its own grade/condition variant; a slab whose grade could not be read is kept in a “grade unknown” variant and never compared against raw'],190 ['Valuation basis', 'transactions only — comps-only and guide-only RIVs never qualify an ask'],191 ['RIV confidence', `≥ ${pct(ASK_MIN_CONFIDENCE)}`],192 ['Transactions used', `≥ ${ASK_MIN_SAMPLE}`],193 ['Listing identification confidence', `≥ ${pct(ASK_MIN_MATCH_CONFIDENCE)}`],194 ['Plausibility band', `ask between ${ASK_ANOMALY_LOW_RATIO}× and ${ASK_ANOMALY_HIGH_RATIO}× RIV — outside it the listing is flagged riv_anomaly and shown as “Data/identity anomaly”`],195 ]}196 />197 <H>Verdicts</H>198 <Table199 head={['Verdict', 'Rule']}200 rows={[201 ['Deal', `discount ≤ ${pct(DEAL_THRESHOLD)} (ask at least ${pct(-DEAL_THRESHOLD)} below RIV)`],202 ['Fair', `between ${pct(DEAL_THRESHOLD)} and ${pct(PREMIUM_THRESHOLD)}`],203 ['Premium', `discount ≥ ${pct(PREMIUM_THRESHOLD)}`],204 ['Needs review', `more than ${Math.round(-DEAL_REVIEW_THRESHOLD * 100)} % below RIV — kept as a number, flagged riv_review, excluded from every deal rail, Deal Radar, digest and Rare Radar until reviewed (§174)`],205 ['Data/identity anomaly', `ratio outside ${ASK_ANOMALY_LOW_RATIO}×–${ASK_ANOMALY_HIGH_RATIO}× — wrong variant, lot, currency, typo or mismatch until reviewed`],206 ['Not compared', 'one of the gates failed; the reason is shown (e.g. riv_sample, riv_basis_guide)'],207 ]}208 />209 <H>Deal Score (0–100)</H>210 <Formula>score = 100 · depth^0.7 · (0.4 + 0.6 · confidence) · (0.5 + 0.5 · sample) · (0.5 + 0.5 · liquidity / 100) · match</Formula>211 <P>212 depth = min(1, −discount / 50 %), sample = log10(1 + n) / log10(41). The components are multiplicative: a huge discount on an illiquid or poorly identified asset cannot score high. The score is 0 for anything that is not a gated discount. Period changes of a valuation beyond ±{MAX_PLAUSIBLE_CHANGE * 100} % are likewise treated as data artefacts and not published as movers.213 </P>214 <P className="text-subtle">Value opportunities and Deal Radar are analytical data, not investment advice: a discount can also mean a misidentified, damaged or incomplete item.</P>215 <Ref>packages/valuation/src/scores.ts (assessAsk, dealScore, isPlausibleChange) · workers/valuation/run.ts</Ref>216 </Section>217218 <Section id="liquidity" n={8} title="Liquidity Score (0–100)" lead="How readily a variant trades. Null when there is no evidence at all — never 0 by default.">219 <Formula>liquidity = 100 · (0.40 · s₁ + 0.15 · s₂ + 0.10 · s₃ + 0.20 · s₄ + 0.15 · s₅)</Formula>220 <Table221 head={['Component', 'Input', 'Scale (clamped 0–1)', 'Weight']}222 rows={[223 ['s₁ sales pace', 'sales per month (12-month average at asset level; 36-month at variant level)', 'log10(1 + x) / log10(31) · 30 / month → 1', '40 %'],224 ['s₂ listing depth', 'active listings', 'log10(1 + x) / log10(51) · 50 → 1', '15 %'],225 ['s₃ source breadth', 'distinct sources with evidence', '(sources − 1) / 4', '10 %'],226 ['s₄ sale spacing', 'median days between sales', '1 − days / 90 · 0 when unknown', '20 %'],227 ['s₅ ask–RIV spread', '(lowest ask of the representative variant − RIV) / RIV', '1 − |spread| / 50 % · 0.5 when there is no ask', '15 %'],228 ]}229 />230 <P>Labels used in the interface: Highly liquid ≥ 75, Moderately liquid ≥ 50, Illiquid ≥ 25, Extremely illiquid below.</P>231 <Ref>packages/valuation/src/scores.ts (liquidityScore)</Ref>232 </Section>233234 <Section id="rarity" n={9} title="Rarity Score (0–100)" lead="A multi-dimensional supply score. It distinguishes documented supply (population, production) from market supply (how often the object appears).">235 <Table236 head={['Signal', 'Scale (clamped 0–1)', 'Weight']}237 rows={[238 ['Graded population (latest published report)', '1 − log10(1 + pop) / 5 · 100 000 → 0', '40 %'],239 ['Documented production quantity', '1 − log10(qty) / 7', '30 %'],240 ['Sales per year', '1 − log10(1 + sales) / 3 · 1 000 → 0', '20 %'],241 ['Listings per year (active × 4)', '1 − log10(1 + listings) / 3', '10 %'],242 ]}243 />244 <P>Weights are renormalised over the signals that exist for the asset; a population growing more than 20 % between reports multiplies the score by 0.9. With no signal at all the score is null. Population figures come only from published grading-company reports and are shown with their report date.</P>245 <Ref>packages/valuation/src/scores.ts (rarityScore)</Ref>246 </Section>247248 <Section id="momentum" n={10} title="Momentum & Trending" lead="Momentum measures how a valuation and its activity are changing; Trending blends momenta into one discovery signal.">249 <Formula>momentum(h) = 100 · (0.7 · clamp(Δprice / 50 %, ±1) + 0.3 · (volume_h − volume_prev) / max(1, volume_h + volume_prev))</Formula>250 <P>Horizons 7 d / 30 d / 90 d / 1 y; Δprice is the change of the representative variant's RIV against its own history. Trending is the geometric mean of the available momenta mapped to 0–1 (price 30 d, transaction volume, listing depth); it requires at least two components. Movers rails require ≥ {ASK_MIN_SAMPLE} transactions, confidence ≥ {pct(ASK_MIN_CONFIDENCE)}, ≥ 3 sales in the last year and a plausible move (≤ ±{MAX_PLAUSIBLE_CHANGE * 100} %).</P>251 <Ref>packages/valuation/src/scores.ts (momentumScore, trendingScore)</Ref>252 </Section>253254 <Section id="outliers" n={11} title="Outliers" lead="Abnormal prices are flagged, never deleted, and every flag is auditable.">255 <P>256 Within a variant with at least five valid sales, the modified z-score of each log price, 0.6745 · (x − median) / MAD, is computed; sales beyond <strong>3.5</strong> are flagged <Code>abnormally_high_price</Code> / <Code>abnormally_low_price</Code> and set to status <Code>flagged</Code> (when MAD is 0, anything beyond 3× the median is flagged). Bundles and multi-quantity lots are <Code>excluded</Code> at ingestion. Flagged and excluded sales stay visible in sales tables for audit and are written to the audit log with their reason; they do not enter RIV, distributions or indices.257 </P>258 <Ref>packages/valuation/src/outliers.ts · workers/valuation/run.ts</Ref>259 </Section>260261 <Section id="verification" n={12} title="Sales verification" lead="What “verified” means today — and what it does not.">262 <P>263 A sale is a completed-transaction record captured from a public source (auction result page, completed marketplace listing, price-guide sold feed). Each sale stores its <strong>status</strong> (<Code>valid</Code>, <Code>flagged</Code>, <Code>excluded</Code>), an <strong>identification confidence</strong> (0–1: the weaker of the record's own confidence and the entity-resolution confidence; deterministic identifiers 0.99, canonical key 0.96, fuzzy match ≤ 0.90), the <strong>trust score</strong> of its source (0–1, weighting valuations), <strong>flags</strong> (<Code>bundle</Code>, <Code>zero_price</Code>, <Code>low_identification_confidence</Code>, outlier reasons) and whether the buyer premium is known to be included.264 </P>265 <P>266 Record-sale and “verified transactions” views use status <Code>valid</Code> with confidence ≥ 0.8. Duplicate captures are collapsed by source, external id or URL, date and price. RareIndex does not yet publish a separate “likely / unverified” verification ladder or cross-source duplicate merging of the same physical transaction; where a transaction is only reported second-hand it appears as a price observation, not a sale.267 </P>268 <Ref>workers/entity-resolution/writers.ts · docs/METHODOLOGY.md §1–2</Ref>269 </Section>270271 <Section id="indices" n={13} title="Indices" lead="RARE and its subindices are chain-linked indices of daily RIV changes. They publish only once enough constituents exist.">272 <Table273 head={['Rule', 'Value']}274 rows={[275 ['Constituent eligibility', 'valuation confidence ≥ 0.4 and ≥ 3 valid sales in the last 12 months; one variant per asset (the most traded)'],276 ['Daily return', 'weighted mean of constituents’ log(RIVₜ / RIV_last observed); equal weights by default, liquidity weights (12-month sales) where configured'],277 ['Robustness', 'top and bottom 5 % of daily constituent returns trimmed when ≥ 20 returns; a single constituent’s daily return is capped at ±ln 2.5'],278 ['Publication', 'a day is published only when ≥ 10 constituents have a value that day (level carried otherwise); RARE requires ≥ 25 constituents across its published subindices'],279 ['Base', '1 000 on 2024-01-01'],280 ['RARE', 'transaction-weighted mean (weight = 1 + transactions) of the daily log returns of published subindices'],281 ['Composition', 'membership can change daily without breaking the level; additions and removals are stored with their reason'],282 ['Market capitalisation', 'Σ (graded population × RIV) only for assets with a published population report, with a coverage-based confidence'],283 ]}284 />285 <P>Indices without a published value are listed as <em>in development</em> with their constituent count against the minimum, outside the primary ticker. Every daily value stores its constituent count and coverage. A repeat-sales estimator exists for research and is not a published index.</P>286 <Ref>packages/indices/src/chain.ts · workers/indices/run.ts</Ref>287 </Section>288289 <Section id="fx" n={14} title="Currencies" lead="Native prices are never overwritten; every conversion is reproducible.">290 <P>291 Each sale stores <Code>price</Code>, <Code>currency</Code>, <Code>price_usd</Code>, <Code>fx_rate</Code> and <Code>fx_date</Code>. Rates are ECB reference rates (via frankfurter), base USD, applied at the <strong>transaction date</strong> (previous business day for weekends and holidays); historical sales are never converted at today's rate. A missing rate blocks the record rather than approximating it. Display currencies (USD, CAD, EUR, GBP, JPY, …) are converted at view time from the stored USD value.292 </P>293 <Ref>workers/fx.ts · workers/lib/fx.ts</Ref>294 </Section>295296 <Section id="freshness" n={15} title="Freshness & limits" lead="Coverage is uneven by design; the interface shows what it knows and when it learned it.">297 <P>Connectors run on schedules tuned to each source; every statistic shows its last update and sample size. Categories with few public transactions show fewer valuations and no index rather than a fabricated one. Listing prices are not confirmed transactions; past performance does not guarantee future results; RareIndex does not authenticate items and its valuations are not appraisals.</P>298 <P>299 <Link href="/data" className="text-fg underline-offset-4 hover:underline">300 Live coverage, connectors and FX ranges →301 </Link>302 </P>303 </Section>304305 <Section id="fees" n={16} title="Auction fees & all-in cost" lead="A hammer price is not what a buyer pays. RareIndex never compares a hammer with a marketplace price without adding the buyer's premium — and labels how it did so.">306 <Formula>all-in = hammer + buyer's premium(hammer, house schedule) · VAT/sales tax on the premium, duties and shipping excluded</Formula>307 <H>How the basis is chosen</H>308 <Table309 head={['Record', 'Treatment', 'Label']}310 rows={[311 ['Connector states the premium is included', 'price kept as recorded', 'premium included'],312 ['Connector states hammer only', 'premium added from the house schedule (marginal tiers, minimum/maximum, fixed fee), default 22 % when the house is not on file', 'premium added (published / ≈ estimated / default)'],313 ['Connector does not say, house on file', 'premium added from the schedule — auction houses invoice a premium by default', 'premium added (published / ≈ estimated)'],314 ['Connector does not say, house unknown', 'price kept as recorded and flagged', 'fees unknown'],315 ['Fixed-price marketplace or dealer', 'no buyer premium; the asking price is what the buyer pays', 'no buyer premium'],316 ]}317 />318 <P>319 Live lots are assessed on the same basis: the current bid (or, without a bid, the low estimate — labelled “est.”) is converted to USD at the current ECB rate, the premium is added, and the result is compared with the RIV of the lot's variant under the same gates as listings (transaction-based RIV, ≥ {ASK_MIN_SAMPLE} sales, confidence ≥ {pct(ASK_MIN_CONFIDENCE)}, plausibility band, review threshold). Opening prices without a bid are never called deals on their own.320 </P>321 <H>Schedules on file (as of {FEE_SCHEDULE_AS_OF})</H>322 <P className="text-subtle">“Published” = read from the house's terms at that date. “≈ Approximate” = widely reported schedule not re-verified — every all-in figure built on it is shown with ≈ and the pill “premium added (≈ estimated)”. Houses not listed fall back to a labelled default of 22 %.</P>323 <div className="overflow-x-auto">324 <table className="w-full text-[12px]">325 <thead>326 <tr className="border-b border-border text-left text-[11px] uppercase tracking-wide text-subtle">327 <th className="py-1.5 pr-3">House</th>328 <th className="py-1.5 pr-3">Buyer's premium</th>329 <th className="py-1.5 pr-3">Currency</th>330 <th className="py-1.5 pr-3">Confidence</th>331 </tr>332 </thead>333 <tbody>334 {[...FEE_SCHEDULES].sort((a, b) => a.name.localeCompare(b.name)).map((s) => (335 <tr key={s.id} className="border-b border-border/60">336 <td className="py-1.5 pr-3 font-medium">{s.source ? <a href={s.source} target="_blank" rel="noopener nofollow" className="underline-offset-2 hover:underline">{s.name}</a> : s.name}</td>337 <td className="num py-1.5 pr-3">{feeScheduleSummary(s)}</td>338 <td className="py-1.5 pr-3 text-muted">{s.currency}</td>339 <td className="py-1.5 pr-3"><FeeConfidenceBadge confidence={s.confidence} /></td>340 </tr>341 ))}342 </tbody>343 </table>344 </div>345 <Ref>data/fees/auction-houses.json · packages/valuation/src/fees.ts (buyerPremium, allInPrice) · workers/auctions</Ref>346 </Section>347 </div>348 </div>349 </div>350 );351}352353function Section({ id, n, title, lead, children }: { id: string; n: number; title: string; lead: string; children: ReactNode }) {354 return (355 <Card as="article" id={id} className="scroll-mt-16 p-4 sm:p-5">356 <header className="mb-3 border-b border-border pb-3">357 <p className="t-label">358 <span className="num">{String(n).padStart(2, '0')}</span>359 </p>360 <h2 className="mt-0.5 text-base font-semibold tracking-tight text-fg">{title}</h2>361 <p className="mt-1 text-[12.5px] leading-relaxed text-muted">{lead}</p>362 </header>363 <div className="grid gap-3 text-[13px] leading-relaxed text-muted">{children}</div>364 </Card>365 );366}367368function H({ children }: { children: ReactNode }) {369 return <h3 className="mt-1 text-[11px] font-semibold uppercase tracking-wider text-subtle">{children}</h3>;370}371function P({ children, className }: { children: ReactNode; className?: string }) {372 return <p className={cn('max-w-[78ch]', className)}>{children}</p>;373}374function Code({ children }: { children: ReactNode }) {375 return <code className="rounded-sm bg-inset px-1 text-[12px] text-fg">{children}</code>;376}377function Formula({ children }: { children: ReactNode }) {378 return <p className="mono-num rounded-sm border border-border bg-sunken px-3 py-2 text-[12px] text-fg">{children}</p>;379}380function Ref({ children }: { children: ReactNode }) {381 return <p className="mono-num text-[10.5px] text-subtle">Source: {children}</p>;382}383function Kind({ tone, name, desc }: { tone: 'gain' | 'index' | 'rarity' | 'alert'; name: string; desc: string }) {384 return (385 <div className="rounded-sm border border-border p-3">386 <Badge tone={tone}>{name}</Badge>387 <p className="mt-1.5 text-[12px] leading-relaxed text-muted">{desc}</p>388 </div>389 );390}391function Table({ head, rows }: { head: string[]; rows: string[][] }) {392 return (393 <div className="overflow-x-auto">394 <table className="w-full min-w-[520px] border-collapse text-[12px]">395 <thead>396 <tr className="border-b border-border text-left text-[10px] uppercase tracking-wider text-subtle">397 {head.map((h) => (398 <th key={h} className="py-1.5 pr-3 font-medium">399 {h}400 </th>401 ))}402 </tr>403 </thead>404 <tbody className="divide-y divide-border">405 {rows.map((r, i) => (406 <tr key={i}>407 {r.map((c, j) => (408 <td key={j} className={cn('py-1.5 pr-3 align-top', j === 0 ? 'font-medium text-fg' : 'text-muted')}>409 {c}410 </td>411 ))}412 </tr>413 ))}414 </tbody>415 </table>416 </div>417 );418}419