How to get live gold prices in Go

Call the goldprice.dev API from Go with net/http, shopspring/decimal, retry-on-429, and a 60-second TTL cache.

Updated

To get live gold prices in Go, send a GET to api.goldprice.dev/v1/spot/XAU-USD-SPOT with your API key in the X-API-Key header using net/http. Decode the JSON into a struct, parse prices with shopspring/decimal for exact math, and cache per symbol for 60 seconds. The free tier covers 1,000 calls/month, no credit card. Tested June 2026 against Go 1.23.

  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 goldprice_live_. Set it as an environment variable:

    export GOLDPRICE_API_KEY=goldprice_live_replace_with_your_key_here

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

    Get your free API key1,000 calls/mo, no credit card
    Sign up free →
  2. 2.Initialise the module and add the decimal dependency

    The standard library covers the HTTP call and JSON decode. The one external package is shopspring/decimal, so price math stays exact instead of drifting on float64. Create a module and add it:

    BASH · shell
    go mod init goldprice-demo
    go get github.com/shopspring/decimal
  3. 3.Write the goldprice client

    Save this as goldprice.go. The Spot() method caches per symbol for 60 seconds behind a mutex, retries up to 3 times on 429 with exponential backoff, and decodes prices straight into decimal.Decimal (its UnmarshalJSON accepts the string prices the API returns).

    GO · goldprice.go
    // go: goldprice.go
    // curl-equivalent: https://api.goldprice.dev/v1/spot/XAU-USD-SPOT
    package main
    
    import (
    	"encoding/json"
    	"errors"
    	"fmt"
    	"net/http"
    	"os"
    	"sync"
    	"time"
    
    	"github.com/shopspring/decimal"
    )
    
    const (
    	apiBase = "https://api.goldprice.dev"
    	timeout = 5 * time.Second
    	ttl     = 60 * time.Second
    )
    
    // SpotResponse holds the fields we consume from /v1/spot/{symbol}.
    type SpotResponse struct {
    	Symbol        string          `json:"symbol"`
    	QuoteCurrency string          `json:"quote_currency"`
    	Price         decimal.Decimal `json:"price"`
    	Bid           decimal.Decimal `json:"bid"`
    	Ask           decimal.Decimal `json:"ask"`
    	IsStale       bool            `json:"is_stale"`
    	ComputedAt    string          `json:"computed_at"`
    }
    
    var errRateLimited = errors.New("goldprice: 429 rate limited")
    
    type cacheEntry struct {
    	resp    SpotResponse
    	expires time.Time
    }
    
    // Client is safe for concurrent use.
    type Client struct {
    	apiKey string
    	http   *http.Client
    	mu     sync.Mutex
    	cache  map[string]cacheEntry
    }
    
    func NewClient() *Client {
    	key := os.Getenv("GOLDPRICE_API_KEY")
    	if key == "" {
    		panic("GOLDPRICE_API_KEY not set")
    	}
    	return &Client{
    		apiKey: key,
    		http:   &http.Client{Timeout: timeout},
    		cache:  make(map[string]cacheEntry),
    	}
    }
    
    // Spot fetches the live spot price. Cached 60s per symbol, retries 3x on 429.
    func (c *Client) Spot(symbol string) (SpotResponse, error) {
    	c.mu.Lock()
    	if e, ok := c.cache[symbol]; ok && time.Now().Before(e.expires) {
    		c.mu.Unlock()
    		return e.resp, nil
    	}
    	c.mu.Unlock()
    
    	var out SpotResponse
    	var err error
    	for attempt := 0; attempt < 3; attempt++ {
    		out, err = c.fetch(symbol)
    		if err == nil {
    			c.mu.Lock()
    			c.cache[symbol] = cacheEntry{resp: out, expires: time.Now().Add(ttl)}
    			c.mu.Unlock()
    			return out, nil
    		}
    		if !errors.Is(err, errRateLimited) {
    			return SpotResponse{}, err
    		}
    		time.Sleep(time.Duration(1<<attempt) * time.Second) // 1s, 2s, 4s
    	}
    	return SpotResponse{}, err
    }
    
    func (c *Client) fetch(symbol string) (SpotResponse, error) {
    	req, err := http.NewRequest(http.MethodGet, apiBase+"/v1/spot/"+symbol, nil)
    	if err != nil {
    		return SpotResponse{}, err
    	}
    	req.Header.Set("X-API-Key", c.apiKey)
    
    	resp, err := c.http.Do(req)
    	if err != nil {
    		return SpotResponse{}, err
    	}
    	defer resp.Body.Close()
    
    	if resp.StatusCode == http.StatusTooManyRequests {
    		return SpotResponse{}, errRateLimited
    	}
    	if resp.StatusCode != http.StatusOK {
    		return SpotResponse{}, fmt.Errorf("goldprice: unexpected status %d", resp.StatusCode)
    	}
    
    	var out SpotResponse
    	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
    		return SpotResponse{}, err
    	}
    	return out, nil
    }
    
    func main() {
    	c := NewClient()
    	r, err := c.Spot("XAU-USD-SPOT")
    	if err != nil {
    		panic(err)
    	}
    	fmt.Printf("%s-%s: %s (computed %s)\n", r.Symbol, r.QuoteCurrency, r.Price, r.ComputedAt)
    }
  4. 4.Run it

    From the same shell where you exported GOLDPRICE_API_KEY, run the file. The first call hits the API; calls within 60 seconds return the cached value without a round-trip. A fresh process starts with an empty cache (in-process only) — back it with Redis if you need cross-process sharing.

    BASH · shell
    go run goldprice.go
    # XAU-USD: 4726.01 (computed 2026-06-26T04:49:01.706844+00:00)
  5. 5.Switch currency or metal

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

    GO · goldprice.go · multi-currency
    c := NewClient()
    
    // USD spot — default
    c.Spot("XAU-USD-SPOT")
    
    // INR — Indian gold market (largest retail gold-search globally)
    c.Spot("XAU-INR-SPOT")
    
    // EUR — European pricing surfaces
    c.Spot("XAU-EUR-SPOT")
    
    // Silver in USD
    c.Spot("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-06-26T04: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-06-26T04: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": 1782259200
}

