Competitor Price Monitoring: Scraper Setup and Cost
Set up competitor price monitoring with a scraper: a URL watchlist, a daily schedule, change-only rows, GTIN matching across stores, and cost per 1,000 checks.
Published · Updated · By Omar Eldeeb
Competitor price monitoring means checking the same competitor product pages on a schedule and recording every price and stock change. Build a watchlist of product URLs (or category pages), run a scraper daily, have it compare each product with the previous run, and keep only the rows that changed. Match products across stores by GTIN. At Apify's GOLD tier, re-checking an unchanged product costs $0.0002.
This guide sets that up with E-commerce Product Scraper Pro on Apify, using real output from runs on 2026-10-18 and 2026-10-20. The method (watchlist, schedule, diff, match) works with any scraper.
Step 1: decide what goes on the watchlist
You have two kinds of input, and most monitors use both:
| Input | Field | Good for | Watch out for |
|---|---|---|---|
| Product URLs | detailsUrls |
Your direct competitors' versions of your best sellers | New products never show up |
| Category, collection or search URLs | listingUrls |
Spotting new products and range changes | Rank shifts change which products you see |
Start small. Twenty to fifty products that drive your revenue tell you more than 5,000 you'll never look at. Check that each store is on the actor's supported-sites matrix: Shopify and WooCommerce stores, amazon.com, amazon.de, Walmart, Target, John Lewis, M&S, Aldi UK, Waitrose and zalando.de are supported; IKEA, other Amazon and Zalando storefronts and many more are best-effort; eBay, Home Depot, Lowe's, Costco, Etsy and Best Buy are not supported (each gets a status row saying why, and nothing beyond the start fee is charged).
A watchlist input with both kinds, capped while you test:
{
"detailsUrls": [
{ "url": "https://www.target.com/p/lego-botanicals-daisies-building-toy-for-kids-11508/-/A-94756752" },
{ "url": "https://www.walmart.com/ip/LEGO-Botanicals-11508/17091312501" }
],
"listingUrls": [{ "url": "https://www.skullcandy.com/collections/headphones" }],
"maxItemsPerStartUrl": 3,
"maxProductResults": 5,
"monitorMode": true,
"onlyChanges": true
}maxItemsPerStartUrl caps each input, so one big category can't eat the run, and maxProductResults caps the whole run (raise both once it works). proxyCountry is optional: set it if a store shows you the wrong currency. I ran this input on 2026-10-20: the first run returned all five products as new, which is the baseline.
Step 2: turn on monitor mode
With monitorMode, the actor remembers every product between runs in a named key-value store (ecommerce-product-monitor) and adds a change verdict to each row. With onlyChanges (which switches monitor mode on), unchanged products aren't written to the dataset at all. They get a status row and are billed as a cheaper "product checked" event.
changeType is one of new, price-down, price-up, back-in-stock, out-of-stock, changed or unchanged. A product page that now returns 404 is reported as removed in the Status dataset.
Two rules matter for a monitor:
- The first run is the baseline. Every product comes back as
new. - The saved state belongs to the exact list of input URLs (plus
proxyCountry). Add or remove a URL and the next run starts a new baseline. So keep one saved task per watchlist, and change caps or schedule freely: those don't reset anything.
What a change row looks like
I baselined Skullcandy's headphones collection on 2026-10-18 and re-ran the same input on 2026-10-20 with onlyChanges. Of the five products checked, three were unchanged (not written, billed as checks) and two changed. One of them, trimmed:
{
"recordType": "product",
"url": "https://www.skullcandy.com/products/aviator-900-anc-headphones",
"inputUrl": "https://www.skullcandy.com/collections/headphones",
"name": "Aviator® 900 ANC",
"price": 229.99,
"currency": "USD",
"originalPrice": 299.99,
"inStock": true,
"sku": "S6AVW-T740",
"changeType": "price-down",
"changes": ["price-down", "original-price-changed", "variant-price-changed"],
"previousPrice": 299.99,
"priceChange": -70,
"priceChangePct": -23.33,
"previousSeenAt": "2026-10-18T08:04:34.964Z",
"changedVariants": [
{ "key": "sku:S6AVW-T740", "name": "True Black", "previousPrice": 299.99, "price": 229.99, "previousInStock": true, "inStock": true }
],
"scrapedAt": "2026-10-20T09:26:11.814Z"
}The other changed row went the opposite way: Crusher ANC 2 moved from $169.99 to $239.99 (price-up, +41.18%), a sale ending. originalPrice appearing or disappearing is how you tell a promotion starting from a list-price change. The three unchanged products appear only in the Status dataset, with the reason:
"changeType": "unchanged", "previousPrice": 229.99,
"errorDescription": "Unchanged since the last run, so not output (onlyChanges). Billed as a product check (product-checked), ..."Step 3: match the same product across stores
Competitor monitoring gets useful when you can line up the same item at several retailers. Each product row carries gtin, sku, mpn and brandName when the page states them. In the runs behind this guide, Target, Walmart and several Shopify stores published GTINs, while none of 583 Amazon rows had one (Amazon rows usually carry brandName, and about a third had an mpn).
GTINs are the reliable key, with one trap: the same barcode comes back in different lengths. On 2026-10-18 Target gave 0673419422000 and Walmart gave 673419422000 for the same LEGO set. A plain string comparison misses that match. Pad both to 14 digits:
"""Match the same product across stores by GTIN and compare prices. pip install apify-client"""
import os
import re
from collections import defaultdict
from apify_client import ApifyClient
def gtin14(code):
"""UPC-12, EAN-13 and GTIN-14 of the same item differ only by leading zeros."""
digits = re.sub(r"\D", "", str(code or ""))
return digits.zfill(14) if 8 <= len(digits) <= 14 else None
run = ApifyClient(os.environ["APIFY_TOKEN"]).run(os.environ["RUN_ID"])
rows = [r for r in run.dataset().list_items(clean=True).items if r.get("recordType") == "product"]
groups = defaultdict(list)
for r in rows:
key = gtin14(r.get("gtin"))
if key and r.get("price") is not None:
groups[key].append(r)
print(f"{len(rows)} products, {sum(1 for g in groups.values() if len({r['site'] for r in g}) > 1)} matched across stores")
for key, g in groups.items():
if len({r["site"] for r in g}) < 2:
continue
g.sort(key=lambda r: r["price"])
print(f"\nGTIN {key} {g[0]['name'][:50]}")
for r in g:
print(f" {r['site']:<11} {r['price']:>7.2f} {r['currency']} raw gtin={r['gtin']} in stock={r['inStock']}")Output for a run over Target's and Walmart's "lego" search pages (10 products each) on 2026-10-18:
20 products, 2 matched across stores
GTIN 00673419422000 LEGO Botanicals Daisies Building Toy for Kids 1150
target 9.89 USD raw gtin=0673419422000 in stock=True
walmart-us 9.89 USD raw gtin=673419422000 in stock=True
GTIN 00673419424301 LEGO Minifigures Shrek Series 1-Pack - Mystery Box
walmart-us 4.97 USD raw gtin=673419424301 in stock=True
target 4.99 USD raw gtin=0673419424301 in stock=TrueTwo matches out of 20 is normal for search pages: each store ranks different products first. For real monitoring, put the matched product URLs into detailsUrls so the same items are checked every time. When there's no GTIN, use brandName plus mpn (Walmart's LEGO rows had the set number there, for example 11508), and review fuzzy title matches by hand.
Step 4: schedule it
Save the input as a task in Apify Console, then add a schedule. Apify schedules take cron expressions, have a timezone setting, and handle daylight-saving shifts. 0 6 * * * runs every day at 06:00 in the schedule's timezone, so the changes are waiting when you start work.
Run one schedule per watchlist, and don't let two runs of the same list overlap: both would compare against the same stored state and could report the same change twice.
To get the changes somewhere you'll see them, add an integration: Slack, Gmail, a webhook to your own endpoint, or Make/Zapier/n8n. The price drop alerts guide has a tested webhook handler that only messages you about drops above a threshold.
What it costs per 1,000 checks
The actor is pay per event. From the README pricing table (FREE and GOLD are Apify plan tiers; BRONZE and SILVER sit in between):
| Event | FREE | GOLD |
|---|---|---|
Product checked, unchanged (onlyChanges) |
$0.0012 | $0.0002 |
| Product scraped (new or changed row) | $0.0048 | $0.0008 |
| Listing page scraped | $0.0004 | $0.00021 |
| Actor start (per run, default 1 GB) | $0.005 | $0.005 |
So 1,000 unchanged checks cost $1.20 on FREE and $0.20 on GOLD, and 1,000 changed or new rows cost $4.80 and $0.80.
The 2026-10-20 run above billed 1 start, 1 listing page, 2 products scraped and 3 products checked. On FREE: $0.005 + $0.0004 + 2 × $0.0048 + 3 × $0.0012 = $0.0186. On GOLD: $0.005 + $0.00021 + 2 × $0.0008 + 3 × $0.0002 = $0.00741.
A worked month, assuming 1,000 Shopify products checked daily and 5% of them changing each day (an assumption; your rate will differ):
- Daily run on FREE: 950 × $0.0012 + 50 × $0.0048 + $0.005 = $1.385, about $41.55 for 30 days.
- Daily run on GOLD: 950 × $0.0002 + 50 × $0.0008 + $0.005 = $0.235, about $7.05 for 30 days.
- The baseline run bills every product as new: 1,000 × $0.0048 + $0.005 = $4.805 on FREE, $0.805 on GOLD.
Add listing-page fees if you watch categories. Sites reached through residential proxies (IKEA, amazon.co.uk and others in the README's traffic table) also bill residential MB, and with onlyChanges an unchanged check still pays for the traffic it used, because the page was fetched. Apify's free plan gives $5 of usage every month with no credit card required, which covers testing a small watchlist. There's no separate free trial inside the actor.
During Black Friday and the holiday season
Your monitor is most valuable when your baseline already exists. If you start now (late October), you'll have about four weeks of prices before Black Friday on Friday 27 November 2026, which is what you need to tell a real cut from a raised-then-lowered price. In the sale window, add a second, temporary schedule on the same task instead of editing the daily one, for example 0 */3 20-30 11 * (every three hours, 20 to 30 November; 88 runs in America/New_York). Delete it in December. The holiday sales calendar has the baseline start for each event, and comparing Black Friday prices across stores covers the cross-retailer view through Google Shopping.
Limits worth knowing
- Unsupported sites (eBay, Home Depot, Lowe's, Costco, Etsy, Allegro, Newegg, Best Buy and others in the matrix) can't be monitored by direct URL with this actor.
- Some listings are read on the first page only (IKEA, Lidl, Sainsbury's and others; see the README's Limitations). Keep those competitors on
detailsUrls. - A run can succeed with zero rows. With
onlyChangesthat's normal on a quiet day. Read the Status dataset or theOUTPUTrecord to tell "nothing changed" from "blocked".
Related: back-in-stock alerts, MAP violation monitoring, and the free e-commerce price tracker query builder, which assembles a monitor input you can paste into the actor.
Disclosure: I built the actor and this site. The watchlist, schedule and GTIN matching work with any scraper.
Sources: E-commerce Product Scraper Pro README (supported sites, monitor mode, pricing table), Apify docs: Schedules, Apify pricing (free plan), and actor runs on 2026-10-18 and 2026-10-20.
Quick answers
How often should I check competitor prices?
Daily is enough for most catalogs. Check every few hours only for fast-moving categories or during sale events such as Black Friday week, and go back to daily afterwards. More frequent checks mostly add cost and alerts about short-lived changes.
Is it legal to monitor competitor prices?
Collecting publicly displayed prices is common practice. This isn't legal advice: read each site's terms of service, don't collect personal data, and ask a lawyer if you're unsure. The actor's README has a short legality section along the same lines.
How do I match the same product across different online stores?
Use a barcode when the pages publish one: a UPC-12 on one site and an EAN-13 on another describe the same item once both are zero-padded to 14 digits (GTIN-14). Without a GTIN, fall back to brand plus manufacturer part number (MPN) or model number, and only then to a fuzzy title match you review by hand.
What's the difference between competitor price monitoring and a price comparison site?
A comparison site shows shoppers today's prices. Monitoring keeps your own history for a fixed list of competitor products and flags each change (price up, price down, out of stock), so you can react and later see patterns.