Case Study/Production case study

How goldprice.dev prices gold in local currencies

How goldprice.dev combines USD metal prices with exchangerate.dev FX rates to serve local-currency gold prices without hiding freshness boundaries.

ERexchangerate.dev·Jul 16, 2026·7 min read

goldprice.dev and exchangerate.dev are sibling products operated by the same company. In production, goldprice.dev owns the metal-price contract while exchangerate.dev supplies the FX leg for supported floating currencies. The boundary matters: a local gold price contains two market observations, and one fresh leg cannot make the other fresh.

Key points
goldprice.dev owns the metal price and public conversion response; exchangerate.dev supplies the FX leg.
The public goldprice.dev /v1/convert endpoint provides the one-call production path.
Developers can fetch both APIs separately when they need an auditable data lineage.
Store the gold computed_at and FX data_updated_at, timestamp, source, and market_session independently.
Converted prices are indicative market values, not executable dealer or settlement quotes.

The production boundary

goldprice.dev starts with a USD metal observation. exchangerate.dev provides the corresponding USD-to-local FX observation for supported floating currencies. goldprice.dev then owns the metal-specific conversion contract, including units, supported symbols, validation, and the final response.

This is a disclosed sibling integration, not an independent customer endorsement. Its value as a case study is operational: the FX API supplies a real production data leg, and the consuming product keeps responsibility for its own domain.

The conversion formula

Gold is commonly quoted as USD per troy ounce. A local display usually needs a currency amount per gram. Convert the weight first, then apply the USD exchange rate:

formula · USD/oz to local currency/gramcopy
const GRAMS_PER_TROY_OZ = 31.1035;

function localGoldPerGram(usdPerTroyOz, usdToLocalRate) {
  return (usdPerTroyOz / GRAMS_PER_TROY_OZ) * usdToLocalRate;
}

The weight constant is fixed. The two market inputs are not: both the gold quote and the FX rate have their own observation time and freshness rules.

The one-call production path

For callers, the production boundary is goldprice.dev `/v1/convert`. It combines the metal and FX legs behind one metal-aware endpoint and does not require an API key. This example requests the value of one gram of gold in Indonesian rupiah:

javascript · one-request conversioncopy
const params = new URLSearchParams({
  from: "XAU",
  to: "IDR",
  amount: "1",
  unit: "gram",
});

const response = await fetch(
  `https://api.goldprice.dev/v1/convert?${params}`,
);

if (!response.ok) {
  throw new Error(`goldprice.dev returned ${response.status}`);
}

const conversion = await response.json();
console.log(conversion.result, conversion.to, conversion.timestamp);

This path keeps the caller contract small: one request, one amount, one metal unit, and one target currency. Its single timestamp does not independently evidence both market legs, so consumers that must audit FX freshness should use the two-leg path below.

The auditable two-leg integration

Fetch both APIs when you need to retain each source timestamp, cache the inputs independently, or explain which leg is stale. goldprice.dev spot supplies USD gold per troy ounce; exchangerate.dev supplies the USD-to-local rate.

javascript · explicit gold and FX legscopy
const GRAMS_PER_TROY_OZ = 31.1035;

async function getGoldInCurrencies(currencies) {
  const symbols = currencies.join(",");
  const [spotResponse, fxResponse] = await Promise.all([
    fetch("https://api.goldprice.dev/v1/spot/XAU-USD-SPOT"),
    fetch(`https://api.exchangerate.dev/v1/latest/USD?symbols=${symbols}`),
  ]);

  if (!spotResponse.ok || !fxResponse.ok) {
    throw new Error("A market-data request failed");
  }

  const spot = await spotResponse.json();
  const fx = await fxResponse.json();
  const usdPerGram = Number(spot.price) / GRAMS_PER_TROY_OZ;

  return {
    goldComputedAt: spot.computed_at,
    fxDataUpdatedAt: fx.data_updated_at,
    fxResponseTimestamp: fx.timestamp,
    fxSource: fx.source,
    fxMarketSession: fx.market_session,
    prices: Object.fromEntries(
      currencies.map((currency) => [
        currency,
        usdPerGram * Number(fx.rates[currency]),
      ]),
    ),
  };
}

const result = await getGoldInCurrencies(["EUR", "IDR", "JPY"]);
A response timestamp is not a market timestamp
Preserve spot.computed_at for the gold observation and fx.data_updated_at for the oldest contributing FX observation. The FX timestamp is when the response was built; source and market_session add data-class and session context. Do not replace these fields with Date.now().

Why the services remain separate

The integration does not turn exchangerate.dev into the whole gold-pricing engine. It remains the FX layer. Keeping that boundary explicit prevents currency freshness, metal freshness, and product-specific conversion rules from collapsing into one ambiguous value.

  • goldprice.dev can expose a small, metal-specific interface without making callers assemble the common path themselves.
  • exchangerate.dev keeps a general FX contract that is useful outside the metals domain.
  • Audit-sensitive consumers can retrieve and store both observations independently.
  • Each service can report failures and freshness using the vocabulary of its own market data.

Handle stale or missing legs explicitly

A combined price should not survive if either input is missing, zero, non-numeric, or outside your product's accepted freshness window. Keep the last valid value only if the interface labels it with its original observation times.

  • Check both HTTP responses before doing arithmetic.
  • Reject non-finite or non-positive prices and rates.
  • Set separate freshness thresholds for gold and FX.
  • Treat market_session: weekend as context, not as proof that a carried rate is current.
  • Log the currency pair and both underlying observation times with conversion errors.

Round for display, not during calculation

Keep full precision through the multiplication and let the display locale decide the final number of decimal places. Intl.NumberFormat handles currency symbols and grouping without changing the underlying value:

javascript · locale-aware displaycopy
function formatPrice(amount, currency, locale) {
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
  }).format(amount);
}

console.log(formatPrice(result.prices.IDR, "IDR", "id-ID"));
console.log(formatPrice(result.prices.JPY, "JPY", "ja-JP"));

Use the result as an indicative value

A calculated market value is not the retail price of a coin or bar. Dealer premiums, taxes, fabrication, payment fees, and executable FX spreads are outside this formula. Label the output as indicative unless your application adds a real dealer quote and its commercial terms.

What this case study proves

goldprice.dev is first-party evidence that exchangerate.dev can supply the FX leg of a production pricing workflow. It is not independent social proof. The useful proof is narrower and reproducible: developers can call the same public products, preserve both observation times, and verify the arithmetic themselves. The published 31-currency gold study applies that method to every supported currency and shows the dated result table.

ER
exchangerate.dev
Production integration notes for developers.

Keep reading

ReferenceReading source and market_sessionRead TutorialCurrency conversion in JavaScript and Node.jsRead GuideIndicative vs executable FX ratesRead
More ComparisonsFixer vs exchangerate.devOpen Exchange Rates vs exchangerate.devCurrencylayer vs exchangerate.devCurrencylayer vs ExchangeRate-API
LearnReading source and market_session in your pipelineIndicative vs executable FX rates: what a rates API actually gives youECB reference rates, explained
Live RatesEUR/USDGBP/USDUSD/JPY