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
MCX gold quoted at 6,150 INR/g, USD/INR = 83.5
(6,150 / 83.5) × 31.1034768 ≈ 73.65 × 31.1034768 ≈ 2,290.79 USD/oz
LBMA PM fix quoted at 1,850 GBP/oz, USD/GBP = 0.79
1,850 / 0.79 ≈ 2,341.77 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
Non-USD prices are converted using cross-rates fetched from ExchangeRate-API and cached for 72 hours (see the data-source table above). This is a separate upstream call from the Metals.dev spot fetch, so the two responses are not atomic and can differ by up to a few minutes at cron time — the observed drift is small but not zero. When ExchangeRate-API fails and no cached value is available, the pipeline surfaces "data unavailable" on the affected card instead of substituting a hard-coded rate.
After every price has been normalized to USD/oz, the premium for market `M` is computed as a simple percentage difference against the USD spot benchmark:
Positive values mean the local market is more expensive than the USD spot benchmark (a premium). Negative values mean cheaper (a discount). Results are always shown to two decimal places. The displayed number is not smoothed, averaged, or otherwise denoised — a 7.34% premium today and a 1.20% premium tomorrow are both surfaced as-is, on the view that hiding volatility in a tracker defeats its purpose.
The pipeline runs two daily Vercel cron jobs (see vercel.json). At 00:00 UTC, the warm-cache job calls each upstream feed once and writes the fresh values into the Vercel KV (Redis) cache used by the dashboard and per-market pages. Fifteen minutes later, at 00:15 UTC, the snapshot-history job reads the fresh cache and appends a per-date, per-market row to the historical dataset that drives the chart on the ratio and rankings pages. Public user requests read only from the cache — they never trigger a live upstream fetch. If the value the user asked for is not in cache, the API returns a temporary "data unavailable" state (HTTP 503) instead of calling Metals.dev, GoldAPI, SGE, or ExchangeRate-API. Upstream calls only happen from the two scheduled cron jobs and from authenticated admin endpoints (ADMIN_API_KEY-gated).
| Data type | TTL |
|---|---|
| Spot price (Metals.dev / GoldAPI) | 72 h |
| FX cross-rates | 72 h |
| SGE quotes | 72 h (rejects sentinel values) |
| Historical snapshot dataset (per date, per market) | 90 d TTL in Redis; a new snapshot for the same date replaces the previous point |
The 72-hour TTL is intentional: cron runs once per day, so a 72-hour window is what keeps a card populated across weekends, holidays, and a single missed cron. When the underlying feed fails a plausibility check the pipeline re-writes the previously cached value — the original read timestamp is preserved (so the card is not re-branded as fresh), but the Redis TTL does get extended so the card does not disappear entirely. Any card whose original timestamp is older than about a day is then rendered with a "stale" badge in the UI.
Each price feed goes through a resolver that checks (a) the upstream call succeeded, (b) the expected field exists on the response, and (c) the number is positive. SGE also rejects a known sentinel value of -2.0 that its endpoint returns on non-trading days. There is no runtime JSON-schema validation layer beyond these field-level checks. If any check fails, the resolver returns null and the layer above attempts the fallback in this order:
Key rule: values are never silently rebranded as fresh. Prices computed from the benchmark plus FX (rather than directly from a market feed) are stored with a "derived" source flag from the outset and the UI badges them as such, and any card whose data has not refreshed within about a day gets a "stale" badge with the original read timestamp.
The USD spot benchmark, MCX-related field, SGE feed, and LBMA-related field do not necessarily refresh on the same calendar or at the same time. On weekends and holidays, a source may return its latest close or a sentinel value. We retain the original timestamp and show stale status rather than presenting the value as real-time.
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.
The premium we publish is a wholesale, pre-tax comparison. Country-level taxes are not added to the displayed premium. Examples include: India's concessional gold import duty (5% BCD + 1% AIDC = 6% per CBIC since the July 2024 Union Budget, with 3% GST applied separately at the retail stage and effective rates that depend on the import form, importer and taxable value — always verify against the current CBIC schedule); the UK's 20% VAT on silver; and China's 13% VAT on non-SGE gold. Readers reasoning about retail prices should consult the tax comparison page and confirm the current rate with the local tax authority.
Any change to a feed, a conversion factor, or the formula above is treated as a methodology change. When one ships this page and the change log below are updated in the same release. Historical snapshots are stored per date per market in Redis with a 90-day TTL; if the daily cron writes a new snapshot for a date that already exists, the previous point is replaced. Long-lived corrections and inflection points that matter to the chart's interpretation are documented in the change log entries below rather than annotated on individual data points.
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.