Tutorial/Gold + FX integration
Tutorial

How to calculate gold prices in local currencies

Convert a USD gold quote into a local-currency price per gram without hiding the two market observations. Use one goldprice.dev request for convenience, or combine goldprice.dev with exchangerate.dev when you need separate source and freshness metadata.

MMexchangerate.dev·Jul 13, 2026·7 min read

A local gold price has two moving inputs: the USD gold price and the USD-to-local exchange rate. The core formula is local_per_gram = usd_per_troy_ounce / 31.1035 * usd_to_local_rate. Keep the gold and FX observation times separate so a fresh response cannot disguise stale market data.

Key points
One troy ounce is 31.1035 grams; keep full precision until display formatting.
Use goldprice.dev /v1/convert when you only need the final converted value.
Fetch goldprice.dev spot and exchangerate.dev latest rates separately when you 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 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.

Option 1: convert gold in one request

The quickest implementation is goldprice.dev `/v1/convert`. It combines live gold and FX data server-side 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);

Choose this path when your interface only needs the final value. Its single timestamp follows the gold spot leg and does not independently evidence FX freshness; use the two-leg path when either input's provenance matters.

Option 2: keep the gold and FX legs separate

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().

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.

MM
exchangerate.dev
Integration guides for developers.

Keep reading

ReferenceReading source and market_sessionRead TutorialCurrency conversion in JavaScript and Node.jsRead GuideIndicative vs executable FX ratesRead