# Build a gold-price alert in Python

This tutorial builds a standalone Python script that polls the [live gold spot price](/prices/xau-usd) and fires an alert when a threshold is crossed. It:

1. Fetches the live XAU-USD spot price from the API
2. Checks it against a configurable threshold
3. Fires a notification when the threshold is crossed
4. Caches responses locally to avoid burning quota
5. Retries on transient errors with exponential backoff

No SDKs required—only the standard library plus `requests`.

## Prerequisites

- Python 3.9+
- `pip install requests`
- A free API key from [goldprice.dev/pricing](https://goldprice.dev/pricing) (1,000 calls/month on the free tier)

## Step 1 — Fetch the live spot price

The spot endpoint returns the current price along with source metadata:

```python
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.goldprice.dev/v1"

def fetch_spot_price() -> float:
    """Return the current XAU-USD spot price in USD per troy ounce."""
    resp = requests.get(
        f"{BASE_URL}/prices",
        params={"symbol": "XAU-USD-SPOT"},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=10,
    )
    resp.raise_for_status()
    data = resp.json()
    # The API returns sources[]. Use the first live spot source.
    for source in data.get("sources", []):
        if source.get("type") == "spot" and not source.get("stale", True):
            return float(source["price"])
    raise ValueError("No live spot price in response")
```

The `sources[]` array lets you inspect *which* oracle is behind each price. If a source is flagged `stale: true`, skip it—the API may return multiple sources and some may be delayed.

## Step 2 — Add a TTL cache

Polling every second against a REST endpoint is wasteful and burns quota. A simple in-memory cache with a configurable TTL is enough for most alert use cases:

```python
import time
from typing import Optional

_cache: dict[str, tuple[float, float]] = {}  # key -> (value, expiry)

def cached_spot_price(ttl_seconds: int = 60) -> float:
    """Return a cached spot price, refreshing only when TTL has elapsed."""
    key = "XAU-USD-SPOT"
    now = time.monotonic()
    if key in _cache:
        value, expiry = _cache[key]
        if now < expiry:
            return value
    price = fetch_spot_price()
    _cache[key] = (price, now + ttl_seconds)
    return price
```

At 60-second TTL you use at most 1,440 calls per day—well within the free tier (1,000/month) for light testing, and comfortable for any paid plan. For a deeper look at cache strategy and rate-limit headers, see [Caching gold prices and staying inside rate limits](/blog/gold-price-caching-rate-limits).

## Step 3 — Retry on transient errors

Network failures and rate-limit responses (HTTP 429) are transient. Wrap the fetch with exponential backoff:

```python
import time
import requests

MAX_RETRIES = 4
BACKOFF_BASE = 2.0  # seconds

def fetch_spot_price_with_retry() -> float:
    for attempt in range(MAX_RETRIES):
        try:
            return fetch_spot_price()
        except requests.exceptions.HTTPError as exc:
            if exc.response is not None and exc.response.status_code == 429:
                wait = BACKOFF_BASE ** attempt
                print(f"Rate limited. Retrying in {wait:.0f}s…")
                time.sleep(wait)
            else:
                raise
        except requests.exceptions.RequestException as exc:
            wait = BACKOFF_BASE ** attempt
            print(f"Request error: {exc}. Retrying in {wait:.0f}s…")
            time.sleep(wait)
    raise RuntimeError("Max retries exceeded fetching spot price")
```

## Step 4 — The alert logic

Define thresholds and fire a notification. Here we use `print` as the notifier—swap in email, Slack, SMS, or any webhook:

```python
def send_alert(message: str) -> None:
    """Replace this with your preferred notification channel."""
    print(f"[ALERT] {message}")

def check_and_alert(
    above: Optional[float] = None,
    below: Optional[float] = None,
) -> None:
    """Fire an alert if the live price crosses either threshold."""
    price = cached_spot_price(ttl_seconds=60)
    print(f"XAU-USD spot: ${price:,.2f}")

    if above is not None and price >= above:
        send_alert(f"Gold crossed ABOVE ${above:,.0f} — current: ${price:,.2f}")
    if below is not None and price <= below:
        send_alert(f"Gold fell BELOW ${below:,.0f} — current: ${price:,.2f}")
```

## Step 5 — Poll on an interval

Wire everything into a polling loop:

```python
import time

def run_alert_loop(
    above: Optional[float] = None,
    below: Optional[float] = None,
    poll_interval: int = 60,
) -> None:
    """
    Poll forever, checking thresholds on each tick.

    Args:
        above: Fire when price >= this value (USD/oz). None = no upper alert.
        below: Fire when price <= this value (USD/oz). None = no lower alert.
        poll_interval: Seconds between checks (minimum 60 recommended).
    """
    print(f"Starting gold alert. above={above}, below={below}, interval={poll_interval}s")
    while True:
        try:
            check_and_alert(above=above, below=below)
        except Exception as exc:
            print(f"Error during check: {exc}")
        time.sleep(poll_interval)

if __name__ == "__main__":
    # Alert if gold goes above $3,500 or below $3,000
    run_alert_loop(above=3500, below=3000, poll_interval=60)
```

## Complete file

Putting it all together in `gold_alert.py`:

```python
"""
gold_alert.py — Poll the live gold spot price and fire threshold alerts.
Requires: pip install requests
"""
import time
from typing import Optional
import requests

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.goldprice.dev/v1"
MAX_RETRIES = 4
BACKOFF_BASE = 2.0

_cache: dict[str, tuple[float, float]] = {}


def fetch_spot_price() -> float:
    resp = requests.get(
        f"{BASE_URL}/prices",
        params={"symbol": "XAU-USD-SPOT"},
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=10,
    )
    resp.raise_for_status()
    data = resp.json()
    for source in data.get("sources", []):
        if source.get("type") == "spot" and not source.get("stale", True):
            return float(source["price"])
    raise ValueError("No live spot price in response")


def fetch_spot_price_with_retry() -> float:
    for attempt in range(MAX_RETRIES):
        try:
            return fetch_spot_price()
        except requests.exceptions.HTTPError as exc:
            if exc.response is not None and exc.response.status_code == 429:
                wait = BACKOFF_BASE ** attempt
                time.sleep(wait)
            else:
                raise
        except requests.exceptions.RequestException:
            time.sleep(BACKOFF_BASE ** attempt)
    raise RuntimeError("Max retries exceeded")


def cached_spot_price(ttl_seconds: int = 60) -> float:
    key = "XAU-USD-SPOT"
    now = time.monotonic()
    if key in _cache:
        value, expiry = _cache[key]
        if now < expiry:
            return value
    price = fetch_spot_price_with_retry()
    _cache[key] = (price, now + ttl_seconds)
    return price


def send_alert(message: str) -> None:
    print(f"[ALERT] {message}")


def check_and_alert(
    above: Optional[float] = None,
    below: Optional[float] = None,
) -> None:
    price = cached_spot_price(ttl_seconds=60)
    print(f"XAU-USD spot: ${price:,.2f}")
    if above is not None and price >= above:
        send_alert(f"Gold crossed ABOVE ${above:,.0f} — current: ${price:,.2f}")
    if below is not None and price <= below:
        send_alert(f"Gold fell BELOW ${below:,.0f} — current: ${price:,.2f}")


def run_alert_loop(
    above: Optional[float] = None,
    below: Optional[float] = None,
    poll_interval: int = 60,
) -> None:
    print(f"Starting gold alert. above={above}, below={below}, interval={poll_interval}s")
    while True:
        try:
            check_and_alert(above=above, below=below)
        except Exception as exc:
            print(f"Error: {exc}")
        time.sleep(poll_interval)


if __name__ == "__main__":
    run_alert_loop(above=3500, below=3000, poll_interval=60)
```

## Extending the notifier

The `send_alert` function is the only thing you need to swap out. A few common patterns:

**Slack webhook:**
```python
import requests as req

def send_alert(message: str) -> None:
    req.post(
        "https://hooks.slack.com/services/YOUR/WEBHOOK",
        json={"text": message},
        timeout=5,
    )
```

**Email via SMTP:**
```python
import smtplib
from email.message import EmailMessage

def send_alert(message: str) -> None:
    msg = EmailMessage()
    msg["Subject"] = "Gold Price Alert"
    msg["From"] = "alerts@yourdomain.com"
    msg["To"] = "you@yourdomain.com"
    msg.set_content(message)
    with smtplib.SMTP_SSL("smtp.yourdomain.com", 465) as s:
        s.login("alerts@yourdomain.com", "YOUR_PASSWORD")
        s.send_message(msg)
```

## Quota guidance

| Plan | Calls/month | At 60s poll | Days of coverage |
|------|-------------|-------------|-----------------|
| Free | 1,000 | ~1,440/day | < 1 day (demo only) |
| Basic | included | ~1,440/day | 30 days |
| Pro | included | ~1,440/day | 30 days |

For continuous 60-second polling, any paid plan is appropriate. Free tier is useful for testing the integration before upgrading. See the [pricing page](/pricing) for plan details.
