How to get live gold prices in .NET

Call the goldprice.dev API from C#/.NET with HttpClient, System.Text.Json, retry-on-429, and a 60-second IMemoryCache.

Updated

To get live gold prices in C#/.NET, send a GET to api.goldprice.dev/v1/spot/XAU-USD-SPOT with your API key in the X-API-Key header using HttpClient. Deserialize the JSON with System.Text.Json into a record with decimal fields, and cache the result for 60 seconds. The free tier covers 1,000 calls/month, no credit card. Tested July 2026 against .NET 8.

  1. 1.Get your goldprice.dev API key

    Sign up for a free account at goldprice.dev/onboarding. After email confirmation, your dashboard shows a key starting with ga_live_. Set it as an environment variable:

    export GOLDPRICE_API_KEY=ga_live_replace_with_your_key_here

    The free tier includes 1,000 calls/month and 31 supported currencies. No credit card required.

    Get your free API key1,000 calls/mo, no credit card
    Sign up free →
  2. 2.Create the project

    The .NET base class library covers everything needed: HttpClient for the request and System.Text.Json for deserialization. No third-party package is required, so scaffold a plain console app:

    BASH · shell
    dotnet new console -o GoldpriceDemo
    cd GoldpriceDemo
  3. 3.Write the goldprice client

    Save this as GoldpriceClient.cs. SpotAsync() caches per symbol for 60 seconds in a ConcurrentDictionary, retries up to 3 times on 429 with exponential backoff, and deserializes prices into decimal fields via a custom JsonConverter — the API returns prices as JSON strings so precision survives the wire.

    CSHARP · GoldpriceClient.cs
    // GoldpriceClient.cs
    // curl-equivalent: https://api.goldprice.dev/v1/spot/XAU-USD-SPOT
    using System.Collections.Concurrent;
    using System.Net.Http.Json;
    using System.Text.Json;
    using System.Text.Json.Serialization;
    
    namespace GoldpriceDemo;
    
    /// Converts the API's string-encoded price fields to decimal without
    /// the precision loss a double round-trip would introduce.
    public sealed class DecimalStringConverter : JsonConverter<decimal>
    {
        public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            => decimal.Parse(reader.GetString()!);
    
        public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
            => writer.WriteStringValue(value.ToString());
    }
    
    public sealed record SpotResponse(
        [property: JsonPropertyName("symbol")] string Symbol,
        [property: JsonPropertyName("quote_currency")] string QuoteCurrency,
        [property: JsonPropertyName("price"), JsonConverter(typeof(DecimalStringConverter))] decimal Price,
        [property: JsonPropertyName("bid"), JsonConverter(typeof(DecimalStringConverter))] decimal? Bid,
        [property: JsonPropertyName("ask"), JsonConverter(typeof(DecimalStringConverter))] decimal? Ask,
        [property: JsonPropertyName("is_stale")] bool IsStale,
        [property: JsonPropertyName("computed_at")] string ComputedAt
    );
    
    public sealed class GoldpriceRateLimitedException : Exception
    {
        public GoldpriceRateLimitedException() : base("goldprice: 429 rate limited") { }
    }
    
    public sealed class GoldpriceClient
    {
        private const string ApiBase = "https://api.goldprice.dev";
        private static readonly TimeSpan Ttl = TimeSpan.FromSeconds(60);
    
        private readonly HttpClient _http;
        private readonly ConcurrentDictionary<string, (SpotResponse Data, DateTimeOffset Expires)> _cache = new();
    
        public GoldpriceClient(HttpClient http)
        {
            var apiKey = Environment.GetEnvironmentVariable("GOLDPRICE_API_KEY")
                ?? throw new InvalidOperationException("GOLDPRICE_API_KEY not set");
            _http = http;
            _http.BaseAddress = new Uri(ApiBase);
            _http.Timeout = TimeSpan.FromSeconds(5);
            _http.DefaultRequestHeaders.Add("X-API-Key", apiKey);
        }
    
        /// Fetches the live spot price. Cached 60s per symbol, retries 3x on 429.
        public async Task<SpotResponse> SpotAsync(string symbol = "XAU-USD-SPOT", CancellationToken ct = default)
        {
            if (_cache.TryGetValue(symbol, out var cached) && cached.Expires > DateTimeOffset.UtcNow)
            {
                return cached.Data;
            }
    
            for (var attempt = 0; attempt < 3; attempt++)
            {
                using var response = await _http.GetAsync($"/v1/spot/{symbol}", ct);
    
                if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
                {
                    await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), ct); // 1s, 2s, 4s
                    continue;
                }
                if (!response.IsSuccessStatusCode)
                {
                    throw new HttpRequestException($"goldprice: unexpected status {(int)response.StatusCode}");
                }
    
                var data = await response.Content.ReadFromJsonAsync<SpotResponse>(cancellationToken: ct)
                    ?? throw new JsonException("goldprice: empty response body");
                _cache[symbol] = (data, DateTimeOffset.UtcNow.Add(Ttl));
                return data;
            }
    
            throw new GoldpriceRateLimitedException();
        }
    }
  4. 4.Run it

    From the same shell where you exported GOLDPRICE_API_KEY, add a Program.cs that constructs the client and calls SpotAsync(). The first call hits the API; calls within 60 seconds return the cached record without a round-trip. A fresh process starts with an empty cache (in-process only) — back it with a distributed cache such as Redis for cross-process sharing.

    CSHARP · Program.cs
    using GoldpriceDemo;
    
    using var http = new HttpClient();
    var client = new GoldpriceClient(http);
    
    var spot = await client.SpotAsync("XAU-USD-SPOT");
    Console.WriteLine($"{spot.Symbol}-{spot.QuoteCurrency}: {spot.Price} (computed {spot.ComputedAt})");
    // XAU-USD: 4726.01 (computed 2026-07-06T04:49:01.706844+00:00)
  5. 5.Switch currency or metal

    Symbols are METAL-CURRENCY-SPOT. Pass any of the 31 supported currencies to SpotAsync(). Each unique symbol caches independently in the ConcurrentDictionary, so a dashboard polling 5 symbols at 60-second intervals fires 5 uncached calls per minute and 0 cached.

    CSHARP · Program.cs · multi-currency
    // USD spot — default
    await client.SpotAsync("XAU-USD-SPOT");
    
    // INR — Indian gold market (largest retail gold-search globally)
    await client.SpotAsync("XAU-INR-SPOT");
    
    // EUR — European pricing surfaces
    await client.SpotAsync("XAU-EUR-SPOT");
    
    // Silver in USD — Pro tier (Free/Basic are gold-only)
    await client.SpotAsync("XAG-USD-SPOT");

