This is a runnable, end-to-end pattern for Amazon competitor price monitoring: fetch snapshots, diff against baselines, and fire alerts only when they matter — with polling tiers and cost math. Adapted from the full guide on the Pangolinfo blog.
Why this exists
Price monitoring is change detection, not data collection. The system's output is the diff, not the snapshot. Three things make it worth building: Buy Box share follows price (a rival $1.50 cheaper takes most orders while your listing looks unchanged); brand pricing erodes when resellers break MAP; and competitor stock-outs are short, lucrative windows — if you detect them in hours, not weeks.
The architecture
Six stages, same in a script or a pipeline:
- Track list — ASINs (not URLs), tagged by tier: hero / competitors / watchlist / long-tail
- Scheduler — what gets checked, how often
- Fetch — structured API data
- Normalize —
"$19.99"→19.99; variants handled; deduped - Store + diff — snapshots appended, compared to a rolling baseline
- Alert + act — the only output anyone ever sees
Step 1: Fetch and normalize
import requests from datetime import date API_KEY = "pgl_xxx" # free key at tool.pangolinfo.com — first 60 requests free headers = {"Authorization": f"Bearer {API_KEY}"} def fetch_snapshot(asin): d = requests.get(DETAIL_URL, headers=headers, params={"asin": asin}).json() return { "asin": asin, # Normalize at ingest: "$19.99" (string) -> 19.99 (number). # Comparing raw strings is the #1 bug in DIY monitors. "buybox_price": float(d["buybox_price"].replace("$", "")), "list_price": float(d.get("list_price", "0").replace("$", "") or 0), "seller": d["buybox_seller"], "offers": d["offers"], # seller -> price -> fulfillment (attribution) "in_stock": d["availability"] == "in_stock", "captured_at": date.today().isoformat(), # never skip the timestamp } Track per ASIN: Buy Box price (what customers pay), offer list (who moved), list price (discount depth), seller + fulfillment (event severity — an FBM seller with 200 ratings dropping price is a different event than the brand owner doing it), availability, timestamp.
Step 2: Diff against the baseline
FLOOR = 18.50 # your floor price for this ASIN def check(asin): baseline = load_baseline(asin) # latest snapshot from your store today = fetch_snapshot(asin) if today["buybox_price"] < baseline["buybox_price"]: drop = (baseline["buybox_price"] - today["buybox_price"]) / baseline["buybox_price"] # Dual thresholds: absolute guards the floor, percentage catches drift if today["buybox_price"] < FLOOR or drop > 0.05: maybe_alert(asin, today, baseline, drop) elif not today["in_stock"] and baseline["in_stock"]: maybe_alert(asin, today, baseline, 0, kind="restock_watch") elif today["in_stock"] and not baseline["in_stock"]: maybe_alert(asin, today, baseline, 0, kind="back_in_stock") save_baseline(asin, today) # roll the baseline forward Step 3: Alert like you mean it
def maybe_alert(asin, today, baseline, drop, kind="drop"): # Cooldown: same ASIN + direction, 24h quiet after firing. # Without this, an oscillating rival pages you six times a night, # and the channel gets muted by Friday. if alerted_recently(asin, kind, hours=24): return # Attribution: attach WHO moved, not just that something moved. # "Price dropped" is trivia; "Seller X cut $2 at 3pm and took # the Buy Box" is intelligence. alert(f"{kind.upper()} {asin}: ${baseline['buybox_price']} -> " f"${today['buybox_price']} by {today['seller']} ({drop:.1%})") record_alert(asin, kind) Severity tiers worth adding on top: critical (floor breached / Buy Box lost → page immediately), watch (moderate shift / new seller entered → daily digest), opportunity (rival out of stock / MAP violation → dashboard).
Step 4: Schedule by tier
Frequency is a cost function — tier it:
from apscheduler.schedulers.blocking import BlockingScheduler sched = BlockingScheduler() sched.add_job(lambda: poll_tier("hero"), "interval", hours=1) # revenue defense sched.add_job(lambda: poll_tier("competitors"), "interval", hours=6) # intraday moves sched.add_job(lambda: poll_tier("watchlist"), "interval", hours=24) # trends sched.add_job(lambda: poll_tier("longtail"), "interval", days=7) # structural shifts sched.start() Two practical notes: prices move in bursts around traffic peaks, so 6-hour polling often catches as much as 2-hour for a third of the cost. And some APIs cache hard targets ~10 minutes — polling faster than that pays twice for one snapshot.
The failure modes this design avoids
- Adversarial target: anti-bot, CAPTCHAs, rate limits — using a structured API instead of raw scraping moves this cost to the provider.
- Wrong price field: six tracked fields, Buy Box first.
- Noise: dual thresholds + 24h cooldown + severity tiers.
- No attribution: offer-list data attached to every alert.
- Stale data: timestamps on every record; cache behavior accounted for in scheduling.
What it costs
On Pangolinfo's Amazon Scraper API: 1 credit/page, one page per ASIN per check.
| Setup | Credits/month |
|---|---|
| 50 hero ASINs, hourly | 36,000 |
| 500 competitors, 4×/day | 60,000 |
| 5,000 watchlist, daily | 150,000 |
First 60 requests free — enough to wire this up against real ASINs. Tier your frequencies and most teams cut 60–70% off the naive number. Prefer zero infra? Their Product Research Tool runs scheduled tracking with alerts at 1.5 credits/page.
Checklist before you ship
- Snapshots carry timestamps; baseline rolls forward
- Prices normalized to numbers at ingest
- Dual thresholds + 24h cooldown + severity tiers
- Alerts carry seller attribution from the offer list
- Frequencies tiered; cache behavior accounted for
- Snapshot table supports replay at any historical point
Adapted from the complete guide — which also walks through MAP enforcement and Buy Box defense scenarios plus provider selection.