Stdout shows one line: symbol, quote currency, the price as a decimal.Decimal, and the ISO 8601 ComputedAt timestamp. The SpotResponse struct 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. NewClient() panics at construction when the env var is empty, so the failure surfaces immediately rather than mid-request.
429Three retries with backoff all return 429, then Spot returns the rate-limit errorYou 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 or add a golang.org/x/sync/singleflight group to collapse duplicate in-flight requests. For sustained volume, see /pricing for Basic and Pro tiers.
N/Aprice decodes as 0 or the build complains about the decimal typeThe API sends prices as JSON strings (e.g. "4726.01"). decimal.Decimal from shopspring/decimal unmarshals strings correctly; a plain float64 field would lose precision and a custom int type would fail to decode. Keep the field typed as decimal.Decimal and run go mod tidy if the import is unresolved.

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 Spot() calls within the window return the cached struct 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?

The Free tier is for personal use. For commercial use (apps you ship, services you sell, internal production systems), upgrade to Basic or Pro — see /pricing.

Why decimal.Decimal instead of float64?

Float arithmetic drifts on summation: 0.1 + 0.2 is 0.30000000000000004 in IEEE-754. Gold prices carry 6+ decimal places; summing thousands of values in float64 introduces basis-point errors over time. decimal.Decimal keeps exact arithmetic, and the API returns prices as strings precisely so clients can decode them without loss.

Is the client safe for concurrent goroutines?

Yes. The cache map is guarded by a sync.Mutex, so multiple goroutines can call Spot() on the same Client. For high concurrency on the same cold symbol, add a singleflight.Group so only one goroutine performs the fetch while the others wait for its result.

What metals are supported?

Gold (XAU), silver (XAG), copper (HG), platinum (XPT), palladium (XPD). Substitute the metal code in the symbol, e.g. c.Spot("XAG-USD-SPOT").

Can I have Claude write this for me?

Yes. Open Claude Desktop with the goldprice.dev MCP server installed and ask: Build a Go client for the goldprice.dev /v1/spot endpoint. Use net/http, shopspring/decimal, a 60-second per-symbol cache behind a mutex, 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 and tags on the first try.

Going further

  • Add a context.Context parameter to Spot() and use http.NewRequestWithContext so callers can set deadlines and cancellation
  • Collapse duplicate in-flight requests with golang.org/x/sync/singleflight so a thundering herd on one symbol makes a single API call
  • Add a History() method for /v1/history/XAU-USD-SPOT?days=30 and feed the rows into a backtest
  • Back the cache with Redis so a fleet of pods shares one warm price instead of each holding its own
  • Expose Prometheus counters for cache hit rate, request latency, and 429-retry totals

Next steps

Try the same setup in a different platform:

Browse all 8 tutorials →