Expected output

The API returns this shape:

JSON · GET /v1/spot/XAU-USD-SPOT
{
  "symbol": "XAU",
  "quote_currency": "USD",
  "unit": "troy_ounce",
  "contract_type": "spot",
  "price": "4726.01",
  "bid": "4726.68",
  "ask": "4725.33",
  "is_stale": false,
  "divergence_flag": false,
  "computed_at": "2026-07-06T04:49:01.706844+00:00",
  "sources": [
    {
      "source": "wgc.fsapi.usd",
      "display_name": "Continuous spot reference (live spot)",
      "price": "4726.01",
      "is_stale": false,
      "timestamp": "2026-07-06T04:47:24+00:00"
    }
    /* + cmc.paxg + cmc.xaut entries — full sources[] in live response */
  ],
  "value_stale": false,
  "price_gram_24k": "151.9447",
  "open_price": "4681.305",
  "high_price": "4729.56",
  "low_price": "4672.927",
  "prev_close_price": "4681.302",
  "ch": "44.6260",
  "chp": "0.9533",
  "open_time": 1783382400
}

Stdout shows one line: symbol, quote currency, the price as a decimal, and the ISO 8601 ComputedAt timestamp. The SpotResponse record is what your downstream code consumes.

Common errors

CodeSymptomFix
401goldprice: unexpected status 401 on the first callAPI key missing or invalid. Confirm the variable is set: echo $GOLDPRICE_API_KEY. If empty, re-export from your dashboard. The GoldpriceClient constructor throws immediately when the env var is empty, so the failure surfaces at startup rather than mid-request.
400goldprice: unexpected status 400, response body has "error": "invalid_symbol"The symbol does not match BASE-QUOTE-CONTRACT (e.g. XAU-USD-SPOT). A bare metal code like XAU is rejected — always include the quote currency and contract token, and use SPOT or FUTURES, never FUT.
429Three retries with backoff all return 429, then SpotAsync throws GoldpriceRateLimitedExceptionYou hit the per-minute cap. The 60-second cache prevents this for steady polling; if a burst of distinct symbols arrives at once, stagger the calls. For sustained high-volume use, see /pricing for Basic and Pro tiers.
N/ADeserialization throws JsonException or prices come through as 0The API sends prices as JSON strings (e.g. "4726.01"), not numbers. A plain decimal property without [JsonConverter(typeof(DecimalStringConverter))] fails to bind from a JSON string. Keep the converter attribute on every price field, or register it globally via JsonSerializerOptions.Converters.

