How to get live gold prices in Next.js
Fetch live gold prices in a Next.js Server Component with server-only, cached fetch, and Decimal-safe price handling.
Updated
To get live gold prices in Next.js, call api.goldprice.dev/v1/spot/XAU-USD-SPOT from a Server Component with Authorization: Bearer, guarded by import "server-only" so the key never reaches the client bundle. Use a cached fetch with revalidate: 3600 for free-tier-safe polling, and keep prices as strings, never floats. The free tier covers 1,000 calls/month. Tested July 2026 against Next.js 16.
1.Create the app and set your API key
Scaffold a Next.js app (App Router) if you do not have one:
npx create-next-app@latest gold-demoSign up for a free account at goldprice.dev/onboarding. After email confirmation, your dashboard shows a key starting with
ga_live_. Add it to.env.localasGOLDPRICE_API_KEY=ga_live_replace_with_your_key_here— noNEXT_PUBLIC_prefix, so Next.js never ships it to the browser. The free tier includes 1,000 calls/month, no credit card.Get your free API key1,000 calls/mo, no credit cardSign up free →2.Write the data function
Save this as
lib/goldprice.ts. Theimport "server-only"line makes the file throw at build time if a Client Component ever imports it, so the key can never leak into client JavaScript. The function checks the response status and the fields it needs before returning.TYPESCRIPT · lib/goldprice.tsimport "server-only"; const API_BASE = "https://api.goldprice.dev"; export interface SpotPrice { symbol: string; quoteCurrency: string; price: string; bid: string | null; ask: string | null; isStale: boolean; computedAt: string | null; } /** Fetch the live spot price for one symbol, e.g. "XAU-USD-SPOT". */ export async function getSpotPrice(symbol = "XAU-USD-SPOT"): Promise<SpotPrice> { const apiKey = process.env.GOLDPRICE_API_KEY; if (!apiKey) { throw new Error("GOLDPRICE_API_KEY is not set"); } const res = await fetch(`${API_BASE}/v1/spot/${symbol}`, { headers: { Authorization: `Bearer ${apiKey}` }, next: { revalidate: 3600 }, }); if (res.status === 401) { throw new Error("goldprice: invalid API key"); } if (res.status === 429) { const retryAfter = res.headers.get("Retry-After"); throw new Error(`goldprice: rate limited, retry after ${retryAfter ?? "60"}s`); } if (!res.ok) { throw new Error(`goldprice: unexpected status ${res.status}`); } const raw = await res.json(); if (typeof raw.symbol !== "string" || typeof raw.price !== "string") { throw new Error("goldprice: unexpected response shape"); } return { symbol: raw.symbol, quoteCurrency: raw.quote_currency, price: raw.price, bid: raw.bid ?? null, ask: raw.ask ?? null, isStale: Boolean(raw.is_stale), computedAt: raw.computed_at ?? null, }; }3.Call it from a Server Component
app/page.tsxcallsgetSpotPrice()directly — no Route Handler in between. Next.js' own guidance on backend-for-frontend patterns is that a Route Handler that just forwards to an external API for a Server Component to then fetch is an unnecessary round trip; the Server Component can call the data function itself, server-side, in the same request.TYPESCRIPT · app/page.tsximport { getSpotPrice } from "@/lib/goldprice"; export default async function Page() { const spot = await getSpotPrice("XAU-USD-SPOT"); return ( <main> <h1> {spot.symbol}/{spot.quoteCurrency}: {spot.price} </h1> <p>Updated: {spot.computedAt}</p> </main> ); }4.Set caching for your call volume
The
next: { revalidate: 3600 }option above caches the fetch for one hour. At 24 refreshes/day that is ~720 calls/month, inside the 1,000-call free tier with headroom for local dev and preview deploys. Paid tiers with a higher monthly quota can drop torevalidate: 60for near-live prices. Pick the number from your quota, not the other way around.TYPESCRIPT · lib/goldprice.ts · revalidate// Free-tier-safe default: ~720 calls/month next: { revalidate: 3600 } // Paid tier with quota to spare: near-live next: { revalidate: 60 }5.Handle failure without crashing the page
Wrap the call so a missing key, a 401, a 429, or a network timeout renders a fallback instead of a 500. Serve the last cached value when you have one —
fetch's cache still holds the previous response even after a failed revalidation.TYPESCRIPT · app/page.tsx · with fallbackimport { getSpotPrice } from "@/lib/goldprice"; export default async function Page() { let spot; try { spot = await getSpotPrice("XAU-USD-SPOT"); } catch (err) { console.error(err); return <p>Gold price is temporarily unavailable. Try again shortly.</p>; } return ( <main> <h1> {spot.symbol}/{spot.quoteCurrency}: {spot.price} </h1> </main> ); }
Expected output
The API returns this shape:
{
"symbol": "XAU",
"quote_currency": "USD",
"unit": "troy_ounce",
"contract_type": "spot",
"price": "4726.01",
"bid": "4726.68",
"ask": "4725.33",
"is_stale": false,
"computed_at": "2026-07-16T04:47:24.000000+00:00"
}The rendered page shows an <h1> with the symbol pair and the price string, and an "Updated" line with the ISO 8601 computedAt timestamp. Both come straight from the SpotPrice object getSpotPrice() returns.
Common errors
| Code | Symptom | Fix |
|---|---|---|
| 401 | goldprice: invalid API key thrown from getSpotPrice() | The key is missing, wrong, or revoked. Confirm GOLDPRICE_API_KEY is set in .env.local (or your host's environment variables in production) and matches the dashboard. Restart the dev server after editing .env.local — Next.js reads env files at boot. |
| 429 | goldprice: rate limited, retry after Ns | You exceeded the per-minute or per-month cap. Honor the Retry-After header before retrying. Raising revalidate (fewer refreshes per hour) is usually the fix for steady traffic; for sustained high-volume use, see /pricing for Physical and Pro tiers. |
| N/A | Fetch hangs on a slow network and the page never renders | Pass an AbortSignal.timeout(5000) in the fetch options so a stalled request fails fast instead of blocking the Server Component render. Catch the resulting AbortError alongside the other errors and fall back to the same graceful message. |
| N/A | Price is set with NEXT_PUBLIC_GOLDPRICE_API_KEY by mistake | Any env var prefixed NEXT_PUBLIC_ is bundled into client JavaScript and visible to anyone who views source. Keep the key as plain GOLDPRICE_API_KEY, read it only inside files marked import "server-only", and never pass it as a prop to a Client Component. |
FAQ
Will this fit the free tier with hourly refresh?
Yes. revalidate: 3600 refreshes once an hour — 24 calls/day, ~720/month, inside the 1,000-call free tier with room to spare for local dev and preview builds. Drop to revalidate: 60 only once you are on a tier with the quota to support it.
Do I need a Route Handler in front of the data function?
No. A Server Component can call getSpotPrice() directly during render — that is the whole point of Server Components. Next.js' backend-for-frontend guidance treats a Route Handler that only proxies to an external API as an extra network hop with no benefit; add one only if a Client Component also needs the same data.
My project uses cacheComponents — does revalidate still work?
With cacheComponents: true, wrap the cached read in a function marked "use cache" and set the cache lifetime with cacheLife() instead of the fetch-level revalidate option. The underlying goal is the same — bound how often you call the API — but the API surface differs between the two caching models; see the Next.js caching guide linked below.
Can I use this commercially?
The Free tier is for personal use. For commercial use (apps you ship, services you sell, internal production systems), upgrade to Pro ($30/mo) — monthly billing, cancel anytime, no annual lock-in. See /pricing.
Why keep price as a string instead of a number?
The API returns price fields as strings precisely so clients don't round-trip them through a binary float. JavaScript's Number is a 64-bit float, so 0.1 + 0.2 === 0.30000000000000004 — fine for display, wrong for money math. Keep the string (or parse it into a Decimal/decimal.js value) through any arithmetic and only convert to a display number at the point you render it.
Can I have Claude write this for me?
Yes. Open Claude Desktop with the goldprice.dev MCP server installed and ask: Build a Next.js Server Component that fetches the goldprice.dev /v1/spot endpoint. Use a server-only data function, Authorization: Bearer, revalidate: 3600, and a graceful fallback on 401/429/timeout. The MCP server gives Claude the live schema, so generated code uses correct field names on first try.
What metals and currencies are supported?
Gold (XAU) spot is on every tier, including Free. Silver (XAG) and copper (HG) spot need Pro. Platinum and palladium are not offered on any tier. Symbols are METAL-CURRENCY-SPOT, e.g. XAU-EUR-SPOT, across 31 supported currencies. Full details in the API reference.
Going further
- Add a Suspense boundary around the price component so the rest of the page streams in before the fetch resolves
- Build a bars() wrapper for /v1/bars?symbol=XAU-USD-SPOT&interval=1d&from=...&to=... and render a price chart from a Server Component
- Move the API key check into a startup assertion (e.g. instrumentation.ts) so a missing env var fails the build instead of the first render
- Add an on-demand revalidation route guarded by a shared secret for cases where you want to bust the cache outside the revalidate window
- Log cache hit/miss and 429 counts from the data function to your existing observability stack
Next steps
Try the same setup in a different platform: