A gold price alert bot has three parts: a price source, a rule such as "crossed 3,900", and a message to a chat. Telegram and Discord each take one HTTPS POST with JSON. The price comes from GET /v1/spot/XAU, which needs no key. A dry run on 2 November 2026 printed both requests. Plus and Pro also offer webhook alerts, which need no polling loop.
- The script polls the spot price every five minutes and posts only when the price changes side of your level.
- Keyless prices are 15 minutes late and keyless calls are capped at 10 a minute and 100 an hour. One call every five minutes is 12 an hour.
- Plus ($39 a month) and Pro ($89 a month) include five webhook alerts that call your URL, so a bot needs no polling loop.
Get the price
curl "https://api.goldprice.dev/v1/spot/XAU"The response holds price in US dollars per troy ounce as a string, is_stale and computed_at.
The two messages
Telegram's Bot API takes a POST to https://api.telegram.org/bot<token>/sendMessage with a chat_id and text of 1 to 4096 characters. You get the token from @BotFather. Discord takes a POST to a channel webhook URL with a content field of up to 2000 characters.
The script
import json, os, sys, timefrom decimal import Decimalfrom urllib.request import urlopen, RequestLEVEL = Decimal("3900") # alert when gold crosses this, USD per troy ounceTELEGRAM = os.environ.get("TELEGRAM_URL", "https://api.telegram.org/bot<token>/sendMessage")DISCORD = os.environ.get("DISCORD_URL", "https://discord.com/api/webhooks/<id>/<token>")DRY = "--dry-run" in sys.argvdef spot(): with urlopen("https://api.goldprice.dev/v1/spot/XAU", timeout=10) as r: return json.load(r)def post(url, body): req = Request(url, data=json.dumps(body).encode(), headers={"Content-Type": "application/json", "User-Agent": "gold-alert-bot/1.0"}) if DRY: print("POST", req.full_url, req.data.decode()) else: urlopen(req, timeout=10).read()def notify(text): post(TELEGRAM, {"chat_id": os.environ.get("TELEGRAM_CHAT", "<chat id>"), "text": text}) post(DISCORD, {"content": text})side = Nonewhile True: q = spot() price = Decimal(q["price"]) now = "above" if price > LEVEL else "below" if side and now != side: notify(f"Gold crossed {LEVEL}: {price} USD/oz, now {now} ({q['computed_at'][:16]}Z)") if DRY: notify(f"Gold {price} USD/oz at {q['computed_at'][:16]}Z, {now} {LEVEL}") break side = now time.sleep(300)The script keeps the last side of the level in memory. On the first pass it only records the side, so a restart does not send a false alert. It then sends a message each time the price moves from one side to the other. Set TELEGRAM_URL, TELEGRAM_CHAT and DISCORD_URL through environment variables, and keep tokens out of the repository. The script sends its own User-Agent header because Discord answers the default Python one with a 403.
A dry run with --dry-run prints the two requests it would send and exits. The output on 2 November 2026:
POST https://api.telegram.org/bot<token>/sendMessage {"chat_id": "<chat id>", "text": "Gold 3904.38 USD/oz at 2026-11-02T11:53Z, above 3900"}POST https://discord.com/api/webhooks/<id>/<token> {"content": "Gold 3904.38 USD/oz at 2026-11-02T11:53Z, above 3900"}The placeholders show where the token, chat id and webhook go. We did not post to a live chat, so run the script once in your own chat before you trust it.
Avoid repeat alerts
A price that sits on the level can cross it many times in an hour. Add a quiet period, such as ten minutes after each alert, or require the price to move a small margin past the level before you flip the side. The webhook alerts below do this for you.
Webhook alerts without polling
Plus and Pro each include five price alerts. An alert watches gold, silver or copper with one rule and sends a signed POST to one URL. The rules are a level cross, a percentage move over 1 hour or 24 hours, and a daily close. A level alert stays quiet for at least 10 minutes after it fires, and a failed delivery is retried up to five more times over about 8.5 hours. Your endpoint receives the event, checks the signature and posts the text to Telegram or Discord with the same two requests as above. The signature check and payload are in the alerts reference.