Loading...
Loading...
This page explains, in technical detail, where every price on this site comes from, how we normalize it to a comparable unit, what formula we use to compute premiums, how we cache the data, what happens when an upstream source fails, and the limitations of the resulting numbers. Nothing here is hidden — if you can read the formula and the source list, you can reproduce our outputs yourself.
We deliberately read each exchange directly wherever feasible. Deriving a "local price" from a single global spot would mechanically zero out the premium and defeat the purpose of the site. The list below documents what we actually call.
Every price arriving at the pipeline is converted to USD per troy ounce (USD/oz) before storage and before any premium calculation. This is the single canonical unit; the dashboards and APIs only emit USD/oz internally. The conversion factors are fixed constants — 1 troy ounce = 31.1034768 grams, 1 kilogram = 1000 grams, 1 tola = 11.6638 grams — and live in `src/lib/calculations/`. They are not refreshed from any external source.
SGE Au9999 quoted at 580 CNY/g, USD/CNY = 7.20
(580 / 7.20) × 31.1034768 = 80.56 × 31.1034768 ≈ 2,506.40 USD/oz
KRX gold quoted at 105,000 KRW/g, USD/KRW = 1,360
(105,000 / 1,360) × 31.1034768 ≈ 77.21 × 31.1034768 ≈ 2,401.50 USD/oz
JPX near-month gold quoted at 12,400 JPY/g, USD/JPY = 155
(12,400 / 155) × 31.1034768 = 80.00 × 31.1034768 ≈ 2,488.28 USD/oz
SGE Ag(T+D) silver quoted at 7,800 CNY/kg, USD/CNY = 7.20
7,800 ÷ 1000 = 7.80 CNY/g → (7.80 / 7.20) × 31.1034768 ≈ 1.0833 × 31.1034768 ≈ 33.70 USD/oz
For each non-USD price, we use the cross-rate captured in the same upstream response wherever possible. When this is unavailable (e.g., reading the JPX CSV gives us JPY/g but no USD/JPY), the pipeline reads the FX layer separately and stamps a single `fxTimestamp` on the result. If `fxTimestamp` drifts more than 60 minutes from the metal-price timestamp, the conversion is flagged as derived rather than synchronous, and the dashboard renders it with an explicit "derived" label.
After every price has been normalized to USD/oz, the premium for market `M` is computed as a simple percentage difference against COMEX:
Positive values mean the local market is more expensive than COMEX (a premium). Negative values mean cheaper (a discount). We always express the result with two decimal places. We do not smooth, average, or otherwise denoise the result before display — a 7.34% premium today and a 1.20% premium tomorrow are both displayed as-is, on the assumption that hiding volatility in a tracker would defeat its purpose.
The pipeline runs on two clocks. A daily Vercel cron job at 00:00 UTC takes a complete snapshot of all sources and writes it into the history dataset that drives the historical chart. Throughout the day, the dashboard and per-market pages serve from a Vercel KV (Redis) cache layer that warms itself lazily: the first user request for a given (market, metal) pair on a given hour triggers an upstream fetch, and the result is cached for the rest of that hour. Subsequent requests hit the cache.
| Data type | TTL |
|---|---|
| Spot price (Metals.dev / GoldAPI) | 15 min |
| FX cross-rates | 60 min |
| SGE quotes | 15 min (rejects sentinel values) |
| KRX quotes (T+1) | 6 hours |
| JPX settlement CSV | Daily, after settlement-time release |
| Historical snapshot dataset | Written once daily, never overwritten |
Each price feed is wrapped in a strict resolver. The resolver returns a value only if (a) the upstream call succeeded, (b) the response shape matched the expected schema, and (c) the numeric value passes a plausibility check (gold > 1000 USD/oz, silver > 10 USD/oz, and no sentinel values like -2.0). If any check fails, the resolver returns null and the layer above attempts the fallback in this order:
Key rule: a derived value is never silently promoted to "live". If we are showing a derived value, the page tells you so, with the timestamp of the last successful read.
COMEX, MCX, SGE, KRX, and JPX all observe different holiday calendars. On weekend reads, several feeds return either the most recent close or a sentinel; we surface the timestamp instead of pretending the value is real-time.
When the active-month COMEX contract rolls (e.g., February → April for gold, March → May for silver), the benchmark briefly jumps by the calendar spread. We roll manually after open interest has shifted, not on the calendar date, and announce the roll in the change log.
Ag(T+D) is quoted in CNY per kilogram, while Au9999 is in CNY per gram. We divide silver by 1000 before the USD/oz conversion. A historical bug in this divisor would produce a silver premium 1000× too large; this is documented to prevent recurrence.
KRX delivers the prior session's closing prices, not intraday quotes. The KRX card always carries a "T+1" tag and a yesterday-dated timestamp; comparing it to today's COMEX figure is an apples-to-oranges comparison and the page warns about this.
The current JPX integration parses gold settlement CSVs. We do not show JPX silver, because the silver contract on Tocom has very thin liquidity and the resulting premium would be more noise than signal.
The premium we publish is a wholesale, pre-tax comparison. India's 15% import duty and 3% GST, the UK's 20% VAT on silver, China's 13% VAT on non-SGE gold — none of these are added to the displayed premium. Readers reasoning about retail prices should consult the tax comparison page.
Any change to a feed, a conversion factor, or the formula above is treated as a methodology change. We do four things when one ships: (1) update the live pipeline; (2) update this page in the same release; (3) publish a dated entry in the change log below; (4) replay the change against the past 30 days of cached snapshots and write a delta note if the impact is greater than 0.10% on any market. Historical chart data is never silently rewritten; if it must be corrected, the affected dates carry an explicit annotation.
Methodology is only useful if it stays accurate. If a feed has changed shape, a tax rate is outdated, a contract month rolled, or a translation reads wrong, please write to us through the contact page. We acknowledge every correction.