Home / Blog / Amazon Price Tracker with GitHub Actions: Scheduled Checks Without a Server

Amazon Price Tracker with GitHub Actions: Scheduled Checks Without a Server

Build an Amazon price tracker with GitHub Actions: a scheduled workflow runs a Python script that reads the Buy Box price for each ASIN, commits the history to prices.csv and opens an issue when a price drops by 5 % or more. No server and no database; tested code, the workflow file and the credit math included.

October 23, 2026
5 min read
SellerMagnet Team
Share & Bookmark
Terminal window running a scheduled Python price check that reports an Amazon price drop

An Amazon price tracker on GitHub Actions needs three pieces: a list of ASINs, a Python script that reads each Buy Box price from /amazon-product-offers and appends it to prices.csv, and a workflow with a schedule trigger that runs the script, commits the history and opens an issue when a price drops. There is no server and no database: the repository holds the history, and the issue is the alert.

Key Takeaways

  • GitHub runs scheduled workflows on POSIX cron syntax in UTC by default; the shortest interval is once every 5 minutes.
  • Scheduled runs can start late at busy times, which include the start of every hour, so this workflow runs at minute 17.
  • In a public repository GitHub disables scheduled workflows after 60 days without repository activity.
  • Error and no-Buy-Box rows are recorded but never replace the last good price, so a gap cannot fake a drop.
  • Four runs a day cost 4 credits per ASIN, about 120 per ASIN in 30 days; malformed ASINs are skipped before they cost anything.

What goes into the repository?

Repository layout of the tracker.
FileWritten byPurpose
asins.txtyouone ASIN per line, # starts a comment
track.pyyoureads the Buy Box price, appends prices.csv, writes drops.md
sellermagnet.py, retry.pyPython quickstartthe API client and the retry helper
.github/workflows/price-tracker.ymlyouschedule, commit, issue
prices.csvthe workflowthe price history, committed after every run
drops.mdtrack.pyonly when something dropped; becomes the issue body, never committed

sellermagnet.py and retry.py are the two helper files from the Python quickstart: new_session and ApiError come from the first, get_with_retry from the second. Keep the repository private if the list of ASINs you watch is not meant to be public.

How does track.py read and compare prices?

For every ASIN the script reads buyBox.totalPrice from the live offer listing: the featured offer's item price plus shipping, which the API returns unrounded, so the script rounds it to two decimals. An empty buyBox means there is no featured offer and therefore no price; that row is stored as no-buy-box. Each new price is compared with the last row whose status is ok, and a fall of DROP_PCT (5 %) or more goes into drops.md. A 401 or 403 stops the run, because a rejected key or an empty balance would fail every remaining call. So does a 503 that survives the retries: it comes from the marketplace's circuit breaker, which every remaining ASIN on the same marketplace would hit too, at a billed credit per attempt. The rows collected so far are still written.

track.py - Buy Box price per ASIN, CSV history, drops.md

import csv
import os
import re
import sys
from datetime import datetime, timezone

import requests

from retry import get_with_retry
from sellermagnet import ApiError, new_session

MARKETPLACE_ID = "ATVPDKIKX0DER"             # amazon.com
DROP_PCT = 5.0                               # report a drop of at least 5 % against the last good price
ASIN_FILE, HISTORY_FILE, DROPS_FILE = "asins.txt", "prices.csv", "drops.md"
FIELDS = ["checked_at", "asin", "price", "currency", "status"]
ASIN_RE = re.compile(r"^[A-Z0-9]{10}$")


def read_asins(path: str) -> tuple[list[str], list[str]]:
    """One ASIN per line, # starts a comment. Bad lines are reported, never sent: a typo is billed too."""
    good, bad = [], []
    with open(path, encoding="utf-8") as fh:
        for line in fh:
            value = line.split("#", 1)[0].strip().upper()
            if value:
                (good if ASIN_RE.match(value) else bad).append(value)
    return list(dict.fromkeys(good)), bad


def last_prices(path: str) -> dict:
    """asin -> (last good price, checks without a price since then). Error rows never replace a price."""
    last = {}
    if os.path.exists(path):
        with open(path, newline="", encoding="utf-8") as fh:
            for row in csv.DictReader(fh):
                price, gap = last.get(row["asin"], (None, 0))
                last[row["asin"]] = (float(row["price"]), 0) if row["status"] == "ok" else (price, gap + 1)
    return last


