Anyone who pays a supplier in another currency ends up watching one number: the exchange rate. It moves up and down all week, and the good days are easy to miss.
Checking it by hand means remembering to look. Most of the time you forget, or you look at the wrong moment.
A small script can watch it for you and send a message only when the number reaches the level you picked. This guide builds one with Python, GitHub Actions and Discord. It's free, and it works for any price that a website or service publishes as data.
What it does
Every few hours, the tracker:
- Reads a list of prices you want to watch
- Fetches each one
- Compares it to your target
- Sends a Discord message the moment a price reaches the target
The important part is the last step. It sends one message when the price crosses your target, not one every time it runs. If the price stays below your target for a week, you get a single alert, not 28.
Where do the prices come from?
This is the part to think about first.
The easiest and most reliable source is a public data feed: a web address that returns the price as structured data (JSON) instead of a web page. Exchange rates, crypto prices and many supplier or marketplace services offer this.
Scraping the HTML of a shop's product page is a different story. It often breaks when the site changes its layout, many shops block automated visitors, and some forbid it in their terms of use. If a shop doesn't offer a feed, check its terms and look for a built-in price alert before you build anything against its pages.
This guide works with feeds only. For the example, I'll use the Frankfurter API, a free service for exchange rates based on European Central Bank data. It needs no account and no API key.
What you need
- A GitHub repository for the tracker (a new, empty one is fine)
- A Discord server where you can add a webhook (a private one just for you works well)
- The address of a price feed. I'll use the exchange rate feed to start
The script uses only what Python already includes, so there's nothing to install.
Step 1: List what you want to watch
Create a file called items.json:
[
{
"name": "EUR to USD",
"url": "https://api.frankfurter.dev/v1/latest?base=EUR&symbols=USD",
"path": "rates.USD",
"target": 1.20,
"alert_when": "above"
}
]
Each item has these parts:
- name: how it appears in your alert
- url: the address of the feed
- path: where the price sits inside the data, with dots between the steps
- target: the number you're waiting for
-
alert_when:
"above"or"below". If you leave it out, it defaults to"below", which suits a product you want to buy cheaper
Here, the feed returns something like {"base": "EUR", "date": "...", "rates": {"USD": 1.17}}. The path rates.USD walks into rates, then picks USD.
Pick a target that suits you. If the price already meets it the first time the tracker runs, you'll get an alert right away.
Step 2: Add the script
Create check_prices.py:
import json
import os
import sys
import urllib.request
WEBHOOK = os.environ.get("DISCORD_WEBHOOK", "")
# Discord turns away requests with Python's default User-Agent, so we set our own
HEADERS = {"User-Agent": "price-tracker/1.0", "Content-Type": "application/json"}
def get_json(url):
request = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(request, timeout=15) as response:
return json.load(response)
def find_price(data, path):
"""Follow a path like 'rates.USD' or 'items.0.price' into the JSON."""
for key in path.split("."):
data = data[int(key)] if isinstance(data, list) else data[key]
return float(data)
def send_alert(text):
if not WEBHOOK:
print("ALERT (no webhook set):", text)
return
body = json.dumps({"content": text}).encode()
request = urllib.request.Request(WEBHOOK, data=body, headers=HEADERS)
urllib.request.urlopen(request, timeout=15).close()
items = json.load(open("items.json"))
state = json.load(open("prices.json")) if os.path.exists("prices.json") else {}
errors = []
for item in items:
name = item["name"]
try:
price = find_price(get_json(item["url"]), item["path"])
except Exception as error:
errors.append(f"{name}: {error}")
continue
mode = item.get("alert_when", "below")
hit = price <= item["target"] if mode == "below" else price >= item["target"]
was_hit = state.get(name, {}).get("hit", False)
print(f"{name}: {price} (target: {mode} {item['target']})")
if hit and not was_hit:
try:
send_alert(f"{name} is now {price} ({mode} your target of {item['target']})")
except Exception as error:
errors.append(f"{name}: could not send the alert ({error})")
continue # don't record the hit, so we try again next run
state[name] = {"price": price, "hit": hit}
with open("prices.json", "w") as file:
json.dump(state, file, indent=2, sort_keys=True)
if errors:
print("Could not check:", *errors, sep="\n- ")
sys.exit(1)
Here's how it works:
- It loads your list and a small file called
prices.json, where it remembers what happened last time. - For each item, it fetches the price and checks it against the target.
- It sends an alert only if the target is met now and wasn't met last time.
- It saves the latest prices and whether each one is "on target."
That memory is what stops the repeat alerts. If a price goes above your target and later drops back, you'll be told again the next time it crosses.
Two details make it more dependable:
- One broken item doesn't stop the rest. Problems are collected, the other items still get checked, and the run is marked as failed at the end so you hear about it.
- A failed alert is retried. If Discord can't be reached, the tracker doesn't record that the alert was sent, so it tries again on the next run.
Step 3: Add the workflow
Create .github/workflows/price-tracker.yml:
name: Price tracker
on:
schedule:
- cron: '17 */6 * * *' # every 6 hours, at 17 minutes past
workflow_dispatch: # adds a "Run workflow" button for testing
concurrency:
group: price-tracker # never run two checks at the same time
permissions:
contents: write # needed to save prices.json
jobs:
track:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4 # use the latest major version
- name: Check prices
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
run: python3 check_prices.py
- name: Save prices
if: always()
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add prices.json
git diff --staged --quiet || git commit -m "Update prices"
git push
A few notes on this:
-
Why
contents: write? GitHub Actions starts fresh every time, so the tracker has no memory between runs. This savesprices.jsonback into your repository. It only makes a commit when something actually changed. - A free bonus. Because each change is saved as a commit, your repository ends up holding a price history you can look back through.
- Why every 6 hours? The exchange rate feed updates once per working day, so checking more often wouldn't show anything new. Match the schedule to how often your feed changes.
-
always()on the save step means the prices are saved even when one item fails.
Step 4: Set up the Discord webhook
A webhook is a private address that lets a script post into a Discord channel. (I explain the idea in my beginner guide to webhooks.)
- In Discord, open the settings for the channel you want alerts in
- Go to Integrations, then Webhooks, and create a new webhook
- Copy the webhook address
- In your GitHub repository, open Settings, then Secrets and variables, then Actions
- Add a new repository secret named
DISCORD_WEBHOOKand paste the address in
Keep the address secret. Anyone who has it can post to your channel, so never paste it into a file in your repository.
Step 5: Test it
Try it on your own computer first. Run this in the folder with your files:
python3 check_prices.py
With no webhook set, an alert prints in your terminal instead of going to Discord. You'll see a line like EUR to USD: 1.17 (target: above 1.2).
Then test the real thing. In your repository, open the Actions tab, choose "Price tracker" and click "Run workflow". To force an alert, set the target somewhere the price has already passed, for example "target": 0.5 with "alert_when": "above". You should get a Discord message within a minute. Put your real target back afterwards.
Adding your own prices
To track something else, add another block to items.json:
{
"name": "My supplier's part",
"url": "https://your-supplier.example.com/api/products/123",
"path": "data.price",
"target": 19.99
}
The address above is a placeholder, so swap in a real feed. To find the path:
- Open the feed address in your browser
- Look at the data it returns and find the price
- Write down the names you pass through to reach it, separated by dots
If the price sits inside a list, use a number for the position, starting at 0. For example, items.0.price means "the first entry in items, then its price."
How often should it check?
Match the schedule to how often the price changes.
An exchange rate that updates once a day doesn't need a check every 10 minutes. A shop that changes prices a few times a week needs even less.
Checking too often doesn't give you better alerts. It uses your free minutes and puts extra load on someone else's server. A few times a day is enough for most things.
What can go wrong
The feed changes its format. If the price moves to a different spot in the data, the tracker can't find it and the run fails. That's a good outcome, because you get a failure email and know to update the path.
The price is tied to something else. Some feeds give prices before tax, or in a different currency, or per pack instead of per item. Check what the number really means before you trust it.
The feed limits how often you can ask. Some services allow only a certain number of requests. If yours does, check less often.
You stop hearing anything. Silence could mean prices never hit your target, or it could mean the tracker stopped. In a public repository, GitHub switches off scheduled workflows after 60 days without repository activity. Now and then, look at the Actions tab. I cover this in how to monitor your automations when something breaks.
Quick answers
Can I track a product on any online shop?
Only if the shop offers a price feed you're allowed to use. Scraping product pages is fragile, and some shops forbid it. Check the terms first, or use the shop's own price alert if it has one.
Will I get spammed with alerts?
No. You're alerted once when the price crosses your target, then again only after it has left the target zone and come back.
Does it cost anything?
No. A run every 6 hours uses about 120 minutes a month, which is nothing on a public repository and a small part of the free allowance on a private one. Discord webhooks are free.
Can I get alerts somewhere other than Discord?
Yes, if the place supports webhooks. You'd change the message format inside send_alert to match what that service expects.
The short version
A price you have to remember to check is a price you'll miss. A small script that watches it and tells you once turns it into something you can forget about until it matters.
Start with one item and get it working. Add more when you trust it.
You can find more practical automation guides on Procwire. If you'd like a check for your own website next, my Python uptime monitor guide is a good place to start.
Top comments (0)