FAQ

How often does the price update?

The spot endpoint refreshes every 60 seconds (live oracle + continuous spot reference + futures settlement aggregation). The 60-second TTL above matches that cadence — repeated SpotAsync() calls within the window return the cached record without an HTTP round-trip.

Will this fit the free tier with auto-refresh?

It depends on your call site. A service polling once per minute = 1,440 calls/day = ~43,000/month — well over the 1,000/month free tier. Cache aggressively and call only on demand. Polling once per hour = ~720/month, inside the free tier. For continuous high-volume use, see /pricing for Basic and Pro tiers.

Can I use this commercially?

Free and Basic are for personal and internal use — dashboards, bots, and backtests, not products you monetize. Commercial use starts at Pro ($30/mo), which permits it with attribution; Realtime covers higher-volume commercial workloads. Monthly billing, cancel anytime. See /pricing.

Why a custom JsonConverter instead of a plain decimal property?

System.Text.Json does not coerce a JSON string into a numeric property by default, and the API returns prices as strings precisely so clients decode them exactly instead of through a lossy double parse. DecimalStringConverter reads the string and hands back a decimal, .NET's base-10 exact type, so gold prices with 6+ decimal places keep their precision through arithmetic.

Is GoldpriceClient safe to use as a singleton with dependency injection?

Yes. Register it with IHttpClientFactory (services.AddHttpClient<GoldpriceClient>() in ASP.NET Core) so the underlying HttpClient is pooled correctly, and register GoldpriceClient itself as a singleton — the ConcurrentDictionary cache is thread-safe, so one instance can serve concurrent requests across a web app.

What metals are supported?

Gold (XAU) spot is on every tier, with gold futures on Basic and up. Silver (XAG) spot and futures, plus copper (HG) futures settlement, are on Pro — e.g. client.SpotAsync("XAG-USD-SPOT") for silver spot on Pro. Platinum and palladium aren't offered.

Can I have Claude write this for me?

Yes. Open Claude Desktop with the goldprice.dev MCP server installed and ask: Build a C#/.NET client for the goldprice.dev /v1/spot endpoint. Use HttpClient, System.Text.Json with a decimal converter for string-encoded prices, a 60-second ConcurrentDictionary cache, and retry-on-429 with exponential backoff. Read the API key from GOLDPRICE_API_KEY. The MCP server gives Claude the live schema, so generated code uses correct field names on first try.

Going further

  • Swap the ConcurrentDictionary for IMemoryCache or a Redis-backed IDistributedCache so an ASP.NET Core app shares one warm price across requests
  • Add a BarsAsync() method for /v1/bars?symbol=XAU-USD-SPOT&interval=1d&from=...&to=... and feed the series into a backtest
  • Register GoldpriceClient with IHttpClientFactory and Polly for a policy-driven retry/circuit-breaker instead of the hand-rolled backoff loop
  • Wire the client into a minimal API endpoint and serve internally for other services on the network
  • Add OpenTelemetry instrumentation for cache hit rate, request latency, and 429-retry counts

Next steps

Try the same setup in a different platform:

Browse all 10 tutorials →