def buy_box_price(session, asin: str) -> tuple[float | None, str]:
    data = get_with_retry(session, "amazon-product-offers", asin=asin, marketplaceId=MARKETPLACE_ID)
    price = (data.get("buyBox") or {}).get("totalPrice")            # item + shipping, as floats
    if not isinstance(price, (int, float)) or price <= 0:
        return None, ""                                             # no featured offer: not a price
    return round(price, 2), (data.get("currency") or {}).get("code", "")


def main() -> int:
    asins, bad = read_asins(ASIN_FILE)
    for value in bad:
        print(f"skipped {value}: not an ASIN")
    last, session = last_prices(HISTORY_FILE), new_session()
    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%MZ")
    rows, drops, stopped = [], [], ""
    for asin in asins:
        try:
            price, currency = buy_box_price(session, asin)
            status = "ok" if price is not None else "no-buy-box"
        except ApiError as err:
            if err.status in (401, 403, 503):      # bad key, no credits, or the marketplace's circuit breaker
                stopped = str(err)                 # is open: every remaining call would fail (a 503 is billed)
                break
            price, currency, status = None, "", f"error {err.status}"
        except requests.RequestException as err:   # network still failing after the retries
            price, currency, status = None, "", f"error {type(err).__name__}"
        rows.append({"checked_at": now, "asin": asin, "price": "" if price is None else f"{price:.2f}",
                     "currency": currency, "status": status})
        old, gap = last.get(asin, (None, 0))
        if price is not None and old and price <= old * (1 - DROP_PCT / 100):
            note = f", after {gap} check(s) without a price" if gap else ""
            drops.append(f"- {asin}: {old:.2f} -> {price:.2f} {currency} ({(price - old) / old:+.1%}{note})")
    new_file = not os.path.exists(HISTORY_FILE) or os.path.getsize(HISTORY_FILE) == 0
    with open(HISTORY_FILE, "a", newline="", encoding="utf-8") as fh:
        writer = csv.DictWriter(fh, fieldnames=FIELDS)
        if new_file:
            writer.writeheader()
        writer.writerows(rows)
    if drops:
        with open(DROPS_FILE, "w", encoding="utf-8") as fh:
            fh.write(f"Price drops on amazon.com, {now}\n\n" + "\n".join(drops) + "\n")
    elif os.path.exists(DROPS_FILE):
        os.remove(DROPS_FILE)                      # no stale file that would open the same issue again
    print(f"{len(rows)} checked, {sum(r['status'] == 'ok' for r in rows)} with a price, {len(drops)} drop(s)")
    for line in drops:
        print(line)
    if stopped:
        print(f"stopped: {stopped}")
        return 1                                   # a failed run shows up red in the Actions tab
    return 0


if __name__ == "__main__":
    sys.exit(main())

Two runs six hours apart (illustrative ASINs and prices)

$ python track.py      # run 1 (illustrative ASINs and prices)
skipped 4.00638E+12: not an ASIN
3 checked, 2 with a price, 0 drop(s)
$ python track.py      # run 2, six hours later
skipped 4.00638E+12: not an ASIN
3 checked, 3 with a price, 1 drop(s)
- B0CL61F39H: 499.99 -> 444.98 USD (-11.0%)
$ cat drops.md
Price drops on amazon.com, 2026-10-23T06:17Z

- B0CL61F39H: 499.99 -> 444.98 USD (-11.0%)
$ cat prices.csv
checked_at,asin,price,currency,status
2026-10-23T00:17Z,B0CL61F39H,499.99,USD,ok
2026-10-23T00:17Z,B0EXAMPLE2,24.99,USD,ok
2026-10-23T00:17Z,B0EXAMPLE3,,,no-buy-box
2026-10-23T06:17Z,B0CL61F39H,444.98,USD,ok
2026-10-23T06:17Z,B0EXAMPLE2,24.49,USD,ok
2026-10-23T06:17Z,B0EXAMPLE3,19.99,USD,ok
Blueprint flow: GitHub schedule runs track.py, which appends prices.csv and writes drops.md for an issue
The schedule runs the script; the history is committed, a drop becomes an issue.

How do I schedule the Amazon price tracker on GitHub Actions?

Add the workflow below as .github/workflows/price-tracker.yml and store your key as a repository secret named SELLERMAGNET_API_KEY; the workflow passes it to the script as an environment variable, so it never appears in the code or the log of the repository. workflow_dispatch adds a button to run it once by hand before you rely on the schedule.

.github/workflows/price-tracker.yml

name: amazon-price-tracker

on:
  schedule:
    - cron: "17 */6 * * *"      # every 6 hours at minute 17 UTC; minute 0 is GitHub's busiest time
  workflow_dispatch:             # adds a "Run workflow" button for a manual test

