Tutorials / Gold price alerts

Gold price alerts with Python and Slack

Use Python to check the XAU/USD spot price from goldprice.dev, detect an upward threshold crossing, and send a Slack message. The script below uses only Python’s standard library and a local SQLite file to remember the last state. Your script polls Goldprice; the outgoing message goes to a Slack incoming webhook.

Updated . Prices are USD per troy ounce, not per gram or a local jeweller’s selling price.

1. Create a private Slack webhook

Create a Slack app, enable Incoming Webhooks, then choose Add New Webhook to Workspace and select your destination channel. Store the resulting URL as a secret environment variable. Follow Slack’s incoming-webhook instructions; do not put the URL in a public repository, browser code, or a screenshot.

2. Set a threshold and persistent state file

Use Python 3.10 or later. Choose a threshold in USD per troy ounce and an absolute database path in a private directory. Use the same path on every run. The example threshold below is a configuration example, not today’s price.

export GOLD_THRESHOLD_USD="4500"
export GOLD_ALERT_DB="/absolute/private/directory/gold-alert.sqlite3"
# Set SLACK_WEBHOOK_URL through your scheduler's secret configuration.
# Keep its value out of shell history and source control.

3. Save and run the Python script

Save this as gold_price_alerts.py, then run python3 gold_price_alerts.py. The first successful run records a baseline, even if the price is already above your threshold. Changing the threshold establishes a new baseline too.

"""Run once per scheduled check. Python 3.10+; standard library only."""
import json
import os
import sqlite3
import sys
from contextlib import closing
from datetime import datetime, timezone
from decimal import Decimal
from urllib.parse import urlparse
from urllib.request import Request, urlopen

PRICE_URL = "https://api.goldprice.dev/v1/prices?symbol=XAU-USD-SPOT"


def fetch_price():
    request = Request(
        PRICE_URL,
        headers={"Accept": "application/json", "User-Agent": "goldprice-alert-tutorial/1.0"},
    )
    with urlopen(request, timeout=15) as response:
        row = json.load(response)["symbols"][0]
    if (row.get("symbol"), row.get("quote_currency"), row.get("unit")) != (
        "XAU", "USD", "troy_ounce"
    ) or row.get("is_stale") is not False:
        raise ValueError("Unexpected or stale quote")
    observed = datetime.fromisoformat(row["computed_at"].replace("Z", "+00:00"))
    age = (datetime.now(timezone.utc) - observed).total_seconds()
    price = Decimal(row["price"])
    if not price.is_finite() or price <= 0 or not 0 <= age <= 900:
        raise ValueError("Invalid price or quote older than 15 minutes")
    return price


def record_crossing(connection, threshold, price):
    """Commit before delivery: at most one attempt per observed upward crossing."""
    connection.execute("CREATE TABLE IF NOT EXISTS alert (id INTEGER PRIMARY KEY CHECK(id=1), threshold TEXT, above INTEGER)")
    previous = connection.execute("SELECT threshold, above FROM alert WHERE id=1").fetchone()
    above = price >= threshold
    crossing = previous is not None and previous[0] == str(threshold) and not previous[1] and above
    connection.execute("INSERT OR REPLACE INTO alert VALUES (1, ?, ?)", (str(threshold), above))
    connection.commit()
    return crossing


def send_slack(webhook, price, threshold):
    payload = json.dumps({"text": f"Gold crossed ${threshold} per troy ounce. Observed XAU/USD: ${price}. Indicative spot price from goldprice.dev."}).encode()
    request = Request(webhook, data=payload, headers={"Content-Type": "application/json"}, method="POST")
    with urlopen(request, timeout=15) as response:
        if response.status != 200 or response.read().strip() != b"ok":
            raise ValueError("Slack did not confirm delivery")


def main():
    # Choose a private, persistent directory; do not share the database between machines.
    os.umask(0o077)
    threshold = Decimal(os.environ["GOLD_THRESHOLD_USD"])
    if not threshold.is_finite() or threshold <= 0:
        raise ValueError("Threshold must be a positive finite number")
    webhook = os.environ["SLACK_WEBHOOK_URL"]
    target = urlparse(webhook)
    if target.scheme != "https" or target.netloc != "hooks.slack.com" or not target.path.startswith("/services/"):
        raise ValueError("Expected a Slack incoming webhook URL")
    database = os.environ.get("GOLD_ALERT_DB", "gold-alert.sqlite3")
    with closing(sqlite3.connect(database, timeout=20)) as connection, connection:
        # Serialize overlapping scheduled runs on this machine, including the quote fetch.
        connection.execute("BEGIN IMMEDIATE")
        price = fetch_price()
        crossing = record_crossing(connection, threshold, price)
    if not crossing:
        print("Baseline recorded or no new upward crossing.")
        return
    try:
        send_slack(webhook, price, threshold)
    except Exception:
        # A timeout can happen AFTER Slack accepts the message. Do not retry blindly.
        print("Alert attempt failed or delivery is uncertain. Check Slack and scheduler logs; this crossing will not be retried automatically.", file=sys.stderr)
        raise SystemExit(1)
    print("Slack confirmed the alert.")


if __name__ == "__main__":
    try:
        main()
    except Exception:
        # Exception URLs may contain the secret webhook; never print them.
        print("Check failed. Check configuration, connectivity and API status; no automatic retry in this run.", file=sys.stderr)
        raise SystemExit(1)

4. Schedule checks and read the result

Run the command with your scheduler every five minutes, supplying the same environment and absolute database path. That is 288 price requests per day on a continuously running schedule. This example makes one anonymous request per run. Check anonymous access limits and authenticated quotas before adding a key or shortening the interval. Keep scheduler error logs and alert on nonzero exits.

For example, observed prices of 4,490 → 4,510 → 4,520 produce one attempt at a 4,500 threshold. Another observation below 4,500 rearms it. A timeout while fetching a quote leaves the stored baseline unchanged. Stale, invalid, or more-than-15-minute-old quotes are rejected.

Failures and delivery tradeoffs

Polling, streams, and no-code alternatives

Polling can miss changes between checks. For continuous events, see SSE and WebSocket documentation and its current plan requirements. For a no-code threshold filter, start with Zapier or n8n. These are monitoring examples, not trading or settlement systems.

Frequently asked questions

Does this use a native Goldprice webhook?

No. Your Python script polls the Goldprice REST API and posts to Slack's incoming webhook. Slack receives the notification; Goldprice does not call your webhook in this example.

Will I get an alert every time the script runs?

No. The first successful check records a baseline without sending. A later check sends only when the observed price moves from below the threshold to at or above it. A fresh below-threshold observation rearms the alert.

What happens after a restart or a failed Slack request?

SQLite preserves the last observed state across restarts. State is committed before posting, so an uncertain delivery is not retried automatically. This limits duplicate attempts but can lose an alert if delivery fails. Check your Slack channel and scheduler logs.

Can polling miss a price crossing?

Yes. A price can cross and return between checks. This guide is for occasional notifications, not tick-level monitoring or trade execution. Streaming is a separate option with its own plan requirements.

Do I need a paid Goldprice plan?

This example uses the anonymous XAU/USD price endpoint. Anonymous IP limits still apply. If you add authentication or poll more frequently, check your current rate limit and monthly quota before choosing a schedule.

Next: Python API integration · Try the anonymous endpoint · Get an API key