If you watch competitor prices, you do not want 200 rows every morning. You want the 3 rows where something moved.
Yesterday I wrote about comparing one product's price across online stores. At the end I showed a small manual diff and said a built-in change mode was next. It is live now, so this post shows how to use it: a daily run that returns only offers whose price or stock changed, with the old price, the new price and the change in percent. Real output from today (2026-11-11) included, plus the limits.
Why the usual approach hurts
A home-made price watch usually grows like this:
- Scrape every product page on a schedule.
- Save all rows somewhere.
- Load yesterday's rows, match them by URL, compare price and stock.
- Decide what counts as a change (currency rounding, sale price, "out of stock").
- Store today's rows as the new baseline, without losing them when a run fails halfway.
- Pay for storing and processing hundreds of rows that did not change.
Steps 3 to 5 are the boring part, and they are where these scripts break: one failed run overwrites the baseline and the next day every row looks "new".
What the change mode does
I built the Cheapest Price Finder: you give it product page URLs from any stores, it reads the real price from each page's structured data (JSON-LD offers, Shopify product data, meta price tags), converts everything to one currency with daily ECB rates and sorts the offers from cheapest to most expensive.
Two inputs turn it into a price watch:
-
onlyChanged: truesaves only offers whose price or stock changed since the last run, plus pages it has never seen. Unchanged offers are not saved and not billed. -
monitorId(optional) names the watch, e.g.aeropress-eu. Runs with the samemonitorIdshare the remembered prices even when you add or remove URLs.
Every returned row gets changeType (new, priceDrop, priceIncrease, availability), previousPrice, priceChange and priceChangePercent. These are in the shop's own currency (the price field), so a moving exchange rate never looks like a price change. The state lives in your own Apify account, so your watch is yours alone.
Step 1: get a token
Create a free Apify account and copy your token from Console (Settings, API and Integrations). Put it in an environment variable:
export APIFY_TOKEN=your_token_here Step 2: the first run with curl
curl -X POST "https://api.apify.com/v2/acts/martin10291~cheapest-price-finder/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"productUrls": [ "https://www.vervecoffee.com/products/aeropress-coffee-brewer", "https://www.intelligentsia.com/products/aeropress", "https://www.espressoparts.com/products/aeropress-coffee-maker", "https://www.stumptowncoffee.com/products/aeropress"], "convertTo": "EUR", "onlyChanged": true, "monitorId": "aeropress-eu"}' The first run has nothing to compare with, so every offer comes back as new. That is your baseline.
Step 3: the same thing in Python
import os import requests TOKEN = os.environ["APIFY_TOKEN"] # your own token URL = ("https://api.apify.com/v2/acts/martin10291~cheapest-price-finder/" "run-sync-get-dataset-items") URLS = [ "https://www.vervecoffee.com/products/aeropress-coffee-brewer", "https://www.intelligentsia.com/products/aeropress", "https://www.espressoparts.com/products/aeropress-coffee-maker", "https://www.stumptowncoffee.com/products/aeropress", ] def price_changes(urls, watch="aeropress-eu", currency="EUR"): r = requests.post(URL, params={"token": TOKEN}, timeout=180, json={ "productUrls": urls, "convertTo": currency, "onlyChanged": True, "monitorId": watch, }) r.raise_for_status() return r.json() for o in price_changes(URLS): if o["changeType"] == "new": print(f"NEW {o['store']:22} {o['priceConverted']:>7} EUR {o['availability']}") else: print(f"{o['changeType']:13} {o['store']:22} " f"{o['previousPrice']} -> {o['price']} {o['currency']} " f"({o['priceChangePercent']:+}%) {o['availability']}") Real output from today
I ran the same input twice, a few minutes apart, with monitorId set.
Run 1 (9.5 s, 4 rows):
NEW vervecoffee.com 35.63 EUR InStock NEW intelligentsia.com 35.67 EUR OutOfStock NEW espressoparts.com 40.08 EUR InStock NEW stumptowncoffee.com 41.02 EUR InStock One row as JSON (trimmed):
{ "rankByPrice": 1, "isCheapest": true, "store": "vervecoffee.com", "price": 39.95, "currency": "USD", "priceConverted": 35.63, "convertedCurrency": "EUR", "availability": "InStock", "changeType": "new", "previousPrice": null, "priceChange": null, "priceChangePercent": null, "url": "https://www.vervecoffee.com/products/aeropress-coffee-brewer" } Run 2 (6.1 s, 0 rows). No price moved in those minutes, so the dataset is empty and the run was charged for 0 results. The run's RUN_SUMMARY record tells you it still checked everything:
{ "pagesChecked": 4, "offers": 4, "failed": [], "lastError": null, "monitor": { "monitorId": "devto-aeropress-1111", "firstRun": false, "onlyChanged": true, "changes": {"new": 0, "priceDrop": 0, "priceIncrease": 0, "availability": 0, "unchanged": 4}, "rowsSaved": 0 } } I did not invent a price drop for this post. When one of these shops changes a price, the row comes back with changeType: "priceDrop" or "priceIncrease", previousPrice, priceChange and priceChangePercent filled. An "in stock" to "out of stock" switch comes back as availability.
An empty dataset on a quiet day is the point: your alert code runs only on real changes.
Step 4: run it every day
The simplest way is an Apify schedule:
- Open the actor page, paste your URLs, tick "Only changed offers" and type a Monitor ID.
- Save it as a Task.
- Add a schedule, e.g. every day at 07:00.
- Optional: add a webhook on "run succeeded" that calls your own endpoint, or connect the dataset to Google Sheets, Slack or email with Apify integrations.
If you prefer your own cron, call price_changes() from a script and send the rows wherever you like:
0 7 * * * /usr/bin/python3 /home/me/price_changes.py >> /home/me/prices.log 2>&1 Add or remove URLs at any time. With the same monitorId, old URLs keep their remembered prices and new URLs come back as new once.
The API.market option
The same tool is on API.market with a free plan, if you want one API key and simple HTTP:
curl -X POST 'https://prod.api.market/api/v1/martindata/price-comparison-api/m/cheapest-price-finder' \ -H 'x-api-market-key: YOUR_API_MARKET_KEY' \ -H 'Content-Type: application/json' \ -d '{"productUrls": ["https://www.espressoparts.com/products/aeropress-coffee-maker"], "convertTo": "EUR"}' One honest difference: on API.market the change mode is switched off. The remembered prices would live in my account, shared by every caller with the same URLs, and one customer's run would "use up" another customer's changes. So there you get the full sorted offers every time and keep the diff yourself (the 10-line diff from the first post works). For a built-in change feed, use the Apify actor with your own token.
Honest limits
- Big marketplaces often block automated requests. Amazon, Walmart, Target, Best Buy and Newegg frequently answer with a bot-check page. The tool does not bypass those; it reports them in
RUN_SUMMARY.failedand does not charge for them. Brand stores and independent shops (Shopify, WooCommerce, Magento, BigCommerce) work best. - The first run is never empty: every page is
newonce. - No shipping costs or coupon codes. It compares the listed product price.
- It does not match products for you. You choose which URLs are the same item;
gtin,skuandbrandhelp you check. - Pages that build the price only with JavaScript and publish no product data return "no price found".
Price
On Apify it costs $3.00 per 1,000 saved results (lower with Apify's Bronze, Silver and Gold plans). With onlyChanged, unchanged offers are not saved, so a quiet day costs nothing for results. My two runs above together cost under one tenth of a cent of platform usage. Apify's free plan includes a small monthly credit, enough to try this.
On API.market there is a free plan to test, then the paid plans shown on the listing.
Wrap-up
I built this because every price script I wrote ended up as the same "compare with yesterday" code, and it was the part that broke. Now the tool keeps yesterday for you.
If you watch prices for a shop, a reseller business or a deal newsletter, try it on Apify or API.market. Which shop would you add to the watch first? If it returns "no price found", post the URL in the comments and I will look at it.