permissions:
  contents: write                # commit prices.csv back to the repository
  issues: write                  # open an issue when a price drops

concurrency: price-tracker       # never two runs writing prices.csv at the same time

jobs:
  track:
    runs-on: ubuntu-latest
    timeout-minutes: 30          # a 503 waits up to 60 s between its 4 attempts; raise it for long ASIN lists
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.12"
      - run: pip install requests
      - name: Check prices
        env:
          SELLERMAGNET_API_KEY: ${{ secrets.SELLERMAGNET_API_KEY }}
        run: python track.py
      - name: Commit the price history
        if: ${{ !cancelled() }}        # keep the rows of a run that stopped early (401, 403, 503)
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add prices.csv
          git diff --cached --quiet || git commit -m "prices $(date -u +%Y-%m-%dT%H:%MZ)"
          git push
      - name: Open an issue on a price drop
        if: ${{ !cancelled() }}
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          if [ -f drops.md ]; then
            gh issue create --title "Amazon price drop $(date -u +%Y-%m-%d)" --body-file drops.md
          fi

The details come from GitHub's documentation (docs.github.com and the actions/checkout README, checked 2026-10-23). Scheduled workflows use POSIX cron syntax and run in UTC unless you set a time zone, only on the default branch, and never more often than once every 5 minutes. The schedule event can be delayed when Actions is under high load, which includes the start of every hour, and some queued jobs may be dropped, hence minute 17. permissions grants the built-in token write access to contents (the commit) and issues; actions/checkout persists that token for later git commands (since v6 in a separate file under $RUNNER_TEMP instead of .git/config), so git push works without extra setup, and the GitHub CLI is preinstalled on GitHub-hosted runners and reads its token from GH_TOKEN. if: ${{ !cancelled() }} is GitHub's recommended way to run a step after a failed one, so a run that stopped on a 401, 403 or 503 still commits its rows and opens the issue for the drops it found.

How many credits does the tracker use?

One /amazon-product-offers call per ASIN per run, so the cost is ASINs times runs. Billing follows the usual rules: a 404 is billed once and not retried, a 502 or 503 is billed on every attempt of get_with_retry, a 500 is refunded, and 400 gate errors, 401, 403 and 429 cost nothing. GitHub Actions itself is free for public repositories on standard GitHub-hosted runners; in a private repository the runs count against the minutes included in your plan (2,000 a month on GitHub Free, per GitHub's billing docs, checked 2026-10-23). If you only need past prices and not your own checks, the price history API returns the recorded history in one call, and the price history chart API adds ready-made PNG charts for the same credit; plans are on Pricing.

Credits per tracked ASIN by schedule (1 credit per offers call).
cronRunsRuns per dayCredits per 30 days
17 every hour24720
17 /2 every 2 hours12360
17 /6 every 6 hours4120
17 6 *once a day130
Blueprint table of cron schedules with runs per day and credits per tracked ASIN per 30 days
Every 6 hours is 120 credits per ASIN a month; hourly is six times that.

Frequently Asked Questions

Why does my scheduled workflow start late?

GitHub says the schedule event can be delayed when Actions is under high load, which includes the start of every hour, and some queued jobs may be dropped. A minute like 17 lowers the chance.

Why did the tracker stop running?

In a public repository GitHub disables scheduled workflows after 60 days without repository activity. Check the Actions tab and enable the workflow again; also check for a red run caused by a 401, 403 or 503.

Can it check prices every minute?

No. The shortest interval GitHub allows for a scheduled workflow is once every 5 minutes, and every run costs 1 credit per ASIN, so an interval of hours keeps the bill small.

What happens when my credits run out mid-run?

The API answers 401 or 403, which are not billed. track.py stops, keeps the rows it already has, and exits with code 1, so the run shows as failed while the history and any drops are saved.

Why is a missing Buy Box not recorded as a drop?

Without a featured offer there is no price to compare. The row is stored as no-buy-box, and the next real price is compared with the last good one, labelled with the number of checks between.

Bottom line: a scheduled workflow, one Python script and a CSV file are a complete Amazon price tracker, as long as missing prices never count as drops and a failed run never overwrites the last good price. For alerts in a chat instead of an issue, see the Telegram price tracker; a free account includes 150 credits to try it.

Ready to Extract Amazon Data at Scale?

Start building with SellerMagnet API today. Real-time product data, competitive pricing, and review analytics at your fingertips.

150 free API credits • No credit card required • Cancel anytime