🎉 Premium Proxies · 24-Hour Free TrialClaim Now
Use Cases

Build Your Own Amazon Price Tracker in Python

Build an Amazon price tracker in Python: daily price, BSR and seller snapshots to SQLite, a Keepa-style chart, alerts, and robot-check detection that works.

S SparkProxy 0 27 min read
Share

An Amazon price tracker in Python is a small program with one awkward job: visit the same list of ASINs every day, write down the Buy Box price, Best Sellers Rank and seller, and never mistake a bot-check page for a product. This guide builds that tracker end to end, from a CSV watchlist to a SQLite history, a step chart that reads like Keepa's, and an alert that fires once when a price crosses your target. The proxy layer uses SparkProxy's rotating US gateway, with the Scraping API as a fallback for the ASINs that refuse to load.

What the Tracker Does, End to End

The whole system is one daily batch job. Nothing runs between batches, so there is no server to babysit.

StageInputOutputFile
WatchlistYou, editing a CSV`asin,label,target_cents` rows`watchlist.csv`
FetchOne ASINRaw HTML from `amazon.com/dp/``fetch.py`, `retry.py`
ClassifyStatus code + HTML`ok`, `interstitial`, `throttled`, `gone`, `unknown``detect.py`
ParseHTML that passed `ok`Price in cents, BSR, seller, Buy Box flag`parse.py`
StoreParsed rowOne row per ASIN per UTC day`store.py`
Chart + alertSQLite historyPNG step chart, email on threshold crossing`chart.py`, `alerts.py`
OrchestrateAll of the aboveOne run per day`run_daily.py`

The watchlist is deliberately boring. Prices are stored in integer cents everywhere, so target_cents is too. A blank target means "track it, never alert":

asin,label,target_cents
B0EXAMPLE1,Espresso grinder,18999
B0EXAMPLE2,Standing desk frame,
B0EXAMPLE3,Replacement filter 3-pack,2450

Install three packages into a virtual environment on Python 3.11 or newer. The floor comes from matplotlib: version 3.11.2, released on PyPI on 2026-09-11, declares Requires-Python >=3.11.

python -m venv .venv
. .venv/bin/activate
pip install "requests>=2.34" "selectolax>=1.0" "matplotlib>=3.11"

If you sell on Amazon and only care about your own listings, the Selling Partner API is the cleaner source, and our breakdown of which proxies Amazon sellers actually need starts from that point. This tracker is for ASINs you don't control: competitors, suppliers, or things you want to buy.

Proxy Choices for amazon.com

Three decisions matter for this job, and each one has a right answer for a daily ASIN sweep.

Country. Amazon decides currency, default delivery region and sometimes the offer itself from where the request comes from. For amazon.com, exit in the US. SparkProxy's USA Rotating pool is listed on the homepage (read 2026-10-30) as "50,000+ premium US datacenter IPs", so every exit is already American and you don't need a country flag at all. On a Worldwide Rotating plan (1M+ IPs, 80+ countries), the dashboard's credential widget shows a country suffix in the username, displayed on the homepage as user-country-us. Copy the exact string your dashboard generates rather than typing it from memory.

Rotation mode. SparkProxy offers Random Rotation, a new exit IP on every request, on port 11000, and Sticky Sessions, which hold one IP for about five minutes, on port 11002. A product-page fetch is a single stateless GET, so Random Rotation is the right fit. It also gives you free retries: when one IP draws a challenge, the next attempt leaves from a different address. Sticky Sessions only earn their place if you set a delivery ZIP through the location picker and need the follow-up page view to come from the same IP. That flow is explained in our guide to how sticky session proxies hold an IP.

Concurrency. SparkProxy plans are sold by threads, with unlimited bandwidth on every plan. The Starter plan's 100 threads are far more than a tracker needs. A daily job gains nothing from running hot, and a burst of parallel hits is the easiest pattern for any anti-bot system to spot. Six workers is the default below, and you can raise it once your fetch_log shows a low challenge rate.

Be clear about what a proxy can and cannot fix here. This table saves a lot of misdirected debugging:

Symptom in your dataCauseDoes a proxy fix it?
Interstitial or bot-check page, often HTTP 200IP reputation or request rateYes: rotate and retry, or move to a residential route
HTTP 503 after a burstRate limiting on one IPYes, together with lower concurrency
Price differs from what you see in your browserDelivery ZIP or Prime/login stateNo. Pin one ZIP, compare logged-out to logged-out
Empty price on a page that loaded fineSelector drift, or no Buy Box winnerNo. Fix the parser or record NULL
HTTP 404 "dogs" pageASIN removed or not sold in this marketplaceNo. Stop requesting it
Parent ASIN shows a price rangeVariation family, not one productNo. Track the child ASIN

ZIP pinning has its own write-up in our Amazon product data scraping guide, along with legal framing and search-page pagination. Neither is repeated here.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Fetching One ASIN Per Request

The fetcher does exactly one thing: make one request through the rotating gateway and return the status and body. It opens a fresh requests.Session per call so no Amazon cookie from a challenged attempt follows the next one.

# fetch.py: one GET per attempt through the rotating US gateway
import os
import requests

USER = os.environ["SPARK_USER"]      # copy both from the SparkProxy dashboard
PASSWORD = os.environ["SPARK_PASS"]
ROTATING = f"http://{USER}:{PASSWORD}@gateway.sparkproxy.io:11000"

HEADERS = {
    "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                   "(KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"),
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
}


def fetch_via_proxy(asin: str) -> tuple[int, str]:
    """Return (http_status, html). A fresh session per call means no cookie
    carry-over, so each attempt looks like a new visitor on a new IP."""
    with requests.Session() as s:
        r = s.get(
            f"https://www.amazon.com/dp/{asin}",
            headers=HEADERS,
            proxies={"http": ROTATING, "https": ROTATING},
            timeout=30,
        )
        return r.status_code, r.text

Request the bare /dp/ URL. The long, keyword-stuffed URLs you copy from a browser carry ref= and tracking parameters that change nothing about the price and make your request log harder to read. If you whitelist your server's IP in the dashboard instead of using user:pass, drop the credentials from ROTATING and keep the host and port.

Before trusting the gateway, confirm rotation from a shell. Two calls should print two different addresses:

curl -s -x "http://$SPARK_USER:$SPARK_PASS@gateway.sparkproxy.io:11000" https://api.ipify.org; echo
curl -s -x "http://$SPARK_USER:$SPARK_PASS@gateway.sparkproxy.io:11000" https://api.ipify.org; echo

Catching the Robot Check Before It Poisons Your Data

This is the part that quietly ruins most homemade trackers. When Amazon decides a visitor looks automated, it doesn't always send an error. It sends a small page with a 200 status, and a scraper that trusts the status code parses it, finds no price, and writes a NULL or, worse, a zero into your history.

We captured what that page looks like today. On 2026-10-30 we requested two product pages directly from one of our own servers, with no proxy, a desktop Chrome User-Agent and Accept-Language: en-US. Both requests got the same response: HTTP 200, 3,781 bytes, with these features:

  • </code> is just <code>Amazon.com</code>, not "Robot Check".</li> <li>The visible heading reads "Click the button below to continue shopping".</li> <li>The form posts to <code>/errors_page/validateCaptcha</code>, not <code>/errors/validateCaptcha</code>.</li> <li>An HTML comment begins "To discuss automated access to Amazon data please contact api-services-support@amazon.com."</li> <li>There is no image CAPTCHA and no "Enter the characters you see below" text.</li> </ul> <p>That matters because the marker list most Amazon tutorials ship, including older code on this blog, checks for "Robot Check", "Enter the characters you see below", "/errors/validateCaptcha" and "Type the characters you see in this image". We ran that four-string check against the captured page and it matched none of them. A tracker using it would have stored this interstitial as a real product page. Two requests is a small sample, so treat this as one observed variant, not a census of every page Amazon serves.</p> <p>The fix has two halves: match on the substring that survived the change (<code>validateCaptcha</code> without the path), and require positive proof that you got a detail page before parsing anything.</p> <pre><code class="language-python"># detect.py: decide what Amazon actually sent back before anything is parsed INTERSTITIAL_MARKERS = ( "validateCaptcha", # matches /errors/ and /errors_page/ forms "Click the button below to continue shopping", "Enter the characters you see below", # older image-CAPTCHA variant "api-services-support@amazon.com", # comment on every bot page we have seen ) def classify(status: int, html: str) -> str: """Return one of: ok, interstitial, throttled, gone, unknown.""" if status == 404: return "gone" # dead or region-locked ASIN: do not retry if status in (429, 503): return "throttled" if any(m in html for m in INTERSTITIAL_MARKERS): return "interstitial" # often arrives with HTTP 200 if status == 200 and 'id="productTitle"' in html: return "ok" # positive proof this is a detail page return "unknown" # 200 but not a product page: treat as a miss </code></pre> <p>The <code>unknown</code> branch is the safety net. When Amazon ships a new interstitial that matches none of the markers, it still lacks <code>productTitle</code>, so it lands in <code>unknown</code> and gets retried instead of parsed. Blocklists age; a positive check ages much more slowly.</p> <p>Retrying is where Random Rotation pays off. Every attempt exits from a new IP, so a challenge on attempt one says little about attempt two:</p> <pre><code class="language-python"># retry.py: each attempt exits from a different IP, so retrying is cheap import random import time from detect import classify from fetch import fetch_via_proxy RETRYABLE = {"interstitial", "throttled", "unknown", "neterror"} def get_product_page(asin: str, attempts: int = 4) -> tuple[str, str | None, int]: """Return (outcome, html_or_None, attempts_used).""" outcome = "neterror" for n in range(1, attempts + 1): try: status, html = fetch_via_proxy(asin) outcome = classify(status, html) except Exception: # timeouts, proxy resets, TLS errors outcome, html = "neterror", "" if outcome == "ok": return outcome, html, n if outcome not in RETRYABLE: return outcome, None, n # "gone" ends here time.sleep(min(30, 2 ** n) + random.uniform(0, 1.5)) return outcome, None, attempts </code></pre> <p>A 404 ends the loop at once. Retrying a removed ASIN four times a day for months is pure waste, and the <code>fetch_log</code> table below lets you find and prune those rows. For a broader catalogue of block signals beyond Amazon, see <a href="https://www.sparkproxy.io/blog/how-to-detect-when-your-scraper-is-blocked">how to detect when your scraper is blocked</a>.</p></div><!--[--><!--]--></section><!----><!--]--><!--[--><section id="parsing-price-bsr-and-the-buy-box-seller" class="scroll-mt-28"><span class="mt-12 block h-1 w-10 rounded-full bg-brand" aria-hidden="true"></span><h2 class="group/h relative font-extrabold text-navy mb-4 mt-3 text-2xl md:text-[1.7rem]"><a href="#parsing-price-bsr-and-the-buy-box-seller" class="absolute -left-6 top-1 hidden text-brand opacity-0 transition-opacity group-hover/h:opacity-100 lg:inline" aria-hidden="true">#</a> Parsing Price, BSR and the Buy Box Seller</h2><!----><div class="blog-content"><p>One dependency change will bite anyone copying older Amazon code. selectolax 1.0.0 shipped on PyPI on 2026-10-03, and in 1.0.x the classic import now fails outright. We installed 1.0.2 (released 2026-10-22, the same day as the repository's latest commit) and <code>from selectolax.parser import HTMLParser</code> raised:</p> <pre><code class="language-text">ImportError: Modest backend is deprecated since selectolax 1.0. The engine has not been updated since 2021, while lexbor has been available since 2021 and is actively developed. Modest is outdated, not maintained, contains bugs and does not follow modern HTML5 standards. Please use lexbor backend instead: `from selectolax.lexbor import LexborHTMLParser`. The APIs are nearly identical, so in most projects swapping the import is all that's needed. </code></pre> <p>The parser below uses the lexbor backend. Two more choices protect the data. Each field tries a list of selectors, because Amazon serves several page templates at once. And <code>text(separator=" ", strip=True)</code> keeps nested spans from gluing together, which matters for the BSR block, where "#1,234 in Kitchen" and "#12 in Electric Kettles" sit in sibling spans.</p> <pre><code class="language-python"># parse.py: selectolax 1.x moved to the lexbor backend import re from decimal import Decimal, InvalidOperation from selectolax.lexbor import LexborHTMLParser PRICE_SELECTORS = ( "#corePrice_feature_div .a-offscreen", "#corePriceDisplay_desktop_feature_div .a-offscreen", "#apex_desktop .a-offscreen", ".a-price .a-offscreen", ) SELLER_SELECTORS = ( "#sellerProfileTriggerId", "#merchantInfoFeature_feature_div .offer-display-feature-text-message", "#merchant-info a", ) DETAIL_BLOCKS = ( "#productDetails_detailBullets_sections1", "#detailBulletsWrapper_feature_div", "#prodDetails", ) BSR_RE = re.compile(r"#([\d,]+)\s+in\s+([^(#]+?)\s*(?:\(|#|$)") def first_text(tree, selectors): for sel in selectors: node = tree.css_first(sel) if node: text = node.text(separator=" ", strip=True) if text: return text return None def to_cents(raw: str | None) -> int | None: if not raw: return None m = re.search(r"\d[\d,]*(?:\.\d{2})?", raw) if not m: return None try: return int(Decimal(m.group(0).replace(",", "")) * 100) except InvalidOperation: return None def parse_bsr(tree) -> tuple[int | None, str | None]: for sel in DETAIL_BLOCKS: node = tree.css_first(sel) if not node: continue text = node.text(separator=" ", strip=True) if "Best Sellers Rank" not in text: continue m = BSR_RE.search(text.split("Best Sellers Rank", 1)[1]) if m: return int(m.group(1).replace(",", "")), m.group(2).strip() return None, None def parse_product(html: str) -> dict: tree = LexborHTMLParser(html) price_raw = first_text(tree, PRICE_SELECTORS) rank, category = parse_bsr(tree) avail = first_text(tree, ("#availability",)) return { "title": first_text(tree, ("#productTitle",)), "price_cents": to_cents(price_raw), "price_raw": price_raw, "bsr": rank, "bsr_category": category, "seller": first_text(tree, SELLER_SELECTORS), "availability": avail, "buy_box": tree.css_first("#add-to-cart-button") is not None, } </code></pre> <p><code>Decimal</code> avoids the float trap where <code>19.99 * 100</code> becomes <code>1998.9999999999998</code> and truncates to 1998 cents. The BSR regex keeps only the top-level rank, the first <code>#N in Category</code> after the "Best Sellers Rank" label; extend it if you also want the sub-category ranks.</p> <p>A caveat on freshness: on the day we wrote this, our direct requests only ever reached the interstitial, so these selectors were tested against a fixture page built from Amazon's known markup, not against a live product page. They are the commonly used ones, but treat them as candidates. The <code>ok</code> gate in <code>detect.py</code> and the <code>price_raw</code> column mean a broken selector shows up as a run of NULL prices on pages that otherwise loaded, which is easy to spot and never mistaken for a price drop.</p></div><!--[--><!--]--></section><!----><!--]--><!--[--><section id="a-sqlite-schema-for-daily-snapshots" class="scroll-mt-28"><span class="mt-12 block h-1 w-10 rounded-full bg-brand" aria-hidden="true"></span><h2 class="group/h relative font-extrabold text-navy mb-4 mt-3 text-2xl md:text-[1.7rem]"><a href="#a-sqlite-schema-for-daily-snapshots" class="absolute -left-6 top-1 hidden text-brand opacity-0 transition-opacity group-hover/h:opacity-100 lg:inline" aria-hidden="true">#</a> A SQLite Schema for Daily Snapshots</h2><!----><div class="blog-content"><p>SQLite is the right database for this until you are tracking a very large catalogue: it's a single file, ships with Python, and handles one writer with ease. Two tables keep data and diagnostics apart.</p> <pre><code class="language-python"># store.py: one row per ASIN per UTC day, plus a log of every attempt import sqlite3 from datetime import datetime, timezone SCHEMA = """ CREATE TABLE IF NOT EXISTS snapshot ( asin TEXT NOT NULL, day TEXT NOT NULL, -- YYYY-MM-DD, UTC price_cents INTEGER, -- NULL = no Buy Box price, never 0 bsr INTEGER, bsr_category TEXT, seller TEXT, buy_box INTEGER NOT NULL, availability TEXT, fetched_at TEXT NOT NULL, PRIMARY KEY (asin, day) ); CREATE TABLE IF NOT EXISTS fetch_log ( asin TEXT NOT NULL, at TEXT NOT NULL, outcome TEXT NOT NULL, -- ok, interstitial, gone, ... attempts INTEGER NOT NULL, route TEXT NOT NULL -- proxy or api ); """ def connect(path: str = "prices.db") -> sqlite3.Connection: db = sqlite3.connect(path, timeout=30) db.execute("PRAGMA journal_mode=WAL") db.executescript(SCHEMA) return db def save_snapshot(db, asin: str, row: dict) -> None: now = datetime.now(timezone.utc) db.execute( """INSERT INTO snapshot VALUES (?,?,?,?,?,?,?,?,?) ON CONFLICT(asin, day) DO UPDATE SET price_cents=excluded.price_cents, bsr=excluded.bsr, bsr_category=excluded.bsr_category, seller=excluded.seller, buy_box=excluded.buy_box, availability=excluded.availability, fetched_at=excluded.fetched_at""", (asin, now.strftime("%Y-%m-%d"), row["price_cents"], row["bsr"], row["bsr_category"], row["seller"], int(row["buy_box"]), row["availability"], now.isoformat(timespec="seconds")), ) db.commit() def log_fetch(db, asin: str, outcome: str, attempts: int, route: str) -> None: db.execute("INSERT INTO fetch_log VALUES (?,?,?,?,?)", (asin, datetime.now(timezone.utc).isoformat(timespec="seconds"), outcome, attempts, route)) db.commit() </code></pre> <p>The primary key <code>(asin, day)</code> makes a rerun idempotent. If the job crashes halfway and you start it again, the second pass overwrites today's rows instead of doubling them. WAL mode lets you open the database in a notebook while the job is writing.</p> <p>Three states need distinct representations, and conflating them is the most common data bug in DIY trackers:</p> <div class="table-wrap"><table><thead><tr><th>State</th><th>How it's stored</th><th>What the chart does</th></tr></thead><tbody><tr><td>Page loaded, Buy Box price found</td><td>Row with `price_cents` set</td><td>Draws the step</td></tr><tr><td>Page loaded, no Buy Box (suppressed or out of stock)</td><td>Row with `price_cents` NULL, `buy_box` 0</td><td>Breaks the line</td></tr><tr><td>Page never loaded (all attempts failed)</td><td>No row; a `fetch_log` entry instead</td><td>Carries the last price forward</td></tr></tbody></table></div> <p>Once a week, this query tells you whether your proxy route is healthy, which ASINs are dead, and whether any are leaning on the paid fallback:</p> <pre><code class="language-sql">SELECT asin, outcome, route, COUNT(*) AS n, ROUND(AVG(attempts), 2) AS avg_attempts FROM fetch_log WHERE at >= date('now', '-7 day') GROUP BY asin, outcome, route ORDER BY n DESC; </code></pre> <p>For a multi-retailer system with raw-HTML archiving and Postgres, the <a href="https://www.sparkproxy.io/blog/how-to-build-an-automated-price-monitoring-system-with-proxy-rotation">automated price monitoring architecture post</a> covers that bigger design. A single-marketplace ASIN tracker doesn't need it.</p></div><!--[--><!--]--></section><!----><!--]--><!--[--><section id="charting-price-history-like-keepa" class="scroll-mt-28"><span class="mt-12 block h-1 w-10 rounded-full bg-brand" aria-hidden="true"></span><h2 class="group/h relative font-extrabold text-navy mb-4 mt-3 text-2xl md:text-[1.7rem]"><a href="#charting-price-history-like-keepa" class="absolute -left-6 top-1 hidden text-brand opacity-0 transition-opacity group-hover/h:opacity-100 lg:inline" aria-hidden="true">#</a> Charting Price History Like Keepa</h2><!----><div class="blog-content"><p>A Keepa-style history works best as a step chart, and the reason is worth stating: a price is a state that holds until the seller changes it, not a sample that drifts smoothly between readings. Drawing straight lines between daily points invents prices nobody ever charged. Matplotlib's <code>drawstyle="steps-post"</code> holds each value flat until the next observation, which is the honest picture.</p> <pre><code class="language-python"># chart.py: a step chart, because a price is a state that holds until it changes import sqlite3 from datetime import date, timedelta import matplotlib matplotlib.use("Agg") # no display needed on a server import matplotlib.pyplot as plt def plot_history(db: sqlite3.Connection, asin: str, target_cents: int | None, out: str | None = None, days: int = 180) -> str: since = (date.today() - timedelta(days=days)).isoformat() rows = db.execute( "SELECT day, price_cents FROM snapshot WHERE asin=? AND day>=? ORDER BY day", (asin, since), ).fetchall() xs = [date.fromisoformat(d) for d, _ in rows] ys = [p / 100 if p is not None else float("nan") for _, p in rows] fig, ax = plt.subplots(figsize=(9, 3.5), layout="constrained") ax.plot(xs, ys, drawstyle="steps-post", linewidth=1.8) if target_cents: ax.axhline(target_cents / 100, linestyle="--", linewidth=1) ax.set_title(f"{asin}: Buy Box price, last {days} days") ax.set_ylabel("USD") ax.grid(alpha=0.3) out = out or f"{asin}.png" fig.savefig(out, dpi=120) plt.close(fig) return out </code></pre> <p>The NULL-to-<code>nan</code> conversion is what produces the gap for no-Buy-Box days, so a suppressed listing is visible at a glance instead of being painted as a continuous price. The <code>Agg</code> backend lets the script run under cron on a headless box. To overlay BSR, add <code>ax2 = ax.twinx()</code> and plot the <code>bsr</code> column on an inverted axis, since a smaller rank is better.</p></div><!--[--><!--]--></section><!----><!--]--><!--[--><section id="threshold-alerts-that-fire-once" class="scroll-mt-28"><span class="mt-12 block h-1 w-10 rounded-full bg-brand" aria-hidden="true"></span><h2 class="group/h relative font-extrabold text-navy mb-4 mt-3 text-2xl md:text-[1.7rem]"><a href="#threshold-alerts-that-fire-once" class="absolute -left-6 top-1 hidden text-brand opacity-0 transition-opacity group-hover/h:opacity-100 lg:inline" aria-hidden="true">#</a> Threshold Alerts That Fire Once</h2><!----><div class="blog-content"><p>A naive alert ("price is below target") emails you every single day the deal lasts. What you want is the crossing: today at or below target, the previous known price above it.</p> <pre><code class="language-python"># alerts.py: fire on the crossing, not on every day the price stays low import smtplib from email.message import EmailMessage def crossed_below(db, asin: str, target_cents: int) -> tuple[bool, int | None]: rows = db.execute( """SELECT price_cents FROM snapshot WHERE asin=? AND price_cents IS NOT NULL ORDER BY day DESC LIMIT 2""", (asin,), ).fetchall() if not rows: return False, None today = rows[0][0] previous = rows[1][0] if len(rows) > 1 else None hit = today <= target_cents and (previous is None or previous > target_cents) return hit, today def send_alert(asin: str, label: str, price_cents: int, target_cents: int, chart_path: str) -> None: msg = EmailMessage() msg["Subject"] = f"{label} hit ${price_cents / 100:.2f} (target ${target_cents / 100:.2f})" msg["From"] = "tracker@sparkproxy.io" msg["To"] = "you@sparkproxy.io" msg.set_content(f"https://www.amazon.com/dp/{asin}") with open(chart_path, "rb") as f: msg.add_attachment(f.read(), maintype="image", subtype="png", filename=f"{asin}.png") with smtplib.SMTP("smtp.sparkproxy.io", 587) as smtp: smtp.starttls() smtp.login("tracker@sparkproxy.io", "APP_PASSWORD") smtp.send_message(msg) </code></pre> <p>The query skips NULL prices on purpose. If a product goes out of stock for three days and returns below target, the comparison is against the last real price, so you still get the alert. A first-ever observation already under target also alerts, which is what you want when you add an item mid-sale. Swap the SMTP block for a Slack or Discord webhook if email isn't your channel; the crossing logic stays the same.</p></div><!--[--><!--]--></section><!----><!--]--><!--[--><section id="scheduling-the-daily-run" class="scroll-mt-28"><span class="mt-12 block h-1 w-10 rounded-full bg-brand" aria-hidden="true"></span><h2 class="group/h relative font-extrabold text-navy mb-4 mt-3 text-2xl md:text-[1.7rem]"><a href="#scheduling-the-daily-run" class="absolute -left-6 top-1 hidden text-brand opacity-0 transition-opacity group-hover/h:opacity-100 lg:inline" aria-hidden="true">#</a> Scheduling the Daily Run</h2><!----><div class="blog-content"><p>The orchestrator fetches in parallel, then does every database write on the main thread. sqlite3 connections shouldn't be shared across threads, and keeping writes serial removes that whole class of bug.</p> <pre><code class="language-python"># run_daily.py: watchlist in, snapshots + charts + alerts out import csv import random from concurrent.futures import ThreadPoolExecutor from alerts import crossed_below, send_alert from api_fallback import fetch_via_api from chart import plot_history from parse import parse_product from retry import get_product_page from store import connect, log_fetch, save_snapshot WORKERS = 6 # well under the thread count on any SparkProxy plan USE_API_FALLBACK = True def collect(asin: str) -> tuple[str, str, int, str, dict | None]: outcome, html, tries = get_product_page(asin) route = "proxy" if outcome != "ok" and outcome != "gone" and USE_API_FALLBACK: outcome, html = fetch_via_api(asin) tries, route = tries + 1, "api" return asin, outcome, tries, route, parse_product(html) if html else None def main() -> None: with open("watchlist.csv", newline="") as f: items = {r["asin"]: r for r in csv.DictReader(f)} order = list(items) random.shuffle(order) # no fixed crawl pattern day to day db = connect() with ThreadPoolExecutor(max_workers=WORKERS) as pool: results = list(pool.map(collect, order)) for asin, outcome, tries, route, row in results: # all writes on one thread log_fetch(db, asin, outcome, tries, route) if row is None or row["title"] is None: continue save_snapshot(db, asin, row) target = int(items[asin]["target_cents"] or 0) hit, price = crossed_below(db, asin, target) if target else (False, None) if hit: path = plot_history(db, asin, target) send_alert(asin, items[asin]["label"], price, target, path) if __name__ == "__main__": main() </code></pre> <p>We ran this orchestrator end to end against stubbed fetches (one ASIN challenged then clean, one challenged on every attempt, one returning 404). The clean ASIN was stored after two attempts and alerted, the stubborn one fell through to the API route and was logged as a miss, and the 404 stopped after a single request.</p> <p>On Linux, schedule it with cron at an off-round minute, add a random delay so the start time drifts, and wrap it in <code>flock</code> so a slow run never overlaps the next one:</p> <pre><code class="language-bash"># crontab -e (server clock in UTC) SHELL=/bin/bash 17 6 * * * sleep $((RANDOM \% 1800)) && cd /opt/asin-tracker && flock -n run.lock .venv/bin/python run_daily.py >> run.log 2>&1 </code></pre> <p>Two cron traps live in that line. The <code>%</code> must be escaped as <code>\%</code>, or cron cuts the command off at that character. And <code>$RANDOM</code> is a bash feature: cron's default <code>/bin/sh</code> is dash on Debian and Ubuntu, where it expands to nothing, hence the <code>SHELL=/bin/bash</code> line. On Windows, Task Scheduler does the same job:</p> <pre><code class="language-bat">schtasks /Create /TN "ASIN tracker" /SC DAILY /ST 06:17 /TR "C:\asin-tracker\.venv\Scripts\python.exe C:\asin-tracker\run_daily.py" </code></pre> <p>Daily is the right cadence for most watchlists. Hourly checks multiply requests by 24, and a chart with 24 points a day is rarely what you open when deciding whether to buy or reprice. If a handful of ASINs genuinely need intraday tracking, give them their own watchlist and cron line instead of speeding up the whole job. <a href="https://www.sparkproxy.io/blog/how-to-schedule-and-automate-web-scrapers">Our guide to scheduling and automating scrapers</a> covers systemd timers and queue-based alternatives.</p></div><!--[--><!--]--></section><!----><!--]--><!--[--><section id="sending-stubborn-asins-to-the-scraping-api" class="scroll-mt-28"><span class="mt-12 block h-1 w-10 rounded-full bg-brand" aria-hidden="true"></span><h2 class="group/h relative font-extrabold text-navy mb-4 mt-3 text-2xl md:text-[1.7rem]"><a href="#sending-stubborn-asins-to-the-scraping-api" class="absolute -left-6 top-1 hidden text-brand opacity-0 transition-opacity group-hover/h:opacity-100 lg:inline" aria-hidden="true">#</a> Sending Stubborn ASINs to the Scraping API</h2><!----><div class="blog-content"><p>SparkProxy's rotating pools are datacenter IPs. The interstitial we captured came from a datacenter address with no proxy at all, on the very first request, so it says nothing about SparkProxy's pool specifically, but it is a fair warning about how Amazon can treat the category. The fallback route gives the ASINs that exhaust their four proxy attempts one more try through the Scraping API's residential pool.</p> <pre><code class="language-python"># api_fallback.py: residential retry through the SparkProxy Scraping API import base64 import os import requests from detect import classify API = "https://scrape.sparkproxy.io/api/v1" API_KEY = os.environ["SPARK_API_KEY"] def fetch_via_api(asin: str, render: bool = False) -> tuple[str, str | None]: params = { "url": f"https://www.amazon.com/dp/{asin}", "render_js": "true" if render else "false", "proxy_type": "premium", # residential pool "country_code": "US", # match the marketplace "json_response": "true", # envelope with status_code + base64 body } r = requests.get(API, headers={"X-API-Key": API_KEY}, params=params, timeout=200) if r.status_code != 200: # 429, 530 etc. come from the API itself return f"api_{r.status_code}", None env = r.json() html = base64.b64decode(env.get("body") or "").decode("utf-8", "replace") outcome = classify(env.get("status_code") or 0, html) return outcome, html if outcome == "ok" else None </code></pre> <p>Four details here are easy to get wrong:</p> <ul> <li><code>render_js</code> defaults to <code>true</code> in the API. A tracker that forgets to set it pays for a headless browser on every call. Start with <code>false</code> and only pass <code>render=True</code> for ASINs whose price block is missing from the plain fetch.</li> <li><code>json_response=true</code> is what returns the envelope with <code>job_id</code>, <code>status_code</code>, <code>duration_ms</code>, <code>credits_used</code> and <code>body</code>. The <code>body</code> is base64. Decode it before running <code>classify</code>, or the marker search runs against base64 text, never matches, and a challenge page gets through as <code>unknown</code> at best.</li> <li><code>format=json</code> is not the envelope. <code>format</code> only converts page content (html, md, mdx, json, screenshot, pdf), so it's the wrong parameter here.</li> <li>The same <code>classify</code> function runs on the decoded body. A residential IP lowers the odds of an interstitial; it doesn't make detection optional.</li> </ul> <p>The docs also list a <code>530</code> status for a failed scrape (with credits refunded) and <code>429</code> for rate or concurrency limits, which is why any non-200 from the API is logged as its own outcome. The full parameter list is in the <a href="https://www.sparkproxy.io/docs/scraping-api/">Scraping API docs</a>. For the wider decision between running proxies yourself and handing the fetch to an API, see <a href="https://www.sparkproxy.io/blog/web-scraping-api-vs-self-managed-proxies">web scraping API vs self-managed proxies</a>.</p></div><!--[--><!--]--></section><!----><!--]--><!--[--><section id="what-a-daily-run-costs" class="scroll-mt-28"><span class="mt-12 block h-1 w-10 rounded-full bg-brand" aria-hidden="true"></span><h2 class="group/h relative font-extrabold text-navy mb-4 mt-3 text-2xl md:text-[1.7rem]"><a href="#what-a-daily-run-costs" class="absolute -left-6 top-1 hidden text-brand opacity-0 transition-opacity group-hover/h:opacity-100 lg:inline" aria-hidden="true">#</a> What a Daily Run Costs</h2><!----><div class="blog-content"><p>The proxy side is a flat fee, because SparkProxy bills by plan and thread count with unlimited bandwidth. Prices below were read from sparkproxy.io on 2026-10-30, in US dollars, excluding VAT/GST:</p> <div class="table-wrap"><table><thead><tr><th>Plan</th><th>Threads</th><th>Monthly price (USD)</th></tr></thead><tbody><tr><td>Starter</td><td>100</td><td>$75</td></tr><tr><td>Core</td><td>250</td><td>$140</td></tr><tr><td>Boost</td><td>500</td><td>$240</td></tr><tr><td>Plus</td><td>1,000</td><td>$440</td></tr></tbody></table></div> <p>Term discounts apply on top: 5% for 3 months, 10% for 6 months, 15% for 12 months. A tracker running six workers never comes near the Starter plan's 100 threads, so Starter covers it whether you watch 50 ASINs or several thousand. There's also a 24-hour free trial with no credit card, enough to run the job a couple of times and read your own <code>fetch_log</code> before paying.</p> <p>The API fallback is metered in credits. From the credit table in the docs, read 2026-10-30:</p> <div class="table-wrap"><table><thead><tr><th>Fallback request</th><th>Credits</th></tr></thead><tbody><tr><td>`proxy_type=premium`, `render_js=false`</td><td>10</td></tr><tr><td>...plus `country_code=US`</td><td>15 total</td></tr><tr><td>`proxy_type=premium`, `render_js=true`, `country_code=US`</td><td>30 total</td></tr></tbody></table></div> <p>New accounts get 1,000 free credits, which buys about 66 plain premium US fallbacks. The cost depends entirely on how many ASINs fail the proxy route, so measure that before budgeting. If 5% of a 1,000-ASIN list falls through each day, that's 50 × 15 = 750 credits a day; at 20% it's 3,000. Your weekly <code>fetch_log</code> query gives you the real rate.</p> <p>Run time is the other budget. As a planning assumption, not a measurement: if an attempt averages 5 seconds and the average ASIN needs 1.5 attempts, 1,000 ASINs across 6 workers take about 1,000 × 1.5 × 5 / 6 ≈ 1,250 seconds, roughly 21 minutes. Replace the 5 and the 1.5 with numbers from your own logs after the first week.</p></div><!--[--><!--]--></section><!----><!--]--><!--]--><div class="mt-14"><h2 class="mb-5 text-2xl font-extrabold text-navy">Frequently asked questions</h2><div class="blog-content faq-block"><div class="faq-accordion"> <h2 class="faq-heading">FAQ</h2> <div class="faq-item open"> <button class="faq-question" aria-expanded="true">How often should an Amazon price tracker in Python check each ASIN?<span class="faq-icon" aria-hidden="true"></span></button> <div class="faq-answer"> <p>Once a day suits most watchlists: it matches the resolution people actually read a price chart at, and hourly checks cost 24 times the requests. Put the few ASINs that need intraday checks on a separate watchlist with its own schedule instead of speeding up the whole job.</p> </div> </div> <div class="faq-item"> <button class="faq-question" aria-expanded="false">Why does my Amazon scraper return HTTP 200 but no price?<span class="faq-icon" aria-hidden="true"></span></button> <div class="faq-answer"> <p>Usually because the 200 is an interstitial, not a product page. On 2026-10-30 we received one titled just "Amazon.com" with a form posting to <code>/errors_page/validateCaptcha</code>. Check for <code>validateCaptcha</code> and require <code>id="productTitle"</code> before parsing; if the page did load, the price selector has drifted or there is no Buy Box winner.</p> </div> </div> <div class="faq-item"> <button class="faq-question" aria-expanded="false">Do I need residential proxies to track Amazon prices?<span class="faq-icon" aria-hidden="true"></span></button> <div class="faq-answer"> <p>Not necessarily for every request. Start with rotating US datacenter proxies and retries, measure your challenge rate in <code>fetch_log</code>, and send only the ASINs that keep failing to a residential route such as the Scraping API with <code>proxy_type=premium</code>. That keeps metered spend tied to the hard cases.</p> </div> </div> <div class="faq-item"> <button class="faq-question" aria-expanded="false">Should I use Random Rotation or Sticky Sessions for Amazon?<span class="faq-icon" aria-hidden="true"></span></button> <div class="faq-answer"> <p>Use Random Rotation (port 11000) for product-page checks, since each fetch is one stateless request and a fresh IP per retry helps. Use Sticky Sessions (port 11002) only when a flow spans several requests that must share an IP, such as setting a delivery ZIP before loading the product page.</p> </div> </div> <div class="faq-item"> <button class="faq-question" aria-expanded="false">Can I track Amazon Best Sellers Rank history the same way?<span class="faq-icon" aria-hidden="true"></span></button> <div class="faq-answer"> <p>Yes. The parser pulls the top-level BSR and its category from the product details block, and the <code>bsr</code> column stores it daily next to the price. Plot it on an inverted secondary axis, because rank 1 is the best position.</p> </div> </div> <div class="faq-item"> <button class="faq-question" aria-expanded="false">How do I track prices on amazon.co.uk or amazon.de instead?<span class="faq-icon" aria-hidden="true"></span></button> <div class="faq-answer"> <p>Change the domain in the fetch URL, exit from the matching country (a Worldwide Rotating plan with a GB or DE country username, or <code>country_code=GB</code> or <code>DE</code> on the API), and store the currency alongside the price. Keep one database per marketplace so prices in different currencies never share a chart.</p> </div> </div> </div></div></div><div class="mt-12 flex flex-col gap-6 border-t border-gray-100 pt-8"><div class="flex flex-wrap justify-center gap-2"><!--[--><a href="https://www.sparkproxy.io/blog/tag/amazon-price-tracker-python" rel="noopener noreferrer" class="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600 transition hover:bg-brand hover:text-white">#amazon price tracker python</a><a href="https://www.sparkproxy.io/blog/tag/amazon-price-history" rel="noopener noreferrer" class="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600 transition hover:bg-brand hover:text-white">#amazon price history</a><a href="https://www.sparkproxy.io/blog/tag/asin-tracker" rel="noopener noreferrer" class="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600 transition hover:bg-brand hover:text-white">#asin tracker</a><a href="https://www.sparkproxy.io/blog/tag/best-sellers-rank-tracking" rel="noopener noreferrer" class="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600 transition hover:bg-brand hover:text-white">#best sellers rank tracking</a><a href="https://www.sparkproxy.io/blog/tag/sqlite-price-tracker" rel="noopener noreferrer" class="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600 transition hover:bg-brand hover:text-white">#sqlite price tracker</a><a href="https://www.sparkproxy.io/blog/tag/amazon-robot-check" rel="noopener noreferrer" class="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600 transition hover:bg-brand hover:text-white">#amazon robot check</a><a href="https://www.sparkproxy.io/blog/tag/rotating-proxies" rel="noopener noreferrer" class="rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-600 transition hover:bg-brand hover:text-white">#rotating proxies</a><!--]--></div><div class="flex gap-2 flex-wrap items-center justify-center"><!--[--><a href="https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.sparkproxy.io%2Fblog%2Famazon-price-tracker-python" target="_blank" rel="noopener" aria-label="Share on Facebook" title="Facebook" class="inline-flex h-9 w-9 items-center justify-center rounded-full text-white shadow-sm transition-transform hover:scale-110" style="background-color:#1877F2;"><svg class="h-[18px] w-[18px]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M22 12a10 10 0 10-11.56 9.88v-6.99H7.9V12h2.54V9.8c0-2.5 1.49-3.89 3.78-3.89 1.09 0 2.24.2 2.24.2v2.46h-1.26c-1.24 0-1.63.77-1.63 1.56V12h2.78l-.44 2.89h-2.34v6.99A10 10 0 0022 12z"></path></svg></a><a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.sparkproxy.io%2Fblog%2Famazon-price-tracker-python&text=Build%20Your%20Own%20Amazon%20Price%20Tracker%20in%20Python" target="_blank" rel="noopener" aria-label="Share on X" title="X" class="inline-flex h-9 w-9 items-center justify-center rounded-full text-white shadow-sm transition-transform hover:scale-110" style="background-color:#111827;"><svg class="h-[18px] w-[18px]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></svg></a><a href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.sparkproxy.io%2Fblog%2Famazon-price-tracker-python" target="_blank" rel="noopener" aria-label="Share on LinkedIn" title="LinkedIn" class="inline-flex h-9 w-9 items-center justify-center rounded-full text-white shadow-sm transition-transform hover:scale-110" style="background-color:#0A66C2;"><svg class="h-[18px] w-[18px]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.5 2h-17A1.5 1.5 0 002 3.5v17A1.5 1.5 0 003.5 22h17a1.5 1.5 0 001.5-1.5v-17A1.5 1.5 0 0020.5 2zM8 19H5v-9h3zM6.5 8.25A1.75 1.75 0 118.3 6.5a1.78 1.78 0 01-1.8 1.75zM19 19h-3v-4.74c0-1.42-.6-1.93-1.38-1.93A1.74 1.74 0 0013 14.19a.66.66 0 000 .14V19h-3v-9h2.9v1.3a3.11 3.11 0 012.7-1.4c1.55 0 3.36.86 3.36 3.66z"></path></svg></a><a href="https://api.whatsapp.com/send?text=Build%20Your%20Own%20Amazon%20Price%20Tracker%20in%20Python%20https%3A%2F%2Fwww.sparkproxy.io%2Fblog%2Famazon-price-tracker-python" target="_blank" rel="noopener" aria-label="Share on WhatsApp" title="WhatsApp" class="inline-flex h-9 w-9 items-center justify-center rounded-full text-white shadow-sm transition-transform hover:scale-110" style="background-color:#25D366;"><svg class="h-[18px] w-[18px]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17.47 14.38c-.3-.15-1.76-.87-2.03-.97-.27-.1-.47-.15-.67.15-.2.3-.77.97-.94 1.16-.17.2-.35.22-.64.07-.3-.15-1.26-.46-2.4-1.48-.88-.79-1.48-1.76-1.65-2.06-.17-.3-.02-.46.13-.6.13-.14.3-.35.44-.52.15-.17.2-.3.3-.5.1-.2.05-.37-.02-.52-.08-.15-.67-1.61-.92-2.21-.24-.58-.49-.5-.67-.51h-.57c-.2 0-.52.07-.79.37-.27.3-1.04 1.02-1.04 2.48 0 1.46 1.07 2.88 1.22 3.08.15.2 2.1 3.2 5.08 4.49.71.3 1.26.49 1.69.63.71.22 1.36.19 1.87.12.57-.09 1.76-.72 2-1.41.25-.7.25-1.29.17-1.42-.07-.12-.27-.19-.57-.34zM12.05 21.8h-.01a9.87 9.87 0 01-5.03-1.38l-.36-.21-3.74.98 1-3.65-.24-.37a9.86 9.86 0 01-1.51-5.26C2.16 6.5 6.6 2.06 12.05 2.06c2.64 0 5.12 1.03 6.99 2.9a9.83 9.83 0 012.89 6.99c0 5.45-4.44 9.88-9.88 9.88zm8.41-18.3A11.82 11.82 0 0012.05.11C5.5.11.16 5.45.16 12.01c0 2.1.55 4.14 1.59 5.95L.06 24l6.31-1.65a11.88 11.88 0 005.68 1.45h.01c6.55 0 11.89-5.34 11.89-11.89 0-3.18-1.24-6.17-3.49-8.41z"></path></svg></a><a href="https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fwww.sparkproxy.io%2Fblog%2Famazon-price-tracker-python&description=Build%20Your%20Own%20Amazon%20Price%20Tracker%20in%20Python" target="_blank" rel="noopener" aria-label="Share on Pinterest" title="Pinterest" class="inline-flex h-9 w-9 items-center justify-center rounded-full text-white shadow-sm transition-transform hover:scale-110" style="background-color:#E60023;"><svg class="h-[18px] w-[18px]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12.02 0C5.4 0 .03 5.37.03 11.99c0 5.08 3.16 9.42 7.62 11.16-.1-.95-.2-2.4.04-3.44.22-.94 1.4-5.96 1.4-5.96s-.36-.72-.36-1.78c0-1.66.97-2.91 2.17-2.91 1.02 0 1.52.77 1.52 1.69 0 1.03-.65 2.57-.99 3.99-.28 1.19.6 2.16 1.77 2.16 2.13 0 3.77-2.24 3.77-5.49 0-2.86-2.06-4.87-5-4.87-3.41 0-5.41 2.56-5.41 5.2 0 1.03.4 2.14.89 2.74.1.12.11.22.08.34-.09.37-.29 1.2-.33 1.36-.05.22-.17.27-.4.16-1.5-.69-2.43-2.88-2.43-4.64 0-3.78 2.75-7.25 7.92-7.25 4.16 0 7.39 2.97 7.39 6.92 0 4.14-2.6 7.47-6.23 7.47-1.21 0-2.35-.63-2.75-1.38l-.75 2.85c-.27 1.04-1 2.35-1.5 3.15 1.12.34 2.3.53 3.55.53 6.6 0 11.98-5.37 11.98-11.99C24 5.37 18.63 0 12.02 0z"></path></svg></a><a href="https://t.me/share/url?url=https%3A%2F%2Fwww.sparkproxy.io%2Fblog%2Famazon-price-tracker-python&text=Build%20Your%20Own%20Amazon%20Price%20Tracker%20in%20Python" target="_blank" rel="noopener" aria-label="Share on Telegram" title="Telegram" class="inline-flex h-9 w-9 items-center justify-center rounded-full text-white shadow-sm transition-transform hover:scale-110" style="background-color:#26A5E4;"><svg class="h-[18px] w-[18px]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M11.94 0A12 12 0 000 12a12 12 0 0012 12 12 12 0 0012-12A12 12 0 0012 0a12 12 0 00-.06 0zm4.96 7.22c.1 0 .32.02.47.14a.5.5 0 01.17.33c.02.09.04.3.02.47-.18 1.9-.96 6.5-1.36 8.63-.17.9-.5 1.2-.82 1.23-.7.06-1.23-.46-1.9-.9-1.06-.7-1.65-1.13-2.68-1.8-1.18-.79-.42-1.22.26-1.92.18-.18 3.25-2.98 3.31-3.23.01-.03.01-.15-.06-.21-.07-.06-.17-.04-.25-.02-.1.02-1.79 1.14-5.06 3.34-.48.33-.91.5-1.3.48-.43 0-1.25-.24-1.86-.44-.75-.24-1.35-.37-1.3-.79.03-.21.33-.43.9-.66 3.5-1.52 5.83-2.53 7-3.01 3.33-1.39 4.02-1.63 4.47-1.63z"></path></svg></a><a href="https://www.reddit.com/submit?url=https%3A%2F%2Fwww.sparkproxy.io%2Fblog%2Famazon-price-tracker-python&title=Build%20Your%20Own%20Amazon%20Price%20Tracker%20in%20Python" target="_blank" rel="noopener" aria-label="Share on Reddit" title="Reddit" class="inline-flex h-9 w-9 items-center justify-center rounded-full text-white shadow-sm transition-transform hover:scale-110" style="background-color:#FF4500;"><svg class="h-[18px] w-[18px]" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 11.78c0-1.46-1.19-2.65-2.66-2.65-.71 0-1.36.29-1.84.75-1.81-1.19-4.26-1.95-6.97-2.05l1.48-4.67 4.02.94v.06c0 1.19.97 2.16 2.17 2.16s2.17-.97 2.17-2.16-.98-2.17-2.17-2.17c-.92 0-1.7.58-2.02 1.38l-4.33-1.01a.38.38 0 00-.44.24L11.98 6.6c-2.76.07-5.26.83-7.09 2.04-.47-.46-1.12-.75-1.84-.75C1.19 7.89 0 8.08 0 11.78c0 .96.52 1.8 1.29 2.26-.04.22-.06.44-.06.67 0 3.49 4.08 6.32 9.11 6.32s9.11-2.83 9.11-6.32c0-.22-.02-.44-.06-.66.78-.46 1.3-1.29 1.3-2.27zm-17.32 1.77c0-.83.68-1.51 1.51-1.51.83 0 1.51.68 1.51 1.51 0 .83-.68 1.51-1.51 1.51-.84 0-1.51-.68-1.51-1.51zm8.15 4.31c-1.02 1.02-2.96 1.1-3.53 1.1-.57 0-2.51-.08-3.53-1.1a.38.38 0 010-.53.38.38 0 01.53 0c.65.65 2.01.87 3 .87s2.36-.23 3-.87a.37.37 0 01.53 0 .37.37 0 010 .53zm-.31-2.8c-.84 0-1.51-.68-1.51-1.51 0-.83.68-1.51 1.51-1.51.83 0 1.51.68 1.51 1.51 0 .83-.68 1.51-1.51 1.51z"></path></svg></a><!--]--><button type="button" aria-label="Copy link" title="Copy link" class="inline-flex h-9 w-9 items-center justify-center rounded-full bg-gray-100 text-gray-500 transition hover:bg-navy hover:text-white"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"></path></svg></button></div></div><div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-[#e11d3a] via-brand to-[#8f1428] p-7 text-white shadow-xl shadow-brand/30 ring-1 ring-white/10 sm:p-9 mt-16" data-v-5ff897cc><div class="pointer-events-none absolute -right-16 -top-20 h-64 w-64 rounded-full bg-white/12 blur-3xl" aria-hidden="true" data-v-5ff897cc></div><div class="pointer-events-none absolute -bottom-24 left-8 h-52 w-52 rounded-full bg-black/20 blur-3xl" aria-hidden="true" data-v-5ff897cc></div><span class="promo-shine pointer-events-none absolute inset-y-0 left-0 w-1/4 bg-gradient-to-r from-transparent via-white/25 to-transparent" aria-hidden="true" data-v-5ff897cc></span><div class="relative flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between" data-v-5ff897cc><div data-v-5ff897cc><span class="mb-3 inline-flex items-center gap-1.5 rounded-full bg-white/15 px-3 py-1 text-xs font-extrabold uppercase tracking-widest ring-1 ring-white/25" data-v-5ff897cc><svg class="h-3.5 w-3.5" viewBox="0 0 384 512" fill="currentColor" aria-hidden="true" data-v-5ff897cc><path d="M0 256L28.5 28c2-16 15.6-28 31.8-28H228.9c15 0 27.1 12.1 27.1 27.1c0 3.2-.6 6.5-1.7 9.5L208 160H347.3c20.2 0 36.7 16.4 36.7 36.7c0 7.4-2.2 14.6-6.4 20.7l-192.2 281c-5.9 8.6-15.6 13.6-25.9 13.6h-2.9c-15.7 0-28.5-12.8-28.5-28.5c0-2.3 .3-4.6 .9-6.9L176 288H32c-17.7 0-32-14.3-32-32z" data-v-5ff897cc></path></svg> Special Discount · 20% off</span><p class="text-2xl font-extrabold leading-tight sm:text-[1.9rem]" data-v-5ff897cc>Get 20% off your first month</p><p class="mt-2 max-w-xl text-white/85" data-v-5ff897cc>Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.</p><p class="mt-2 text-xs font-semibold uppercase tracking-wide text-white/60" data-v-5ff897cc>Save up to 15% more on quarterly, half-yearly and yearly plans</p></div><div class="flex shrink-0 items-center gap-4" data-v-5ff897cc><div class="hidden rounded-xl border border-dashed border-white/45 bg-white/10 px-5 py-2.5 text-center sm:block" data-v-5ff897cc><span class="block text-[10px] font-bold uppercase tracking-widest text-white/70" data-v-5ff897cc>Code</span><span class="text-xl font-extrabold tracking-[0.22em]" data-v-5ff897cc>SPARK20</span></div><a href="/#pricing" class="group inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl bg-white px-7 py-3.5 text-sm font-extrabold text-brand shadow-lg transition hover:-translate-y-0.5 hover:shadow-xl" data-v-5ff897cc>Claim Discount <svg class="h-4 w-4 transition-transform group-hover:translate-x-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true" data-v-5ff897cc><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M13 6l6 6-6 6" data-v-5ff897cc></path></svg></a></div></div></div><div class="mt-12"><div id="about-the-author" class="scroll-mt-28 rounded-2xl bg-navy p-7 text-white shadow-lg shadow-navy/25 sm:p-8"><h2 class="mb-3 flex items-center gap-2.5 text-xl font-extrabold text-white"><span class="inline-block h-5 w-1 rounded-full bg-brand"></span> About the Author</h2><div class="blog-content author-bio"><p><strong>SparkProxy Technical Team.</strong> SparkProxy builds and runs rotating datacenter proxy pools in the US and 80+ countries, plus a managed Scraping API with residential routing. Every code block in this guide compiles under Python 3.14, and the orchestrator was run end to end against stubbed fetches using requests 2.34.2, selectolax 1.0.2 and matplotlib 3.11.2. The Amazon interstitial described above was captured on 2026-10-30.</p> <p><em>Sources: <a href="https://www.sparkproxy.io/docs/scraping-api/">SparkProxy Scraping API docs</a> · <a href="https://www.sparkproxy.io/">SparkProxy plans and pools</a> · <a rel="noopener noreferrer" target="_blank" href="https://pypi.org/project/selectolax/">selectolax on PyPI</a> · <a rel="noopener noreferrer" target="_blank" href="https://pypi.org/project/matplotlib/">matplotlib on PyPI</a> · <a rel="noopener noreferrer" target="_blank" href="https://pypi.org/project/requests/">requests on PyPI</a></em></p></div></div></div></div><aside class="hidden lg:block"><div class="sidebar-scroll sticky top-24 max-h-[calc(100vh-7rem)] overflow-y-auto overscroll-contain pr-1"><nav aria-label="On this page" class="rounded-2xl bg-brand p-6 text-white shadow-lg shadow-brand/25 mb-6"><p class="mb-4 flex items-center gap-2.5 border-b border-white/20 pb-3 text-sm font-extrabold uppercase tracking-widest"><svg class="h-4 w-4 shrink-0" viewBox="0 0 512 512" fill="currentColor" aria-hidden="true"><path d="M64 144a48 48 0 1 0 0-96 48 48 0 1 0 0 96zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zM64 464a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm48-208a48 48 0 1 0 -96 0 48 48 0 1 0 96 0z"></path></svg> On this page </p><ul class="space-y-0.5 border-l border-white/25"><!--[--><li><a href="#what-the-tracker-does-end-to-end" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">What the Tracker Does, End to End</a></li><li><a href="#proxy-choices-for-amazoncom" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">Proxy Choices for amazon.com</a></li><li><a href="#fetching-one-asin-per-request" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">Fetching One ASIN Per Request</a></li><li><a href="#catching-the-robot-check-before-it-poisons-your-data" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">Catching the Robot Check Before It Poisons Your Data</a></li><li><a href="#parsing-price-bsr-and-the-buy-box-seller" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">Parsing Price, BSR and the Buy Box Seller</a></li><li><a href="#a-sqlite-schema-for-daily-snapshots" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">A SQLite Schema for Daily Snapshots</a></li><li><a href="#charting-price-history-like-keepa" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">Charting Price History Like Keepa</a></li><li><a href="#threshold-alerts-that-fire-once" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">Threshold Alerts That Fire Once</a></li><li><a href="#scheduling-the-daily-run" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">Scheduling the Daily Run</a></li><li><a href="#sending-stubborn-asins-to-the-scraping-api" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">Sending Stubborn ASINs to the Scraping API</a></li><li><a href="#what-a-daily-run-costs" class="pl-3 text-sm font-semibold border-transparent text-white/65 hover:text-white -ml-px block border-l-2 py-1 leading-snug transition-colors">What a Daily Run Costs</a></li><!--]--></ul></nav><div class="relative overflow-hidden rounded-2xl bg-gradient-to-br from-[#e11d3a] via-brand to-[#8f1428] p-5 text-white shadow-xl shadow-brand/30 ring-1 ring-white/10 mb-6" data-v-5ff897cc><div class="pointer-events-none absolute -right-10 -top-12 h-32 w-32 rounded-full bg-white/15 blur-2xl" aria-hidden="true" data-v-5ff897cc></div><span class="promo-shine pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-transparent via-white/25 to-transparent" aria-hidden="true" data-v-5ff897cc></span><div class="relative" data-v-5ff897cc><span class="mb-2.5 inline-flex items-center gap-1.5 rounded-full bg-white/15 px-2.5 py-1 text-[10px] font-extrabold uppercase tracking-widest ring-1 ring-white/25" data-v-5ff897cc><svg class="h-3 w-3" viewBox="0 0 384 512" fill="currentColor" aria-hidden="true" data-v-5ff897cc><path d="M0 256L28.5 28c2-16 15.6-28 31.8-28H228.9c15 0 27.1 12.1 27.1 27.1c0 3.2-.6 6.5-1.7 9.5L208 160H347.3c20.2 0 36.7 16.4 36.7 36.7c0 7.4-2.2 14.6-6.4 20.7l-192.2 281c-5.9 8.6-15.6 13.6-25.9 13.6h-2.9c-15.7 0-28.5-12.8-28.5-28.5c0-2.3 .3-4.6 .9-6.9L176 288H32c-17.7 0-32-14.3-32-32z" data-v-5ff897cc></path></svg> Free trial</span><p class="text-lg font-extrabold leading-snug" data-v-5ff897cc>Try Datacentre proxies</p><p class="mt-1.5 text-sm leading-relaxed text-white/85" data-v-5ff897cc>250 threads, zero cost, instant access.</p><!----><a href="/#pricing" class="group mt-4 flex items-center justify-center gap-2 rounded-lg bg-white px-4 py-2.5 text-sm font-extrabold text-brand shadow-md transition hover:-translate-y-0.5 hover:shadow-lg" data-v-5ff897cc>Start Free Trial <svg class="h-4 w-4 transition-transform group-hover:translate-x-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true" data-v-5ff897cc><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M13 6l6 6-6 6" data-v-5ff897cc></path></svg></a><!----></div></div><div class="columns-1 gap-8 md:columns-2 lg:columns-1 [&>section]:mb-8 [&>section]:break-inside-avoid [&>section:last-child]:mb-0"><section><h3 class="mb-4 flex items-center gap-2 border-b border-gray-100 pb-2 text-xs font-extrabold uppercase tracking-widest text-navy"><span class="inline-block h-4 w-1 rounded-full bg-brand"></span> Popular Posts </h3><ul class="space-y-4"><!--[--><li><a href="https://www.sparkproxy.io/blog/top-12-antidetect-browsers-best-tools-for-privacy-multi-account-management" rel="noopener noreferrer" class="group flex gap-3"><div class="h-14 w-16 shrink-0 overflow-hidden rounded-lg bg-navy/5"><img src="https://blog.cms.sparkproxy.io/uploads/images/202609/image_140x98_6a966a19c6b8c.webp" alt="Best Antidetect Browsers: Top 12 Compared for 2026" width="64" height="56" loading="lazy" decoding="async" class="h-full w-full object-cover"></div><div class="min-w-0"><h4 class="line-clamp-2 text-sm font-bold leading-snug text-navy transition-colors group-hover:text-brand">Best Antidetect Browsers: Top 12 Compared for 2026</h4><time class="text-xs text-gray-400" datetime="2026-05-17T22:53:20+05:30">May 17, 2026</time></div></a></li><li><a href="https://www.sparkproxy.io/blog/what-is-proxy-server" rel="noopener noreferrer" class="group flex gap-3"><div class="h-14 w-16 shrink-0 overflow-hidden rounded-lg bg-navy/5"><img src="https://blog.cms.sparkproxy.io/uploads/images/202605/image_140x98_6a06ac2b0ceb5.webp" alt="What Is a Proxy Server? Meaning & How It Works (2026)" width="64" height="56" loading="lazy" decoding="async" class="h-full w-full object-cover"></div><div class="min-w-0"><h4 class="line-clamp-2 text-sm font-bold leading-snug text-navy transition-colors group-hover:text-brand">What Is a Proxy Server? Meaning & How It Works (2026)</h4><time class="text-xs text-gray-400" datetime="2026-05-15T10:46:54+05:30">May 15, 2026</time></div></a></li><li><a href="https://www.sparkproxy.io/blog/how-to-build-a-captcha-solver-with-machine-learning" rel="noopener noreferrer" class="group flex gap-3"><div class="h-14 w-16 shrink-0 overflow-hidden rounded-lg bg-navy/5"><img src="https://blog.cms.sparkproxy.io/uploads/images/202605/image_140x98_69ff7aae08194.webp" alt="CAPTCHA Solver with Machine Learning: Build & Troubleshoot (2026)" width="64" height="56" loading="lazy" decoding="async" class="h-full w-full object-cover"></div><div class="min-w-0"><h4 class="line-clamp-2 text-sm font-bold leading-snug text-navy transition-colors group-hover:text-brand">CAPTCHA Solver with Machine Learning: Build & Troubleshoot (2026)</h4><time class="text-xs text-gray-400" datetime="2026-05-09T23:53:54+05:30">May 9, 2026</time></div></a></li><li><a href="https://www.sparkproxy.io/blog/proxy-ports-explained-80-443-8080-and-more" rel="noopener noreferrer" class="group flex gap-3"><div class="h-14 w-16 shrink-0 overflow-hidden rounded-lg bg-navy/5"><img src="https://blog.cms.sparkproxy.io/uploads/images/202606/image_140x98_6a37d92fd9edc.webp" alt="Proxy Ports Explained: 80, 443, 8080 & Which to Use" width="64" height="56" loading="lazy" decoding="async" class="h-full w-full object-cover"></div><div class="min-w-0"><h4 class="line-clamp-2 text-sm font-bold leading-snug text-navy transition-colors group-hover:text-brand">Proxy Ports Explained: 80, 443, 8080 & Which to Use</h4><time class="text-xs text-gray-400" datetime="2026-06-21T17:56:45+05:30">Jun 21, 2026</time></div></a></li><li><a href="https://www.sparkproxy.io/blog/how-to-bypass-imperva-incapsula" rel="noopener noreferrer" class="group flex gap-3"><div class="h-14 w-16 shrink-0 overflow-hidden rounded-lg bg-navy/5"><img src="https://blog.cms.sparkproxy.io/uploads/images/202608/image_140x98_6a925ea20ee9a.webp" alt="How to Bypass Imperva (Incapsula) When Web Scraping" width="64" height="56" loading="lazy" decoding="async" class="h-full w-full object-cover"></div><div class="min-w-0"><h4 class="line-clamp-2 text-sm font-bold leading-snug text-navy transition-colors group-hover:text-brand">How to Bypass Imperva (Incapsula) When Web Scraping</h4><time class="text-xs text-gray-400" datetime="2026-08-01T14:00:02+05:30">Aug 1, 2026</time></div></a></li><!--]--></ul></section><section class="rounded-2xl bg-brand p-5 text-white shadow-lg shadow-brand/20"><h3 class="mb-4 flex items-center gap-2 border-b border-white/20 pb-2 text-xs font-extrabold uppercase tracking-widest text-white"><span class="inline-block h-4 w-1 rounded-full bg-white"></span> Follow Us </h3><div class="flex gap-3"><!--[--><a href="https://www.facebook.com/sparkproxy.io" target="_blank" rel="noopener noreferrer" aria-label="Facebook" title="Facebook" class="flex h-10 w-10 items-center justify-center rounded-lg bg-white text-navy shadow-sm transition-colors hover:bg-navy hover:text-white"><svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"></path></svg></a><a href="https://www.instagram.com/sparkproxy.io/" target="_blank" rel="noopener noreferrer" aria-label="Instagram" title="Instagram" class="flex h-10 w-10 items-center justify-center rounded-lg bg-white text-navy shadow-sm transition-colors hover:bg-navy hover:text-white"><svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.012-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163C8.741 0 8.332.014 7.052.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"></path></svg></a><a href="https://www.linkedin.com/company/sparkproxy/" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn" title="LinkedIn" class="flex h-10 w-10 items-center justify-center rounded-lg bg-white text-navy shadow-sm transition-colors hover:bg-navy hover:text-white"><svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.225 0z"></path></svg></a><!--]--></div></section><section><h3 class="mb-4 flex items-center gap-2 border-b border-gray-100 pb-2 text-xs font-extrabold uppercase tracking-widest text-navy"><span class="inline-block h-4 w-1 rounded-full bg-brand"></span> Recommended </h3><ul class="space-y-4"><!--[--><li><a href="https://www.sparkproxy.io/blog/how-to-bypass-kasada" rel="noopener noreferrer" class="group flex gap-3"><div class="h-14 w-16 shrink-0 overflow-hidden rounded-lg bg-navy/5"><img src="https://blog.cms.sparkproxy.io/uploads/images/202608/image_140x98_6a925ea719974.webp" alt="How to Bypass Kasada When Web Scraping" width="64" height="56" loading="lazy" decoding="async" class="h-full w-full object-cover"></div><div class="min-w-0"><h4 class="line-clamp-2 text-sm font-bold leading-snug text-navy transition-colors group-hover:text-brand">How to Bypass Kasada When Web Scraping</h4><time class="text-xs text-gray-400" datetime="2026-07-30T14:00:01+05:30">Jul 30, 2026</time></div></a></li><li><a href="https://www.sparkproxy.io/blog/how-to-bypass-perimeterx" rel="noopener noreferrer" class="group flex gap-3"><div class="h-14 w-16 shrink-0 overflow-hidden rounded-lg bg-navy/5"><img src="https://blog.cms.sparkproxy.io/uploads/images/202608/image_140x98_6a925eabbad1b.webp" alt="How to Bypass PerimeterX (HUMAN Security) When Scraping" width="64" height="56" loading="lazy" decoding="async" class="h-full w-full object-cover"></div><div class="min-w-0"><h4 class="line-clamp-2 text-sm font-bold leading-snug text-navy transition-colors group-hover:text-brand">How to Bypass PerimeterX (HUMAN Security) When Scraping</h4><time class="text-xs text-gray-400" datetime="2026-07-28T14:00:02+05:30">Jul 28, 2026</time></div></a></li><li><a href="https://www.sparkproxy.io/blog/guide-to-proxy-dns-leak-testing-and-mitigation" rel="noopener noreferrer" class="group flex gap-3"><div class="h-14 w-16 shrink-0 overflow-hidden rounded-lg bg-navy/5"><img src="https://blog.cms.sparkproxy.io/uploads/images/202605/image_140x98_6a157b4a65ae7.webp" alt="Guide to Proxy DNS Leak Testing and Mitigation" width="64" height="56" loading="lazy" decoding="async" class="h-full w-full object-cover"></div><div class="min-w-0"><h4 class="line-clamp-2 text-sm font-bold leading-snug text-navy transition-colors group-hover:text-brand">Guide to Proxy DNS Leak Testing and Mitigation</h4><time class="text-xs text-gray-400" datetime="2026-05-26T16:22:24+05:30">May 26, 2026</time></div></a></li><li><a href="https://www.sparkproxy.io/blog/comparison-of-residential-vs-datacenter-vs-mobile-proxy-types" rel="noopener noreferrer" class="group flex gap-3"><div class="h-14 w-16 shrink-0 overflow-hidden rounded-lg bg-navy/5"><img src="https://blog.cms.sparkproxy.io/uploads/images/202605/image_140x98_69fc216bebd4e.webp" alt="Residential vs Datacenter vs Mobile Proxy Comparison (2026)" width="64" height="56" loading="lazy" decoding="async" class="h-full w-full object-cover"></div><div class="min-w-0"><h4 class="line-clamp-2 text-sm font-bold leading-snug text-navy transition-colors group-hover:text-brand">Residential vs Datacenter vs Mobile Proxy Comparison (2026)</h4><time class="text-xs text-gray-400" datetime="2026-05-07T10:57:00+05:30">May 7, 2026</time></div></a></li><!--]--></ul></section><section class="rounded-2xl bg-brand p-5 text-white shadow-lg shadow-brand/20"><h3 class="mb-4 flex items-center gap-2 border-b border-white/20 pb-2 text-xs font-extrabold uppercase tracking-widest text-white"><span class="inline-block h-4 w-1 rounded-full bg-white"></span> Popular Tags </h3><div class="flex flex-wrap gap-2"><!--[--><a href="https://www.sparkproxy.io/blog/tag/web-scraping" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#web scraping</a><a href="https://www.sparkproxy.io/blog/tag/datacenter-proxies" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#datacenter proxies</a><a href="https://www.sparkproxy.io/blog/tag/residential-proxies" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#residential proxies</a><a href="https://www.sparkproxy.io/blog/tag/antidetect-browser" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#antidetect browser</a><a href="https://www.sparkproxy.io/blog/tag/scraping-api" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#scraping api</a><a href="https://www.sparkproxy.io/blog/tag/multi-account-management" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#multi-account management</a><a href="https://www.sparkproxy.io/blog/tag/web-scraping-api" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#web scraping api</a><a href="https://www.sparkproxy.io/blog/tag/proxy-comparison" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#proxy comparison</a><a href="https://www.sparkproxy.io/blog/tag/proxy-buying-guide" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#proxy buying guide</a><a href="https://www.sparkproxy.io/blog/tag/proxy-pricing" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#proxy pricing</a><a href="https://www.sparkproxy.io/blog/tag/browser-fingerprinting" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#browser fingerprinting</a><a href="https://www.sparkproxy.io/blog/tag/sticky-sessions" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#sticky sessions</a><a href="https://www.sparkproxy.io/blog/tag/kameleo" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#kameleo</a><a href="https://www.sparkproxy.io/blog/tag/multilogin" rel="noopener noreferrer" class="rounded-full bg-white/15 px-3 py-1 text-xs text-white transition hover:bg-white hover:text-brand">#multilogin</a><!--]--></div></section></div></div></aside></div></div><section class="section border-t border-gray-100 bg-gray-50"><div class="mx-auto max-w-[1440px] px-4 sm:px-6 md:px-8 lg:px-12"><div class="mb-10 text-center"><span class="mb-3 inline-block rounded-full bg-brand-light px-3 py-1 text-xs font-bold uppercase tracking-widest text-brand">Keep reading</span><h2 class="text-2xl font-bold text-navy md:text-3xl">Related articles</h2></div><div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"><!--[--><article class="group flex flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"><a href="https://www.sparkproxy.io/blog/foxyproxy-setup-username-password" rel="noopener noreferrer" class="block aspect-[16/9] overflow-hidden bg-navy/5"><!----></a><div class="flex flex-1 flex-col p-5"><h3 class="mb-2 line-clamp-2 min-h-[3.25rem] text-lg font-bold leading-snug text-navy"><a href="https://www.sparkproxy.io/blog/foxyproxy-setup-username-password" rel="noopener noreferrer" class="transition-colors group-hover:text-brand">FoxyProxy Setup With Username and Password</a></h3><p class="mb-4 line-clamp-2 min-h-[2.5rem] text-sm leading-relaxed text-gray-500">FoxyProxy setup for per-site routing: saved username and password, patterns that match, why Chrome rejects SOCKS5 auth, and how to stop the login popup.</p><div class="mt-auto flex flex-wrap items-center gap-2 text-xs text-gray-400"><span class="font-semibold text-gray-500">SparkProxy</span><span>·</span><time datetime="2026-11-07T15:00:01+05:30">Nov 7, 2026</time><span class="ml-auto rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white" style="background-color:#c51e39;">Use Cases</span></div></div></article><article class="group flex flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"><a href="https://www.sparkproxy.io/blog/test-gdpr-cookie-banner-from-eu" rel="noopener noreferrer" class="block aspect-[16/9] overflow-hidden bg-navy/5"><!----></a><div class="flex flex-1 flex-col p-5"><h3 class="mb-2 line-clamp-2 min-h-[3.25rem] text-lg font-bold leading-snug text-navy"><a href="https://www.sparkproxy.io/blog/test-gdpr-cookie-banner-from-eu" rel="noopener noreferrer" class="transition-colors group-hover:text-brand">Test Your GDPR Cookie Banner From the EU</a></h3><p class="mb-4 line-clamp-2 min-h-[2.5rem] text-sm leading-relaxed text-gray-500">Test cookie consent banner EU behaviour from outside Europe: load your site via EU exits with Playwright and catch cookies and pixels that fire before consent.</p><div class="mt-auto flex flex-wrap items-center gap-2 text-xs text-gray-400"><span class="font-semibold text-gray-500">SparkProxy</span><span>·</span><time datetime="2026-11-07T11:00:01+05:30">Nov 7, 2026</time><span class="ml-auto rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white" style="background-color:#c51e39;">Use Cases</span></div></div></article><article class="group flex flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"><a href="https://www.sparkproxy.io/blog/scrapebox-proxies-setup" rel="noopener noreferrer" class="block aspect-[16/9] overflow-hidden bg-navy/5"><!----></a><div class="flex flex-1 flex-col p-5"><h3 class="mb-2 line-clamp-2 min-h-[3.25rem] text-lg font-bold leading-snug text-navy"><a href="https://www.sparkproxy.io/blog/scrapebox-proxies-setup" rel="noopener noreferrer" class="transition-colors group-hover:text-brand">ScrapeBox Proxies: Load, Test, and Harvest Google</a></h3><p class="mb-4 line-clamp-2 min-h-[2.5rem] text-sm leading-relaxed text-gray-500">Set up ScrapeBox proxies that keep harvesting: the IP:PORT:USER:PASS format, loading a rotating gateway, what the Google test checks, and safe connections.</p><div class="mt-auto flex flex-wrap items-center gap-2 text-xs text-gray-400"><span class="font-semibold text-gray-500">SparkProxy</span><span>·</span><time datetime="2026-11-06T19:00:02+05:30">Nov 6, 2026</time><span class="ml-auto rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white" style="background-color:#c51e39;">Use Cases</span></div></div></article><!--]--></div></div></section></article></div><!--]--></main><button type="button" aria-label="Back to top" class="fixed bottom-24 right-5 z-40 flex h-11 w-11 items-center justify-center rounded-full bg-brand text-white shadow-lg shadow-brand/30 transition-colors hover:bg-brand-dark focus:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 sm:h-12 sm:w-12" style="display:none;" data-v-607a4033><svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" data-v-607a4033><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" data-v-607a4033></path></svg></button><footer class="bg-navy text-white pt-24 pb-8"><div class="max-w-[1440px] mx-auto px-4 sm:px-6 md:px-8 lg:px-12"><div class="relative rounded-2xl overflow-hidden mb-16"><div class="absolute inset-0 bg-gradient-to-br from-brand via-[#9b1530] to-[#6b0e20]"></div><div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(255,255,255,0.08),transparent_60%)]"></div><div class="absolute -top-16 -right-16 w-64 h-64 rounded-full bg-white/5 blur-2xl pointer-events-none"></div><div class="absolute -bottom-10 -left-10 w-48 h-48 rounded-full bg-white/5 blur-2xl pointer-events-none"></div><div class="relative px-8 py-12 md:px-14 md:py-14 flex flex-col md:flex-row items-center justify-between gap-10"><div class="text-center md:text-left max-w-lg"><span class="inline-block text-xs font-bold uppercase tracking-widest bg-white/15 text-white px-3 py-1 rounded-full mb-4">Access Any Network · Any Country · 24-Hour Free Trial</span><h2 class="text-3xl md:text-4xl font-extrabold leading-tight capitalize"> Any site. Any country.<br class="hidden md:block"> Zero blocks. </h2></div><div class="flex flex-col items-center md:items-end gap-6 shrink-0"><div class="flex gap-6"><div class="text-center"><div class="text-2xl font-extrabold">1M+</div><div class="text-white/55 text-xs uppercase tracking-wide">Premium IPs</div></div><div class="w-px bg-white/20"></div><div class="text-center"><div class="text-2xl font-extrabold">80+</div><div class="text-white/55 text-xs uppercase tracking-wide">Countries</div></div><div class="w-px bg-white/20"></div><div class="text-center"><div class="text-2xl font-extrabold">99.9%</div><div class="text-white/55 text-xs uppercase tracking-wide">Uptime</div></div></div><div class="flex flex-col sm:flex-row gap-3"><a href="https://app.sparkproxy.io/register?utm_source=sparkproxy&utm_medium=website&utm_campaign=cta&utm_content=footer-proxy-cta" target="_blank" rel="noopener" class="inline-flex items-center justify-center gap-2 bg-white text-brand hover:bg-gray-100 font-bold px-7 py-3.5 rounded-xl transition-all shadow-lg hover:shadow-xl text-sm"> Start Free Trial <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"></path></svg></a><a href="/#pricing" class="inline-flex items-center justify-center gap-2 bg-white/20 hover:bg-white/30 text-white font-bold px-7 py-3.5 rounded-xl transition-all border border-white/50 text-sm"> View Pricing </a></div></div></div></div><div class="mb-12 grid gap-10 lg:grid-cols-[1.3fr_3fr]"><div><a href="/" class="mb-4 inline-block"><img src="/logo_dark.webp" alt="SparkProxy" class="h-9 w-auto brightness-0 invert" width="77" height="36" loading="lazy"></a><p class="mb-4 max-w-xs text-sm leading-relaxed text-white/60">Scrape the Web with Confidence and Anonymity.</p><address class="text-xs not-italic leading-relaxed text-white/60"> 1, Chikkanna Layout, Hennur,<br> Bengaluru, 560043. India<br><a href="mailto:support@sparkproxy.io" class="text-red-300 transition-colors hover:text-red-200"> support@sparkproxy.io </a></address></div><div class="grid grid-cols-2 gap-8 md:grid-cols-4"><div><h3 class="mb-4 text-sm font-bold capitalize tracking-wider text-white">Quick Links</h3><ul class="space-y-2"><!--[--><li><a href="/free-premium-proxies/" class="text-sm text-white/60 transition-colors hover:text-brand">Free Premium Proxies</a></li><li><a href="/free-vpn/" class="text-sm text-white/60 transition-colors hover:text-brand">Free VPN</a></li><li><a href="/about/" class="text-sm text-white/60 transition-colors hover:text-brand">About Us</a></li><li><a href="/blog/" class="text-sm text-white/60 transition-colors hover:text-brand">Blog</a></li><li><a href="/#countries" class="text-sm text-white/60 transition-colors hover:text-brand">Locations</a></li><li><a href="/#pricing" class="text-sm text-white/60 transition-colors hover:text-brand">Pricing</a></li><!--]--></ul></div><div><h3 class="mb-4 text-sm font-bold capitalize tracking-wider text-white">Scraping API</h3><ul class="space-y-2"><li><a href="/scraping-api/" class="text-sm text-white/60 transition-colors hover:text-brand">Overview</a></li><li><a href="/docs/scraping-api/" class="text-sm text-white/60 transition-colors hover:text-brand">API Reference</a></li><li><a href="/docs/scraping-api/" class="text-sm text-white/60 transition-colors hover:text-brand">Documentation</a></li><li><a href="/docs/files/" class="text-sm text-white/60 transition-colors hover:text-brand">Async Results</a></li><li><a href="/scraping-api/#pricing" class="text-sm text-white/60 transition-colors hover:text-brand">Pricing</a></li></ul></div><div><h3 class="mb-4 text-sm font-bold capitalize tracking-wider text-white">Proxies</h3><ul class="space-y-2 text-sm text-white/60"><li>USA Rotating Proxies</li><li>Worldwide Rotating Proxies</li></ul><h3 class="mt-8 mb-4 text-sm font-bold capitalize tracking-wider text-white"><a href="/free-vpn/" class="transition-colors hover:text-brand">Free VPN</a></h3><ul class="space-y-2"><!--[--><li><a href="https://downloads.sparkproxy.io/SparkProxy-Windows-x64-Setup.exe" rel="noopener" class="text-sm text-white/60 transition-colors hover:text-brand">Hybrid VPN for Windows</a></li><li><a href="https://downloads.sparkproxy.io/SparkProxy-Android.apk" rel="noopener" class="text-sm text-white/60 transition-colors hover:text-brand">Hybrid VPN for Android</a></li><li><a href="https://downloads.sparkproxy.io/SparkProxy-Linux-x86_64.AppImage" rel="noopener" class="text-sm text-white/60 transition-colors hover:text-brand">Hybrid VPN for Linux (Intel/AMD)</a></li><li><a href="https://downloads.sparkproxy.io/SparkProxy-Linux-aarch64.AppImage" rel="noopener" class="text-sm text-white/60 transition-colors hover:text-brand">Hybrid VPN for Linux (ARM64)</a></li><!--]--></ul></div><div><h3 class="mb-4 text-sm font-bold capitalize tracking-wider text-white">Legal</h3><ul class="space-y-2"><!--[--><li><a href="/terms-of-use/" class="text-sm text-white/60 transition-colors hover:text-brand">Terms of Use</a></li><li><a href="/terms-of-use/#end-user-agreement" class="text-sm text-white/60 transition-colors hover:text-brand">End User Agreement</a></li><li><a href="/terms-of-use/#app-acceptable-use" class="text-sm text-white/60 transition-colors hover:text-brand">Acceptable Usage Policy</a></li><li><a href="/privacy-policy/" class="text-sm text-white/60 transition-colors hover:text-brand">Privacy Policy</a></li><li><a href="/refund-policy/" class="text-sm text-white/60 transition-colors hover:text-brand">Refund Policy</a></li><li><a href="/disclaimer/" class="text-sm text-white/60 transition-colors hover:text-brand">Disclaimer</a></li><li><a href="/cookie-policy/" class="text-sm text-white/60 transition-colors hover:text-brand">Cookie Policy</a></li><!--]--></ul></div></div></div><div class="border-t border-white/10 pt-8 flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-white/60"><p>Copyright © 2026. All Rights Reserved To SparkProxy.</p></div></div></footer></div><!----><!--]--></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{},app:{baseURL:"/",buildId:"2c07b580-ce7b-47da-89f1-8c828a5dca50",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script> <script type="application/json" data-nuxt-data="nuxt-app" data-ssr="true" id="__NUXT_DATA__">[["ShallowReactive",1],{"data":2,"state":682,"once":690,"_errors":691,"serverRendered":5,"path":693},["ShallowReactive",3],{"$f67pYq0kdLAGcy5sgC80vqLM66Ou-XjEZNkH-7pE-f3E":4,"$ftobFREwkbef1kEw4fzFEYg3CR0Z57HebcGPY6pmOlQk":199,"blog-sidebar":567},{"ok":5,"data":6},true,{"type":7,"slug":8,"post":9},"post","amazon-price-tracker-python",{"id":10,"slug":8,"url":11,"title":12,"summary":13,"image":14,"category":16,"author":21,"post_type":25,"views":26,"published_at":27,"updated_at":28,"content_html":29,"faq_html":30,"faq":31,"keywords":50,"optional_url":15,"is_feed_post":51,"reading_time":52,"tags":53,"related":81,"type_payload":114,"seo":198},624,"https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Famazon-price-tracker-python","Build Your Own Amazon Price Tracker in Python","Build an Amazon price tracker in Python: daily price, BSR and seller snapshots to SQLite, a Keepa-style chart, alerts, and robot-check detection that works.",{"big":15,"default":15,"slider":15,"mid":15,"small":15,"alt":12},"",{"name":17,"slug":18,"color":19,"url":20},"Use Cases","use-cases","#c51e39","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fuse-cases",{"name":22,"slug":23,"url":24},"SparkProxy","sparkproxy","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fprofile\u002Fsparkproxy","table_of_contents",0,"2026-10-31T19:00:01+05:30",null,"\u003Cp>An \u003Cstrong>Amazon price tracker in Python\u003C\u002Fstrong> is a small program with one awkward job: visit the same list of ASINs every day, write down the Buy Box price, Best Sellers Rank and seller, and never mistake a bot-check page for a product. This guide builds that tracker end to end, from a CSV watchlist to a SQLite history, a step chart that reads like Keepa's, and an alert that fires once when a price crosses your target. The proxy layer uses SparkProxy's rotating US gateway, with the Scraping API as a fallback for the ASINs that refuse to load.\u003C\u002Fp>","\u003Cdiv class=\"faq-accordion\">\n \u003Ch2 class=\"faq-heading\">FAQ\u003C\u002Fh2>\n \u003Cdiv class=\"faq-item open\">\n \u003Cbutton class=\"faq-question\" aria-expanded=\"true\">How often should an Amazon price tracker in Python check each ASIN?\u003Cspan class=\"faq-icon\" aria-hidden=\"true\">\u003C\u002Fspan>\u003C\u002Fbutton>\n \u003Cdiv class=\"faq-answer\">\n \u003Cp>Once a day suits most watchlists: it matches the resolution people actually read a price chart at, and hourly checks cost 24 times the requests. Put the few ASINs that need intraday checks on a separate watchlist with its own schedule instead of speeding up the whole job.\u003C\u002Fp>\n \u003C\u002Fdiv>\n \u003C\u002Fdiv>\n \u003Cdiv class=\"faq-item\">\n \u003Cbutton class=\"faq-question\" aria-expanded=\"false\">Why does my Amazon scraper return HTTP 200 but no price?\u003Cspan class=\"faq-icon\" aria-hidden=\"true\">\u003C\u002Fspan>\u003C\u002Fbutton>\n \u003Cdiv class=\"faq-answer\">\n \u003Cp>Usually because the 200 is an interstitial, not a product page. On 2026-10-30 we received one titled just \"Amazon.com\" with a form posting to \u003Ccode>\u002Ferrors_page\u002FvalidateCaptcha\u003C\u002Fcode>. Check for \u003Ccode>validateCaptcha\u003C\u002Fcode> and require \u003Ccode>id=\"productTitle\"\u003C\u002Fcode> before parsing; if the page did load, the price selector has drifted or there is no Buy Box winner.\u003C\u002Fp>\n \u003C\u002Fdiv>\n \u003C\u002Fdiv>\n \u003Cdiv class=\"faq-item\">\n \u003Cbutton class=\"faq-question\" aria-expanded=\"false\">Do I need residential proxies to track Amazon prices?\u003Cspan class=\"faq-icon\" aria-hidden=\"true\">\u003C\u002Fspan>\u003C\u002Fbutton>\n \u003Cdiv class=\"faq-answer\">\n \u003Cp>Not necessarily for every request. Start with rotating US datacenter proxies and retries, measure your challenge rate in \u003Ccode>fetch_log\u003C\u002Fcode>, and send only the ASINs that keep failing to a residential route such as the Scraping API with \u003Ccode>proxy_type=premium\u003C\u002Fcode>. That keeps metered spend tied to the hard cases.\u003C\u002Fp>\n \u003C\u002Fdiv>\n \u003C\u002Fdiv>\n \u003Cdiv class=\"faq-item\">\n \u003Cbutton class=\"faq-question\" aria-expanded=\"false\">Should I use Random Rotation or Sticky Sessions for Amazon?\u003Cspan class=\"faq-icon\" aria-hidden=\"true\">\u003C\u002Fspan>\u003C\u002Fbutton>\n \u003Cdiv class=\"faq-answer\">\n \u003Cp>Use Random Rotation (port 11000) for product-page checks, since each fetch is one stateless request and a fresh IP per retry helps. Use Sticky Sessions (port 11002) only when a flow spans several requests that must share an IP, such as setting a delivery ZIP before loading the product page.\u003C\u002Fp>\n \u003C\u002Fdiv>\n \u003C\u002Fdiv>\n \u003Cdiv class=\"faq-item\">\n \u003Cbutton class=\"faq-question\" aria-expanded=\"false\">Can I track Amazon Best Sellers Rank history the same way?\u003Cspan class=\"faq-icon\" aria-hidden=\"true\">\u003C\u002Fspan>\u003C\u002Fbutton>\n \u003Cdiv class=\"faq-answer\">\n \u003Cp>Yes. The parser pulls the top-level BSR and its category from the product details block, and the \u003Ccode>bsr\u003C\u002Fcode> column stores it daily next to the price. Plot it on an inverted secondary axis, because rank 1 is the best position.\u003C\u002Fp>\n \u003C\u002Fdiv>\n \u003C\u002Fdiv>\n \u003Cdiv class=\"faq-item\">\n \u003Cbutton class=\"faq-question\" aria-expanded=\"false\">How do I track prices on amazon.co.uk or amazon.de instead?\u003Cspan class=\"faq-icon\" aria-hidden=\"true\">\u003C\u002Fspan>\u003C\u002Fbutton>\n \u003Cdiv class=\"faq-answer\">\n \u003Cp>Change the domain in the fetch URL, exit from the matching country (a Worldwide Rotating plan with a GB or DE country username, or \u003Ccode>country_code=GB\u003C\u002Fcode> or \u003Ccode>DE\u003C\u002Fcode> on the API), and store the currency alongside the price. Keep one database per marketplace so prices in different currencies never share a chart.\u003C\u002Fp>\n \u003C\u002Fdiv>\n \u003C\u002Fdiv>\n\u003C\u002Fdiv>",[32,35,38,41,44,47],{"question":33,"answer":34},"How often should an Amazon price tracker in Python check each ASIN?","Once a day suits most watchlists: it matches the resolution people actually read a price chart at, and hourly checks cost 24 times the requests. Put the few ASINs that need intraday checks on a separate watchlist with its own schedule instead of speeding up the whole job.",{"question":36,"answer":37},"Why does my Amazon scraper return HTTP 200 but no price?","Usually because the 200 is an interstitial, not a product page. On 2026-10-30 we received one titled just \"Amazon.com\" with a form posting to \u002Ferrors_page\u002FvalidateCaptcha. Check for validateCaptcha and require id=\"productTitle\" before parsing; if the page did load, the price selector has drifted or there is no Buy Box winner.",{"question":39,"answer":40},"Do I need residential proxies to track Amazon prices?","Not necessarily for every request. Start with rotating US datacenter proxies and retries, measure your challenge rate in fetch_log, and send only the ASINs that keep failing to a residential route such as the Scraping API with proxy_type=premium. That keeps metered spend tied to the hard cases.",{"question":42,"answer":43},"Should I use Random Rotation or Sticky Sessions for Amazon?","Use Random Rotation (port 11000) for product-page checks, since each fetch is one stateless request and a fresh IP per retry helps. Use Sticky Sessions (port 11002) only when a flow spans several requests that must share an IP, such as setting a delivery ZIP before loading the product page.",{"question":45,"answer":46},"Can I track Amazon Best Sellers Rank history the same way?","Yes. The parser pulls the top-level BSR and its category from the product details block, and the bsr column stores it daily next to the price. Plot it on an inverted secondary axis, because rank 1 is the best position.",{"question":48,"answer":49},"How do I track prices on amazon.co.uk or amazon.de instead?","Change the domain in the fetch URL, exit from the matching country (a Worldwide Rotating plan with a GB or DE country username, or country_code=GB or DE on the API), and store the currency alongside the price. Keep one database per marketplace so prices in different currencies never share a chart.","amazon price tracker python,amazon price history,asin tracker,best sellers rank tracking,sqlite price tracker,amazon robot check,rotating proxies",false,27,[54,57,61,65,69,73,77],{"name":55,"slug":8,"url":56},"amazon price tracker python","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Famazon-price-tracker-python",{"name":58,"slug":59,"url":60},"amazon price history","amazon-price-history","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Famazon-price-history",{"name":62,"slug":63,"url":64},"asin tracker","asin-tracker","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fasin-tracker",{"name":66,"slug":67,"url":68},"best sellers rank tracking","best-sellers-rank-tracking","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fbest-sellers-rank-tracking",{"name":70,"slug":71,"url":72},"sqlite price tracker","sqlite-price-tracker","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fsqlite-price-tracker",{"name":74,"slug":75,"url":76},"amazon robot check","amazon-robot-check","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Famazon-robot-check",{"name":78,"slug":79,"url":80},"rotating proxies","rotating-proxies","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Frotating-proxies",[82,93,103],{"id":83,"slug":84,"url":85,"title":86,"summary":87,"image":88,"category":89,"author":90,"post_type":25,"views":91,"published_at":92,"updated_at":28},616,"foxyproxy-setup-username-password","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ffoxyproxy-setup-username-password","FoxyProxy Setup With Username and Password","FoxyProxy setup for per-site routing: saved username and password, patterns that match, why Chrome rejects SOCKS5 auth, and how to stop the login popup.",{"big":15,"default":15,"slider":15,"mid":15,"small":15,"alt":86},{"name":17,"slug":18,"color":19,"url":20},{"name":22,"slug":23,"url":24},2,"2026-11-07T15:00:01+05:30",{"id":94,"slug":95,"url":96,"title":97,"summary":98,"image":99,"category":100,"author":101,"post_type":25,"views":91,"published_at":102,"updated_at":28},643,"test-gdpr-cookie-banner-from-eu","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftest-gdpr-cookie-banner-from-eu","Test Your GDPR Cookie Banner From the EU","Test cookie consent banner EU behaviour from outside Europe: load your site via EU exits with Playwright and catch cookies and pixels that fire before consent.",{"big":15,"default":15,"slider":15,"mid":15,"small":15,"alt":97},{"name":17,"slug":18,"color":19,"url":20},{"name":22,"slug":23,"url":24},"2026-11-07T11:00:01+05:30",{"id":104,"slug":105,"url":106,"title":107,"summary":108,"image":109,"category":110,"author":111,"post_type":25,"views":112,"published_at":113,"updated_at":28},614,"scrapebox-proxies-setup","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fscrapebox-proxies-setup","ScrapeBox Proxies: Load, Test, and Harvest Google","Set up ScrapeBox proxies that keep harvesting: the IP:PORT:USER:PASS format, loading a rotating gateway, what the Google test checks, and safe connections.",{"big":15,"default":15,"slider":15,"mid":15,"small":15,"alt":107},{"name":17,"slug":18,"color":19,"url":20},{"name":22,"slug":23,"url":24},3,"2026-11-06T19:00:02+05:30",{"kind":25,"sections":115},[116,123,129,135,142,149,156,163,170,177,184,191],{"id":117,"title":118,"anchor":119,"content_html":120,"image":15,"image_alt":15,"item_order":121,"parent_link_num":26,"children":122},11555,"What the Tracker Does, End to End","what-the-tracker-does-end-to-end","\u003Cp>The whole system is one daily batch job. Nothing runs between batches, so there is no server to babysit.\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Stage\u003C\u002Fth>\u003Cth>Input\u003C\u002Fth>\u003Cth>Output\u003C\u002Fth>\u003Cth>File\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Watchlist\u003C\u002Ftd>\u003Ctd>You, editing a CSV\u003C\u002Ftd>\u003Ctd>`asin,label,target_cents` rows\u003C\u002Ftd>\u003Ctd>`watchlist.csv`\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Fetch\u003C\u002Ftd>\u003Ctd>One ASIN\u003C\u002Ftd>\u003Ctd>Raw HTML from `amazon.com\u002Fdp\u002F\u003CASIN>`\u003C\u002Ftd>\u003Ctd>`fetch.py`, `retry.py`\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Classify\u003C\u002Ftd>\u003Ctd>Status code + HTML\u003C\u002Ftd>\u003Ctd>`ok`, `interstitial`, `throttled`, `gone`, `unknown`\u003C\u002Ftd>\u003Ctd>`detect.py`\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Parse\u003C\u002Ftd>\u003Ctd>HTML that passed `ok`\u003C\u002Ftd>\u003Ctd>Price in cents, BSR, seller, Buy Box flag\u003C\u002Ftd>\u003Ctd>`parse.py`\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Store\u003C\u002Ftd>\u003Ctd>Parsed row\u003C\u002Ftd>\u003Ctd>One row per ASIN per UTC day\u003C\u002Ftd>\u003Ctd>`store.py`\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Chart + alert\u003C\u002Ftd>\u003Ctd>SQLite history\u003C\u002Ftd>\u003Ctd>PNG step chart, email on threshold crossing\u003C\u002Ftd>\u003Ctd>`chart.py`, `alerts.py`\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Orchestrate\u003C\u002Ftd>\u003Ctd>All of the above\u003C\u002Ftd>\u003Ctd>One run per day\u003C\u002Ftd>\u003Ctd>`run_daily.py`\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>The watchlist is deliberately boring. Prices are stored in integer cents everywhere, so \u003Ccode>target_cents\u003C\u002Fcode> is too. A blank target means \"track it, never alert\":\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-csv\">asin,label,target_cents\nB0EXAMPLE1,Espresso grinder,18999\nB0EXAMPLE2,Standing desk frame,\nB0EXAMPLE3,Replacement filter 3-pack,2450\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Install three packages into a virtual environment on Python 3.11 or newer. The floor comes from matplotlib: version 3.11.2, released on PyPI on 2026-09-11, declares \u003Ccode>Requires-Python >=3.11\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">python -m venv .venv\n. .venv\u002Fbin\u002Factivate\npip install "requests>=2.34" "selectolax>=1.0" "matplotlib>=3.11"\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>If you sell on Amazon and only care about your own listings, the Selling Partner API is the cleaner source, and \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fbest-proxies-for-amazon-sellers\">our breakdown of which proxies Amazon sellers actually need\u003C\u002Fa> starts from that point. This tracker is for ASINs you don't control: competitors, suppliers, or things you want to buy.\u003C\u002Fp>",1,[],{"id":124,"title":125,"anchor":126,"content_html":127,"image":15,"image_alt":15,"item_order":91,"parent_link_num":26,"children":128},11556,"Proxy Choices for amazon.com","proxy-choices-for-amazoncom","\u003Cp>Three decisions matter for this job, and each one has a right answer for a daily ASIN sweep.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Country.\u003C\u002Fstrong> Amazon decides currency, default delivery region and sometimes the offer itself from where the request comes from. For \u003Ccode>amazon.com\u003C\u002Fcode>, exit in the US. SparkProxy's USA Rotating pool is listed on the homepage (read 2026-10-30) as \"50,000+ premium US datacenter IPs\", so every exit is already American and you don't need a country flag at all. On a Worldwide Rotating plan (1M+ IPs, 80+ countries), the dashboard's credential widget shows a country suffix in the username, displayed on the homepage as \u003Ccode>user-country-us\u003C\u002Fcode>. Copy the exact string your dashboard generates rather than typing it from memory.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Rotation mode.\u003C\u002Fstrong> SparkProxy offers Random Rotation, a new exit IP on every request, on port \u003Ccode>11000\u003C\u002Fcode>, and Sticky Sessions, which hold one IP for about five minutes, on port \u003Ccode>11002\u003C\u002Fcode>. A product-page fetch is a single stateless GET, so Random Rotation is the right fit. It also gives you free retries: when one IP draws a challenge, the next attempt leaves from a different address. Sticky Sessions only earn their place if you set a delivery ZIP through the location picker and need the follow-up page view to come from the same IP. That flow is explained in our guide to \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fwhat-is-a-sticky-session-proxy\">how sticky session proxies hold an IP\u003C\u002Fa>.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Concurrency.\u003C\u002Fstrong> SparkProxy plans are sold by threads, with unlimited bandwidth on every plan. The Starter plan's 100 threads are far more than a tracker needs. A daily job gains nothing from running hot, and a burst of parallel hits is the easiest pattern for any anti-bot system to spot. Six workers is the default below, and you can raise it once your \u003Ccode>fetch_log\u003C\u002Fcode> shows a low challenge rate.\u003C\u002Fp>\n\u003Cp>Be clear about what a proxy can and cannot fix here. This table saves a lot of misdirected debugging:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Symptom in your data\u003C\u002Fth>\u003Cth>Cause\u003C\u002Fth>\u003Cth>Does a proxy fix it?\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Interstitial or bot-check page, often HTTP 200\u003C\u002Ftd>\u003Ctd>IP reputation or request rate\u003C\u002Ftd>\u003Ctd>Yes: rotate and retry, or move to a residential route\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>HTTP 503 after a burst\u003C\u002Ftd>\u003Ctd>Rate limiting on one IP\u003C\u002Ftd>\u003Ctd>Yes, together with lower concurrency\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Price differs from what you see in your browser\u003C\u002Ftd>\u003Ctd>Delivery ZIP or Prime\u002Flogin state\u003C\u002Ftd>\u003Ctd>No. Pin one ZIP, compare logged-out to logged-out\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Empty price on a page that loaded fine\u003C\u002Ftd>\u003Ctd>Selector drift, or no Buy Box winner\u003C\u002Ftd>\u003Ctd>No. Fix the parser or record NULL\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>HTTP 404 \"dogs\" page\u003C\u002Ftd>\u003Ctd>ASIN removed or not sold in this marketplace\u003C\u002Ftd>\u003Ctd>No. Stop requesting it\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Parent ASIN shows a price range\u003C\u002Ftd>\u003Ctd>Variation family, not one product\u003C\u002Ftd>\u003Ctd>No. Track the child ASIN\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>ZIP pinning has its own write-up in \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-scrape-amazon-product-data\">our Amazon product data scraping guide\u003C\u002Fa>, along with legal framing and search-page pagination. Neither is repeated here.\u003C\u002Fp>",[],{"id":130,"title":131,"anchor":132,"content_html":133,"image":15,"image_alt":15,"item_order":112,"parent_link_num":26,"children":134},11557,"Fetching One ASIN Per Request","fetching-one-asin-per-request","\u003Cp>The fetcher does exactly one thing: make one request through the rotating gateway and return the status and body. It opens a fresh \u003Ccode>requests.Session\u003C\u002Fcode> per call so no Amazon cookie from a challenged attempt follows the next one.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\"># fetch.py: one GET per attempt through the rotating US gateway\nimport os\nimport requests\n\nUSER = os.environ["SPARK_USER"] # copy both from the SparkProxy dashboard\nPASSWORD = os.environ["SPARK_PASS"]\nROTATING = f"http:\u002F\u002F{USER}:{PASSWORD}@gateway.sparkproxy.io:11000"\n\nHEADERS = {\n "User-Agent": ("Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 "\n "(KHTML, like Gecko) Chrome\u002F130.0.0.0 Safari\u002F537.36"),\n "Accept": "text\u002Fhtml,application\u002Fxhtml+xml,application\u002Fxml;q=0.9,*\u002F*;q=0.8",\n "Accept-Language": "en-US,en;q=0.9",\n}\n\n\ndef fetch_via_proxy(asin: str) -> tuple[int, str]:\n """Return (http_status, html). A fresh session per call means no cookie\n carry-over, so each attempt looks like a new visitor on a new IP."""\n with requests.Session() as s:\n r = s.get(\n f"https:\u002F\u002Fwww.amazon.com\u002Fdp\u002F{asin}",\n headers=HEADERS,\n proxies={"http": ROTATING, "https": ROTATING},\n timeout=30,\n )\n return r.status_code, r.text\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Request the bare \u003Ccode>\u002Fdp\u002F\u003CASIN>\u003C\u002Fcode> URL. The long, keyword-stuffed URLs you copy from a browser carry \u003Ccode>ref=\u003C\u002Fcode> and tracking parameters that change nothing about the price and make your request log harder to read. If you whitelist your server's IP in the dashboard instead of using user:pass, drop the credentials from \u003Ccode>ROTATING\u003C\u002Fcode> and keep the host and port.\u003C\u002Fp>\n\u003Cp>Before trusting the gateway, confirm rotation from a shell. Two calls should print two different addresses:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">curl -s -x "http:\u002F\u002F$SPARK_USER:$SPARK_PASS@gateway.sparkproxy.io:11000" https:\u002F\u002Fapi.ipify.org; echo\ncurl -s -x "http:\u002F\u002F$SPARK_USER:$SPARK_PASS@gateway.sparkproxy.io:11000" https:\u002F\u002Fapi.ipify.org; echo\n\u003C\u002Fcode>\u003C\u002Fpre>",[],{"id":136,"title":137,"anchor":138,"content_html":139,"image":15,"image_alt":15,"item_order":140,"parent_link_num":26,"children":141},11558,"Catching the Robot Check Before It Poisons Your Data","catching-the-robot-check-before-it-poisons-your-data","\u003Cp>This is the part that quietly ruins most homemade trackers. When Amazon decides a visitor looks automated, it doesn't always send an error. It sends a small page with a 200 status, and a scraper that trusts the status code parses it, finds no price, and writes a NULL or, worse, a zero into your history.\u003C\u002Fp>\n\u003Cp>We captured what that page looks like today. On 2026-10-30 we requested two product pages directly from one of our own servers, with no proxy, a desktop Chrome User-Agent and \u003Ccode>Accept-Language: en-US\u003C\u002Fcode>. Both requests got the same response: HTTP 200, 3,781 bytes, with these features:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>\u003Ctitle>\u003C\u002Fcode> is just \u003Ccode>Amazon.com\u003C\u002Fcode>, not \"Robot Check\".\u003C\u002Fli>\n\u003Cli>The visible heading reads \"Click the button below to continue shopping\".\u003C\u002Fli>\n\u003Cli>The form posts to \u003Ccode>\u002Ferrors_page\u002FvalidateCaptcha\u003C\u002Fcode>, not \u003Ccode>\u002Ferrors\u002FvalidateCaptcha\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>An HTML comment begins \"To discuss automated access to Amazon data please contact api-services-support@amazon.com.\"\u003C\u002Fli>\n\u003Cli>There is no image CAPTCHA and no \"Enter the characters you see below\" text.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>That matters because the marker list most Amazon tutorials ship, including older code on this blog, checks for \"Robot Check\", \"Enter the characters you see below\", \"\u002Ferrors\u002FvalidateCaptcha\" and \"Type the characters you see in this image\". We ran that four-string check against the captured page and it matched none of them. A tracker using it would have stored this interstitial as a real product page. Two requests is a small sample, so treat this as one observed variant, not a census of every page Amazon serves.\u003C\u002Fp>\n\u003Cp>The fix has two halves: match on the substring that survived the change (\u003Ccode>validateCaptcha\u003C\u002Fcode> without the path), and require positive proof that you got a detail page before parsing anything.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\"># detect.py: decide what Amazon actually sent back before anything is parsed\nINTERSTITIAL_MARKERS = (\n "validateCaptcha", # matches \u002Ferrors\u002F and \u002Ferrors_page\u002F forms\n "Click the button below to continue shopping",\n "Enter the characters you see below", # older image-CAPTCHA variant\n "api-services-support@amazon.com", # comment on every bot page we have seen\n)\n\n\ndef classify(status: int, html: str) -> str:\n """Return one of: ok, interstitial, throttled, gone, unknown."""\n if status == 404:\n return "gone" # dead or region-locked ASIN: do not retry\n if status in (429, 503):\n return "throttled"\n if any(m in html for m in INTERSTITIAL_MARKERS):\n return "interstitial" # often arrives with HTTP 200\n if status == 200 and 'id="productTitle"' in html:\n return "ok" # positive proof this is a detail page\n return "unknown" # 200 but not a product page: treat as a miss\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The \u003Ccode>unknown\u003C\u002Fcode> branch is the safety net. When Amazon ships a new interstitial that matches none of the markers, it still lacks \u003Ccode>productTitle\u003C\u002Fcode>, so it lands in \u003Ccode>unknown\u003C\u002Fcode> and gets retried instead of parsed. Blocklists age; a positive check ages much more slowly.\u003C\u002Fp>\n\u003Cp>Retrying is where Random Rotation pays off. Every attempt exits from a new IP, so a challenge on attempt one says little about attempt two:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\"># retry.py: each attempt exits from a different IP, so retrying is cheap\nimport random\nimport time\n\nfrom detect import classify\nfrom fetch import fetch_via_proxy\n\nRETRYABLE = {"interstitial", "throttled", "unknown", "neterror"}\n\n\ndef get_product_page(asin: str, attempts: int = 4) -> tuple[str, str | None, int]:\n """Return (outcome, html_or_None, attempts_used)."""\n outcome = "neterror"\n for n in range(1, attempts + 1):\n try:\n status, html = fetch_via_proxy(asin)\n outcome = classify(status, html)\n except Exception: # timeouts, proxy resets, TLS errors\n outcome, html = "neterror", ""\n if outcome == "ok":\n return outcome, html, n\n if outcome not in RETRYABLE:\n return outcome, None, n # "gone" ends here\n time.sleep(min(30, 2 ** n) + random.uniform(0, 1.5))\n return outcome, None, attempts\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>A 404 ends the loop at once. Retrying a removed ASIN four times a day for months is pure waste, and the \u003Ccode>fetch_log\u003C\u002Fcode> table below lets you find and prune those rows. For a broader catalogue of block signals beyond Amazon, see \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-detect-when-your-scraper-is-blocked\">how to detect when your scraper is blocked\u003C\u002Fa>.\u003C\u002Fp>",4,[],{"id":143,"title":144,"anchor":145,"content_html":146,"image":15,"image_alt":15,"item_order":147,"parent_link_num":26,"children":148},11559,"Parsing Price, BSR and the Buy Box Seller","parsing-price-bsr-and-the-buy-box-seller","\u003Cp>One dependency change will bite anyone copying older Amazon code. selectolax 1.0.0 shipped on PyPI on 2026-10-03, and in 1.0.x the classic import now fails outright. We installed 1.0.2 (released 2026-10-22, the same day as the repository's latest commit) and \u003Ccode>from selectolax.parser import HTMLParser\u003C\u002Fcode> raised:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-text\">ImportError: Modest backend is deprecated since selectolax 1.0. The engine has not been updated since 2021, while lexbor has been available since 2021 and is actively developed. Modest is outdated, not maintained, contains bugs and does not follow modern HTML5 standards. Please use lexbor backend instead: `from selectolax.lexbor import LexborHTMLParser`. The APIs are nearly identical, so in most projects swapping the import is all that's needed.\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The parser below uses the lexbor backend. Two more choices protect the data. Each field tries a list of selectors, because Amazon serves several page templates at once. And \u003Ccode>text(separator=\" \", strip=True)\u003C\u002Fcode> keeps nested spans from gluing together, which matters for the BSR block, where \"#1,234 in Kitchen\" and \"#12 in Electric Kettles\" sit in sibling spans.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\"># parse.py: selectolax 1.x moved to the lexbor backend\nimport re\nfrom decimal import Decimal, InvalidOperation\n\nfrom selectolax.lexbor import LexborHTMLParser\n\nPRICE_SELECTORS = (\n "#corePrice_feature_div .a-offscreen",\n "#corePriceDisplay_desktop_feature_div .a-offscreen",\n "#apex_desktop .a-offscreen",\n ".a-price .a-offscreen",\n)\nSELLER_SELECTORS = (\n "#sellerProfileTriggerId",\n "#merchantInfoFeature_feature_div .offer-display-feature-text-message",\n "#merchant-info a",\n)\nDETAIL_BLOCKS = (\n "#productDetails_detailBullets_sections1",\n "#detailBulletsWrapper_feature_div",\n "#prodDetails",\n)\nBSR_RE = re.compile(r"#([\\d,]+)\\s+in\\s+([^(#]+?)\\s*(?:\\(|#|$)")\n\n\ndef first_text(tree, selectors):\n for sel in selectors:\n node = tree.css_first(sel)\n if node:\n text = node.text(separator=" ", strip=True)\n if text:\n return text\n return None\n\n\ndef to_cents(raw: str | None) -> int | None:\n if not raw:\n return None\n m = re.search(r"\\d[\\d,]*(?:\\.\\d{2})?", raw)\n if not m:\n return None\n try:\n return int(Decimal(m.group(0).replace(",", "")) * 100)\n except InvalidOperation:\n return None\n\n\ndef parse_bsr(tree) -> tuple[int | None, str | None]:\n for sel in DETAIL_BLOCKS:\n node = tree.css_first(sel)\n if not node:\n continue\n text = node.text(separator=" ", strip=True)\n if "Best Sellers Rank" not in text:\n continue\n m = BSR_RE.search(text.split("Best Sellers Rank", 1)[1])\n if m:\n return int(m.group(1).replace(",", "")), m.group(2).strip()\n return None, None\n\n\ndef parse_product(html: str) -> dict:\n tree = LexborHTMLParser(html)\n price_raw = first_text(tree, PRICE_SELECTORS)\n rank, category = parse_bsr(tree)\n avail = first_text(tree, ("#availability",))\n return {\n "title": first_text(tree, ("#productTitle",)),\n "price_cents": to_cents(price_raw),\n "price_raw": price_raw,\n "bsr": rank,\n "bsr_category": category,\n "seller": first_text(tree, SELLER_SELECTORS),\n "availability": avail,\n "buy_box": tree.css_first("#add-to-cart-button") is not None,\n }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>Decimal\u003C\u002Fcode> avoids the float trap where \u003Ccode>19.99 * 100\u003C\u002Fcode> becomes \u003Ccode>1998.9999999999998\u003C\u002Fcode> and truncates to 1998 cents. The BSR regex keeps only the top-level rank, the first \u003Ccode>#N in Category\u003C\u002Fcode> after the \"Best Sellers Rank\" label; extend it if you also want the sub-category ranks.\u003C\u002Fp>\n\u003Cp>A caveat on freshness: on the day we wrote this, our direct requests only ever reached the interstitial, so these selectors were tested against a fixture page built from Amazon's known markup, not against a live product page. They are the commonly used ones, but treat them as candidates. The \u003Ccode>ok\u003C\u002Fcode> gate in \u003Ccode>detect.py\u003C\u002Fcode> and the \u003Ccode>price_raw\u003C\u002Fcode> column mean a broken selector shows up as a run of NULL prices on pages that otherwise loaded, which is easy to spot and never mistaken for a price drop.\u003C\u002Fp>",5,[],{"id":150,"title":151,"anchor":152,"content_html":153,"image":15,"image_alt":15,"item_order":154,"parent_link_num":26,"children":155},11560,"A SQLite Schema for Daily Snapshots","a-sqlite-schema-for-daily-snapshots","\u003Cp>SQLite is the right database for this until you are tracking a very large catalogue: it's a single file, ships with Python, and handles one writer with ease. Two tables keep data and diagnostics apart.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\"># store.py: one row per ASIN per UTC day, plus a log of every attempt\nimport sqlite3\nfrom datetime import datetime, timezone\n\nSCHEMA = """\nCREATE TABLE IF NOT EXISTS snapshot (\n asin TEXT NOT NULL,\n day TEXT NOT NULL, -- YYYY-MM-DD, UTC\n price_cents INTEGER, -- NULL = no Buy Box price, never 0\n bsr INTEGER,\n bsr_category TEXT,\n seller TEXT,\n buy_box INTEGER NOT NULL,\n availability TEXT,\n fetched_at TEXT NOT NULL,\n PRIMARY KEY (asin, day)\n);\nCREATE TABLE IF NOT EXISTS fetch_log (\n asin TEXT NOT NULL,\n at TEXT NOT NULL,\n outcome TEXT NOT NULL, -- ok, interstitial, gone, ...\n attempts INTEGER NOT NULL,\n route TEXT NOT NULL -- proxy or api\n);\n"""\n\n\ndef connect(path: str = "prices.db") -> sqlite3.Connection:\n db = sqlite3.connect(path, timeout=30)\n db.execute("PRAGMA journal_mode=WAL")\n db.executescript(SCHEMA)\n return db\n\n\ndef save_snapshot(db, asin: str, row: dict) -> None:\n now = datetime.now(timezone.utc)\n db.execute(\n """INSERT INTO snapshot VALUES (?,?,?,?,?,?,?,?,?)\n ON CONFLICT(asin, day) DO UPDATE SET\n price_cents=excluded.price_cents, bsr=excluded.bsr,\n bsr_category=excluded.bsr_category, seller=excluded.seller,\n buy_box=excluded.buy_box, availability=excluded.availability,\n fetched_at=excluded.fetched_at""",\n (asin, now.strftime("%Y-%m-%d"), row["price_cents"], row["bsr"],\n row["bsr_category"], row["seller"], int(row["buy_box"]),\n row["availability"], now.isoformat(timespec="seconds")),\n )\n db.commit()\n\n\ndef log_fetch(db, asin: str, outcome: str, attempts: int, route: str) -> None:\n db.execute("INSERT INTO fetch_log VALUES (?,?,?,?,?)",\n (asin, datetime.now(timezone.utc).isoformat(timespec="seconds"),\n outcome, attempts, route))\n db.commit()\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The primary key \u003Ccode>(asin, day)\u003C\u002Fcode> makes a rerun idempotent. If the job crashes halfway and you start it again, the second pass overwrites today's rows instead of doubling them. WAL mode lets you open the database in a notebook while the job is writing.\u003C\u002Fp>\n\u003Cp>Three states need distinct representations, and conflating them is the most common data bug in DIY trackers:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>State\u003C\u002Fth>\u003Cth>How it's stored\u003C\u002Fth>\u003Cth>What the chart does\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Page loaded, Buy Box price found\u003C\u002Ftd>\u003Ctd>Row with `price_cents` set\u003C\u002Ftd>\u003Ctd>Draws the step\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Page loaded, no Buy Box (suppressed or out of stock)\u003C\u002Ftd>\u003Ctd>Row with `price_cents` NULL, `buy_box` 0\u003C\u002Ftd>\u003Ctd>Breaks the line\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Page never loaded (all attempts failed)\u003C\u002Ftd>\u003Ctd>No row; a `fetch_log` entry instead\u003C\u002Ftd>\u003Ctd>Carries the last price forward\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Once a week, this query tells you whether your proxy route is healthy, which ASINs are dead, and whether any are leaning on the paid fallback:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">SELECT asin, outcome, route, COUNT(*) AS n, ROUND(AVG(attempts), 2) AS avg_attempts\nFROM fetch_log\nWHERE at >= date('now', '-7 day')\nGROUP BY asin, outcome, route\nORDER BY n DESC;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>For a multi-retailer system with raw-HTML archiving and Postgres, the \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-build-an-automated-price-monitoring-system-with-proxy-rotation\">automated price monitoring architecture post\u003C\u002Fa> covers that bigger design. A single-marketplace ASIN tracker doesn't need it.\u003C\u002Fp>",6,[],{"id":157,"title":158,"anchor":159,"content_html":160,"image":15,"image_alt":15,"item_order":161,"parent_link_num":26,"children":162},11561,"Charting Price History Like Keepa","charting-price-history-like-keepa","\u003Cp>A Keepa-style history works best as a step chart, and the reason is worth stating: a price is a state that holds until the seller changes it, not a sample that drifts smoothly between readings. Drawing straight lines between daily points invents prices nobody ever charged. Matplotlib's \u003Ccode>drawstyle=\"steps-post\"\u003C\u002Fcode> holds each value flat until the next observation, which is the honest picture.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\"># chart.py: a step chart, because a price is a state that holds until it changes\nimport sqlite3\nfrom datetime import date, timedelta\n\nimport matplotlib\nmatplotlib.use("Agg") # no display needed on a server\nimport matplotlib.pyplot as plt\n\n\ndef plot_history(db: sqlite3.Connection, asin: str, target_cents: int | None,\n out: str | None = None, days: int = 180) -> str:\n since = (date.today() - timedelta(days=days)).isoformat()\n rows = db.execute(\n "SELECT day, price_cents FROM snapshot WHERE asin=? AND day>=? ORDER BY day",\n (asin, since),\n ).fetchall()\n xs = [date.fromisoformat(d) for d, _ in rows]\n ys = [p \u002F 100 if p is not None else float("nan") for _, p in rows]\n\n fig, ax = plt.subplots(figsize=(9, 3.5), layout="constrained")\n ax.plot(xs, ys, drawstyle="steps-post", linewidth=1.8)\n if target_cents:\n ax.axhline(target_cents \u002F 100, linestyle="--", linewidth=1)\n ax.set_title(f"{asin}: Buy Box price, last {days} days")\n ax.set_ylabel("USD")\n ax.grid(alpha=0.3)\n out = out or f"{asin}.png"\n fig.savefig(out, dpi=120)\n plt.close(fig)\n return out\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The NULL-to-\u003Ccode>nan\u003C\u002Fcode> conversion is what produces the gap for no-Buy-Box days, so a suppressed listing is visible at a glance instead of being painted as a continuous price. The \u003Ccode>Agg\u003C\u002Fcode> backend lets the script run under cron on a headless box. To overlay BSR, add \u003Ccode>ax2 = ax.twinx()\u003C\u002Fcode> and plot the \u003Ccode>bsr\u003C\u002Fcode> column on an inverted axis, since a smaller rank is better.\u003C\u002Fp>",7,[],{"id":164,"title":165,"anchor":166,"content_html":167,"image":15,"image_alt":15,"item_order":168,"parent_link_num":26,"children":169},11562,"Threshold Alerts That Fire Once","threshold-alerts-that-fire-once","\u003Cp>A naive alert (\"price is below target\") emails you every single day the deal lasts. What you want is the crossing: today at or below target, the previous known price above it.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\"># alerts.py: fire on the crossing, not on every day the price stays low\nimport smtplib\nfrom email.message import EmailMessage\n\n\ndef crossed_below(db, asin: str, target_cents: int) -> tuple[bool, int | None]:\n rows = db.execute(\n """SELECT price_cents FROM snapshot\n WHERE asin=? AND price_cents IS NOT NULL\n ORDER BY day DESC LIMIT 2""",\n (asin,),\n ).fetchall()\n if not rows:\n return False, None\n today = rows[0][0]\n previous = rows[1][0] if len(rows) > 1 else None\n hit = today <= target_cents and (previous is None or previous > target_cents)\n return hit, today\n\n\ndef send_alert(asin: str, label: str, price_cents: int, target_cents: int,\n chart_path: str) -> None:\n msg = EmailMessage()\n msg["Subject"] = f"{label} hit ${price_cents \u002F 100:.2f} (target ${target_cents \u002F 100:.2f})"\n msg["From"] = "tracker@sparkproxy.io"\n msg["To"] = "you@sparkproxy.io"\n msg.set_content(f"https:\u002F\u002Fwww.amazon.com\u002Fdp\u002F{asin}")\n with open(chart_path, "rb") as f:\n msg.add_attachment(f.read(), maintype="image", subtype="png",\n filename=f"{asin}.png")\n with smtplib.SMTP("smtp.sparkproxy.io", 587) as smtp:\n smtp.starttls()\n smtp.login("tracker@sparkproxy.io", "APP_PASSWORD")\n smtp.send_message(msg)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The query skips NULL prices on purpose. If a product goes out of stock for three days and returns below target, the comparison is against the last real price, so you still get the alert. A first-ever observation already under target also alerts, which is what you want when you add an item mid-sale. Swap the SMTP block for a Slack or Discord webhook if email isn't your channel; the crossing logic stays the same.\u003C\u002Fp>",8,[],{"id":171,"title":172,"anchor":173,"content_html":174,"image":15,"image_alt":15,"item_order":175,"parent_link_num":26,"children":176},11563,"Scheduling the Daily Run","scheduling-the-daily-run","\u003Cp>The orchestrator fetches in parallel, then does every database write on the main thread. sqlite3 connections shouldn't be shared across threads, and keeping writes serial removes that whole class of bug.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\"># run_daily.py: watchlist in, snapshots + charts + alerts out\nimport csv\nimport random\nfrom concurrent.futures import ThreadPoolExecutor\n\nfrom alerts import crossed_below, send_alert\nfrom api_fallback import fetch_via_api\nfrom chart import plot_history\nfrom parse import parse_product\nfrom retry import get_product_page\nfrom store import connect, log_fetch, save_snapshot\n\nWORKERS = 6 # well under the thread count on any SparkProxy plan\nUSE_API_FALLBACK = True\n\n\ndef collect(asin: str) -> tuple[str, str, int, str, dict | None]:\n outcome, html, tries = get_product_page(asin)\n route = "proxy"\n if outcome != "ok" and outcome != "gone" and USE_API_FALLBACK:\n outcome, html = fetch_via_api(asin)\n tries, route = tries + 1, "api"\n return asin, outcome, tries, route, parse_product(html) if html else None\n\n\ndef main() -> None:\n with open("watchlist.csv", newline="") as f:\n items = {r["asin"]: r for r in csv.DictReader(f)}\n order = list(items)\n random.shuffle(order) # no fixed crawl pattern day to day\n\n db = connect()\n with ThreadPoolExecutor(max_workers=WORKERS) as pool:\n results = list(pool.map(collect, order))\n\n for asin, outcome, tries, route, row in results: # all writes on one thread\n log_fetch(db, asin, outcome, tries, route)\n if row is None or row["title"] is None:\n continue\n save_snapshot(db, asin, row)\n target = int(items[asin]["target_cents"] or 0)\n hit, price = crossed_below(db, asin, target) if target else (False, None)\n if hit:\n path = plot_history(db, asin, target)\n send_alert(asin, items[asin]["label"], price, target, path)\n\n\nif __name__ == "__main__":\n main()\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>We ran this orchestrator end to end against stubbed fetches (one ASIN challenged then clean, one challenged on every attempt, one returning 404). The clean ASIN was stored after two attempts and alerted, the stubborn one fell through to the API route and was logged as a miss, and the 404 stopped after a single request.\u003C\u002Fp>\n\u003Cp>On Linux, schedule it with cron at an off-round minute, add a random delay so the start time drifts, and wrap it in \u003Ccode>flock\u003C\u002Fcode> so a slow run never overlaps the next one:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\"># crontab -e (server clock in UTC)\nSHELL=\u002Fbin\u002Fbash\n17 6 * * * sleep $((RANDOM \\% 1800)) && cd \u002Fopt\u002Fasin-tracker && flock -n run.lock .venv\u002Fbin\u002Fpython run_daily.py >> run.log 2>&1\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Two cron traps live in that line. The \u003Ccode>%\u003C\u002Fcode> must be escaped as \u003Ccode>\\%\u003C\u002Fcode>, or cron cuts the command off at that character. And \u003Ccode>$RANDOM\u003C\u002Fcode> is a bash feature: cron's default \u003Ccode>\u002Fbin\u002Fsh\u003C\u002Fcode> is dash on Debian and Ubuntu, where it expands to nothing, hence the \u003Ccode>SHELL=\u002Fbin\u002Fbash\u003C\u002Fcode> line. On Windows, Task Scheduler does the same job:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bat\">schtasks \u002FCreate \u002FTN "ASIN tracker" \u002FSC DAILY \u002FST 06:17 \u002FTR "C:\\asin-tracker\\.venv\\Scripts\\python.exe C:\\asin-tracker\\run_daily.py"\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Daily is the right cadence for most watchlists. Hourly checks multiply requests by 24, and a chart with 24 points a day is rarely what you open when deciding whether to buy or reprice. If a handful of ASINs genuinely need intraday tracking, give them their own watchlist and cron line instead of speeding up the whole job. \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-schedule-and-automate-web-scrapers\">Our guide to scheduling and automating scrapers\u003C\u002Fa> covers systemd timers and queue-based alternatives.\u003C\u002Fp>",9,[],{"id":178,"title":179,"anchor":180,"content_html":181,"image":15,"image_alt":15,"item_order":182,"parent_link_num":26,"children":183},11564,"Sending Stubborn ASINs to the Scraping API","sending-stubborn-asins-to-the-scraping-api","\u003Cp>SparkProxy's rotating pools are datacenter IPs. The interstitial we captured came from a datacenter address with no proxy at all, on the very first request, so it says nothing about SparkProxy's pool specifically, but it is a fair warning about how Amazon can treat the category. The fallback route gives the ASINs that exhaust their four proxy attempts one more try through the Scraping API's residential pool.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\"># api_fallback.py: residential retry through the SparkProxy Scraping API\nimport base64\nimport os\n\nimport requests\n\nfrom detect import classify\n\nAPI = "https:\u002F\u002Fscrape.sparkproxy.io\u002Fapi\u002Fv1"\nAPI_KEY = os.environ["SPARK_API_KEY"]\n\n\ndef fetch_via_api(asin: str, render: bool = False) -> tuple[str, str | None]:\n params = {\n "url": f"https:\u002F\u002Fwww.amazon.com\u002Fdp\u002F{asin}",\n "render_js": "true" if render else "false",\n "proxy_type": "premium", # residential pool\n "country_code": "US", # match the marketplace\n "json_response": "true", # envelope with status_code + base64 body\n }\n r = requests.get(API, headers={"X-API-Key": API_KEY}, params=params, timeout=200)\n if r.status_code != 200: # 429, 530 etc. come from the API itself\n return f"api_{r.status_code}", None\n env = r.json()\n html = base64.b64decode(env.get("body") or "").decode("utf-8", "replace")\n outcome = classify(env.get("status_code") or 0, html)\n return outcome, html if outcome == "ok" else None\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Four details here are easy to get wrong:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>render_js\u003C\u002Fcode> defaults to \u003Ccode>true\u003C\u002Fcode> in the API. A tracker that forgets to set it pays for a headless browser on every call. Start with \u003Ccode>false\u003C\u002Fcode> and only pass \u003Ccode>render=True\u003C\u002Fcode> for ASINs whose price block is missing from the plain fetch.\u003C\u002Fli>\n\u003Cli>\u003Ccode>json_response=true\u003C\u002Fcode> is what returns the envelope with \u003Ccode>job_id\u003C\u002Fcode>, \u003Ccode>status_code\u003C\u002Fcode>, \u003Ccode>duration_ms\u003C\u002Fcode>, \u003Ccode>credits_used\u003C\u002Fcode> and \u003Ccode>body\u003C\u002Fcode>. The \u003Ccode>body\u003C\u002Fcode> is base64. Decode it before running \u003Ccode>classify\u003C\u002Fcode>, or the marker search runs against base64 text, never matches, and a challenge page gets through as \u003Ccode>unknown\u003C\u002Fcode> at best.\u003C\u002Fli>\n\u003Cli>\u003Ccode>format=json\u003C\u002Fcode> is not the envelope. \u003Ccode>format\u003C\u002Fcode> only converts page content (html, md, mdx, json, screenshot, pdf), so it's the wrong parameter here.\u003C\u002Fli>\n\u003Cli>The same \u003Ccode>classify\u003C\u002Fcode> function runs on the decoded body. A residential IP lowers the odds of an interstitial; it doesn't make detection optional.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The docs also list a \u003Ccode>530\u003C\u002Fcode> status for a failed scrape (with credits refunded) and \u003Ccode>429\u003C\u002Fcode> for rate or concurrency limits, which is why any non-200 from the API is logged as its own outcome. The full parameter list is in the \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002Fdocs\u002Fscraping-api\u002F\">Scraping API docs\u003C\u002Fa>. For the wider decision between running proxies yourself and handing the fetch to an API, see \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fweb-scraping-api-vs-self-managed-proxies\">web scraping API vs self-managed proxies\u003C\u002Fa>.\u003C\u002Fp>",10,[],{"id":185,"title":186,"anchor":187,"content_html":188,"image":15,"image_alt":15,"item_order":189,"parent_link_num":26,"children":190},11565,"What a Daily Run Costs","what-a-daily-run-costs","\u003Cp>The proxy side is a flat fee, because SparkProxy bills by plan and thread count with unlimited bandwidth. Prices below were read from sparkproxy.io on 2026-10-30, in US dollars, excluding VAT\u002FGST:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Plan\u003C\u002Fth>\u003Cth>Threads\u003C\u002Fth>\u003Cth>Monthly price (USD)\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>Starter\u003C\u002Ftd>\u003Ctd>100\u003C\u002Ftd>\u003Ctd>$75\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Core\u003C\u002Ftd>\u003Ctd>250\u003C\u002Ftd>\u003Ctd>$140\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Boost\u003C\u002Ftd>\u003Ctd>500\u003C\u002Ftd>\u003Ctd>$240\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>Plus\u003C\u002Ftd>\u003Ctd>1,000\u003C\u002Ftd>\u003Ctd>$440\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>Term discounts apply on top: 5% for 3 months, 10% for 6 months, 15% for 12 months. A tracker running six workers never comes near the Starter plan's 100 threads, so Starter covers it whether you watch 50 ASINs or several thousand. There's also a 24-hour free trial with no credit card, enough to run the job a couple of times and read your own \u003Ccode>fetch_log\u003C\u002Fcode> before paying.\u003C\u002Fp>\n\u003Cp>The API fallback is metered in credits. From the credit table in the docs, read 2026-10-30:\u003C\u002Fp>\n\u003Cdiv class=\"table-wrap\">\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Fallback request\u003C\u002Fth>\u003Cth>Credits\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\u003Ctr>\u003Ctd>`proxy_type=premium`, `render_js=false`\u003C\u002Ftd>\u003Ctd>10\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>...plus `country_code=US`\u003C\u002Ftd>\u003Ctd>15 total\u003C\u002Ftd>\u003C\u002Ftr>\u003Ctr>\u003Ctd>`proxy_type=premium`, `render_js=true`, `country_code=US`\u003C\u002Ftd>\u003Ctd>30 total\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>New accounts get 1,000 free credits, which buys about 66 plain premium US fallbacks. The cost depends entirely on how many ASINs fail the proxy route, so measure that before budgeting. If 5% of a 1,000-ASIN list falls through each day, that's 50 × 15 = 750 credits a day; at 20% it's 3,000. Your weekly \u003Ccode>fetch_log\u003C\u002Fcode> query gives you the real rate.\u003C\u002Fp>\n\u003Cp>Run time is the other budget. As a planning assumption, not a measurement: if an attempt averages 5 seconds and the average ASIN needs 1.5 attempts, 1,000 ASINs across 6 workers take about 1,000 × 1.5 × 5 \u002F 6 ≈ 1,250 seconds, roughly 21 minutes. Replace the 5 and the 1.5 with numbers from your own logs after the first week.\u003C\u002Fp>",11,[],{"id":192,"title":193,"anchor":194,"content_html":195,"image":15,"image_alt":15,"item_order":196,"parent_link_num":26,"children":197},11566,"About the Author","about-the-author","\u003Cp>\u003Cstrong>SparkProxy Technical Team.\u003C\u002Fstrong> SparkProxy builds and runs rotating datacenter proxy pools in the US and 80+ countries, plus a managed Scraping API with residential routing. Every code block in this guide compiles under Python 3.14, and the orchestrator was run end to end against stubbed fetches using requests 2.34.2, selectolax 1.0.2 and matplotlib 3.11.2. The Amazon interstitial described above was captured on 2026-10-30.\u003C\u002Fp>\n\u003Cp>\u003Cem>Sources: \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002Fdocs\u002Fscraping-api\u002F\">SparkProxy Scraping API docs\u003C\u002Fa> · \u003Ca href=\"https:\u002F\u002Fwww.sparkproxy.io\u002F\">SparkProxy plans and pools\u003C\u002Fa> · \u003Ca rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\u002F\u002Fpypi.org\u002Fproject\u002Fselectolax\u002F\">selectolax on PyPI\u003C\u002Fa> · \u003Ca rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\u002F\u002Fpypi.org\u002Fproject\u002Fmatplotlib\u002F\">matplotlib on PyPI\u003C\u002Fa> · \u003Ca rel=\"noopener noreferrer\" target=\"_blank\" href=\"https:\u002F\u002Fpypi.org\u002Fproject\u002Frequests\u002F\">requests on PyPI\u003C\u002Fa>\u003C\u002Fem>\u003C\u002Fp>",12,[],{"title":12,"description":13,"keywords":50,"canonical":11,"og_image":15,"article_published":27,"article_modified":28},{"ok":5,"data":200},{"featured":201,"recommended":258,"popular":328,"trending":388,"tags":491},[202,218,238],{"id":203,"slug":204,"url":205,"title":206,"summary":207,"image":208,"category":214,"author":215,"post_type":25,"views":182,"published_at":216,"updated_at":217},51,"datacenter-proxies-for-cybersecurity-threat-intelligence","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fdatacenter-proxies-for-cybersecurity-threat-intelligence","Threat Intelligence Proxies: OSINT & Dark Web Recon","Learn how datacenter proxies power threat intelligence: OSINT collection, dark web monitoring, and malware analysis without exposing your infrastructure.",{"big":209,"default":210,"slider":211,"mid":212,"small":213,"alt":206},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_870x580_6a33955e731a7.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_870x_6a33955f61a5f.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_694x532_6a33956023aaa.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_430x256_6a339560aea05.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_140x98_6a3395611f6e8.webp",{"name":17,"slug":18,"color":19,"url":20},{"name":22,"slug":23,"url":24},"2026-06-18T12:19:05+05:30","2026-06-18T12:22:22+05:30",{"id":219,"slug":220,"url":221,"title":222,"summary":223,"image":224,"category":230,"author":234,"post_type":25,"views":235,"published_at":236,"updated_at":237},19,"what-is-proxy-server","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fwhat-is-proxy-server","What Is a Proxy Server? Meaning & How It Works (2026)","A proxy server is an intermediary between your device and the internet, it hides your IP, filters traffic, and boosts security for 65%+ of enterprises.",{"big":225,"default":226,"slider":227,"mid":228,"small":229,"alt":222},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x580_6a06ac27a665d.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x_6a06ac28c2130.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_694x532_6a06ac29b3fcc.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_430x256_6a06ac2a8a74f.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_140x98_6a06ac2b0ceb5.webp",{"name":231,"slug":232,"color":19,"url":233},"Proxy Basic","proxy-basic","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fproxy-basic",{"name":22,"slug":23,"url":24},309,"2026-05-15T10:46:54+05:30","2026-09-17T19:22:39+05:30",{"id":239,"slug":240,"url":241,"title":242,"summary":243,"image":244,"category":250,"author":254,"post_type":25,"views":255,"published_at":256,"updated_at":257},14,"how-to-build-a-captcha-solver-with-machine-learning","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-build-a-captcha-solver-with-machine-learning","CAPTCHA Solver with Machine Learning: Build & Troubleshoot (2026)","Build a CAPTCHA solver with machine learning in Python. CNN models hit 95%+ accuracy on text CAPTCHAs. Step-by-step code, model comparison & troubleshooting.",{"big":245,"default":246,"slider":247,"mid":248,"small":249,"alt":242},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x580_69ff7aa947352.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x_69ff7aaa67dc6.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_694x532_69ff7aac5ab3b.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_430x256_69ff7aad65722.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_140x98_69ff7aae08194.webp",{"name":251,"slug":252,"color":19,"url":253},"Guides","guides","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fguides",{"name":22,"slug":23,"url":24},295,"2026-05-09T23:53:54+05:30","2026-05-09T23:52:46+05:30",[259,276,293,309],{"id":260,"slug":261,"url":262,"title":263,"summary":264,"image":265,"category":271,"author":272,"post_type":25,"views":273,"published_at":274,"updated_at":275},140,"how-to-bypass-kasada","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-bypass-kasada","How to Bypass Kasada When Web Scraping","Bypass Kasada when scraping public data: how KPSDK proof-of-work, the x-kpsdk-ct and x-kpsdk-cd headers, and the 429 challenge work, plus real API code.",{"big":266,"default":267,"slider":268,"mid":269,"small":270,"alt":263},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_870x580_6a925ea519b4f.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_870x_6a925ea5b95b9.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_694x532_6a925ea63450a.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_430x256_6a925ea6b6494.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_140x98_6a925ea719974.webp",{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},94,"2026-07-30T14:00:01+05:30","2026-10-09T10:36:17+05:30",{"id":277,"slug":278,"url":279,"title":280,"summary":281,"image":282,"category":288,"author":289,"post_type":25,"views":290,"published_at":291,"updated_at":292},126,"how-to-bypass-perimeterx","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-bypass-perimeterx","How to Bypass PerimeterX (HUMAN Security) When Scraping","Bypass PerimeterX (HUMAN) when scraping public data: how _px cookies, the sensor JS, and Press and Hold work, plus real browsers, residential IPs, and API code.",{"big":283,"default":284,"slider":285,"mid":286,"small":287,"alt":280},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_870x580_6a925eaa150dc.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_870x_6a925eaa9e6c8.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_694x532_6a925eab087e7.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_430x256_6a925eab6b6f6.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_140x98_6a925eabbad1b.webp",{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},91,"2026-07-28T14:00:02+05:30","2026-09-18T00:56:56+05:30",{"id":294,"slug":295,"url":296,"title":297,"summary":298,"image":299,"category":305,"author":306,"post_type":25,"views":203,"published_at":307,"updated_at":308},31,"guide-to-proxy-dns-leak-testing-and-mitigation","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fguide-to-proxy-dns-leak-testing-and-mitigation","Guide to Proxy DNS Leak Testing and Mitigation","DNS leaks expose your queries to ISPs even when using a proxy. Learn how to test for proxy DNS leaks and apply proven mitigation steps to protect your privacy.",{"big":300,"default":301,"slider":302,"mid":303,"small":304,"alt":297},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x580_6a157b46bb504.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x_6a157b47ef1bf.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_694x532_6a157b4903bfb.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_430x256_6a157b49d35f6.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_140x98_6a157b4a65ae7.webp",{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},"2026-05-26T16:22:24+05:30","2026-09-17T19:22:34+05:30",{"id":196,"slug":310,"url":311,"title":312,"summary":313,"image":314,"category":320,"author":324,"post_type":25,"views":325,"published_at":326,"updated_at":327},"comparison-of-residential-vs-datacenter-vs-mobile-proxy-types","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fcomparison-of-residential-vs-datacenter-vs-mobile-proxy-types","Residential vs Datacenter vs Mobile Proxy Comparison (2026)","Datacenter proxies block at 30-60% on protected sites; residential hits 85-99%, mobile 97%. Compare all 3 proxy types with 2026 cost and use case guides.",{"big":315,"default":316,"slider":317,"mid":318,"small":319,"alt":312},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x580_69fc216857aed.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x_69fc21698dbd9.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_694x532_69fc216a86ea4.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_430x256_69fc216b50b58.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_140x98_69fc216bebd4e.webp",{"name":321,"slug":322,"color":19,"url":323},"Comparisons","comparisons","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fcomparisons",{"name":22,"slug":23,"url":24},159,"2026-05-07T10:57:00+05:30","2026-05-13T13:28:14+05:30",[329,346,350,354,371],{"id":330,"slug":331,"url":332,"title":333,"summary":334,"image":335,"category":341,"author":342,"post_type":25,"views":343,"published_at":344,"updated_at":345},22,"top-12-antidetect-browsers-best-tools-for-privacy-multi-account-management","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftop-12-antidetect-browsers-best-tools-for-privacy-multi-account-management","Best Antidetect Browsers: Top 12 Compared for 2026","Compare the 12 best antidetect browsers for 2026, from Multilogin and GoLogin to AdsPower and Octo, with fingerprint tech, free tiers, and pricing at a glance.",{"big":336,"default":337,"slider":338,"mid":339,"small":340,"alt":333},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202609\u002Fimage_870x580_6a966a1823bfc.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202609\u002Fimage_870x_6a966a18ad167.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202609\u002Fimage_694x532_6a966a1917f18.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202609\u002Fimage_430x256_6a966a19774ed.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202609\u002Fimage_140x98_6a966a19c6b8c.webp",{"name":321,"slug":322,"color":19,"url":323},{"name":22,"slug":23,"url":24},526,"2026-05-17T22:53:20+05:30","2026-09-18T00:57:41+05:30",{"id":219,"slug":220,"url":221,"title":222,"summary":223,"image":347,"category":348,"author":349,"post_type":25,"views":235,"published_at":236,"updated_at":237},{"big":225,"default":226,"slider":227,"mid":228,"small":229,"alt":222},{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},{"id":239,"slug":240,"url":241,"title":242,"summary":243,"image":351,"category":352,"author":353,"post_type":25,"views":255,"published_at":256,"updated_at":257},{"big":245,"default":246,"slider":247,"mid":248,"small":249,"alt":242},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":355,"slug":356,"url":357,"title":358,"summary":359,"image":360,"category":366,"author":367,"post_type":25,"views":368,"published_at":369,"updated_at":370},55,"proxy-ports-explained-80-443-8080-and-more","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fproxy-ports-explained-80-443-8080-and-more","Proxy Ports Explained: 80, 443, 8080 & Which to Use","Proxy ports 80, 443, 8080, 3128, and 1080 explained: which port to use for HTTP vs SOCKS5, why 8080 is the default, and how to fix blocked-port connection errors.",{"big":361,"default":362,"slider":363,"mid":364,"small":365,"alt":358},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_870x580_6a37d92d017d9.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_870x_6a37d92de61a6.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_694x532_6a37d92e9eaa1.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_430x256_6a37d92f4a6b8.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_140x98_6a37d92fd9edc.webp",{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},294,"2026-06-21T17:56:45+05:30","2026-09-18T00:57:30+05:30",{"id":372,"slug":373,"url":374,"title":375,"summary":376,"image":377,"category":383,"author":384,"post_type":25,"views":385,"published_at":386,"updated_at":387},150,"how-to-bypass-imperva-incapsula","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-bypass-imperva-incapsula","How to Bypass Imperva (Incapsula) When Web Scraping","Bypass Imperva when web scraping public data: how Incapsula fingerprints via visid_incap, reese84, and ___utmvc, plus real browsers, clean IPs, and API code.",{"big":378,"default":379,"slider":380,"mid":381,"small":382,"alt":375},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_870x580_6a925ea05c8fb.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_870x_6a925ea0d96b7.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_694x532_6a925ea154378.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_430x256_6a925ea1b9967.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_140x98_6a925ea20ee9a.webp",{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},229,"2026-08-01T14:00:02+05:30","2026-08-29T09:52:56+05:30",[389,405,409,426,430,434,438,454,458,474],{"id":182,"slug":390,"url":391,"title":392,"summary":393,"image":394,"category":400,"author":401,"post_type":25,"views":402,"published_at":403,"updated_at":404},"how-to-test-proxies-complete-guide-to-proxy-testing-in-2026","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-test-proxies-complete-guide-to-proxy-testing-in-2026","How to Test Proxies: Complete Guide to Proxy Testing in 2026","How to test proxies in 2026: verify connectivity, latency, anonymity level, and geo accuracy. Includes a Python bulk tester. 43% of free proxy IPs are dead.",{"big":395,"default":396,"slider":397,"mid":398,"small":399,"alt":392},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x580_69f98431376f8.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x_69f9843245040.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_694x532_69f984336eb4a.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_430x256_69f984343ccdc.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_140x98_69f98434bbee5.webp",{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},782,"2026-05-05T11:21:24+05:30","2026-05-05T11:16:36+05:30",{"id":330,"slug":331,"url":332,"title":333,"summary":334,"image":406,"category":407,"author":408,"post_type":25,"views":343,"published_at":344,"updated_at":345},{"big":336,"default":337,"slider":338,"mid":339,"small":340,"alt":333},{"name":321,"slug":322,"color":19,"url":323},{"name":22,"slug":23,"url":24},{"id":410,"slug":411,"url":412,"title":413,"summary":414,"image":415,"category":421,"author":422,"post_type":25,"views":423,"published_at":424,"updated_at":425},47,"how-to-integrate-proxies-with-selenium","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-integrate-proxies-with-selenium","Selenium Proxy: Setup, Auth & Rotation (2026 Guide)","Set up a Selenium proxy in Python for Chrome and Firefox. Covers Selenium 4 Options, Selenium Wire auth, proxy rotation, and headless fixes with working code.",{"big":416,"default":417,"slider":418,"mid":419,"small":420,"alt":413},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_870x580_6a1e7df182c49.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_870x_6a1e7df24ecb0.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_694x532_6a1e7df2ed069.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_430x256_6a1e7df37fd0a.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_140x98_6a1e7df3e50f4.webp",{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},467,"2026-06-16T12:16:51+05:30","2026-09-18T00:56:59+05:30",{"id":219,"slug":220,"url":221,"title":222,"summary":223,"image":427,"category":428,"author":429,"post_type":25,"views":235,"published_at":236,"updated_at":237},{"big":225,"default":226,"slider":227,"mid":228,"small":229,"alt":222},{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},{"id":239,"slug":240,"url":241,"title":242,"summary":243,"image":431,"category":432,"author":433,"post_type":25,"views":255,"published_at":256,"updated_at":257},{"big":245,"default":246,"slider":247,"mid":248,"small":249,"alt":242},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":355,"slug":356,"url":357,"title":358,"summary":359,"image":435,"category":436,"author":437,"post_type":25,"views":368,"published_at":369,"updated_at":370},{"big":361,"default":362,"slider":363,"mid":364,"small":365,"alt":358},{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},{"id":189,"slug":439,"url":440,"title":441,"summary":442,"image":443,"category":449,"author":450,"post_type":25,"views":451,"published_at":452,"updated_at":453},"guide-on-ethical-scraping-and-rate-limiting","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fguide-on-ethical-scraping-and-rate-limiting","Ethical Web Scraping: Rate Limiting & robots.txt Guide (2026)","50% of web traffic is non-human (Imperva, 2025). Learn to build ethical scrapers with proper rate limiting, robots.txt compliance, and 429 backoff handling.",{"big":444,"default":445,"slider":446,"mid":447,"small":448,"alt":441},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x580_69fae5fe274fb.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_870x_69fae5ff314b8.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_694x532_69fae6002c40f.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_430x256_69fae600f3ebe.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202605\u002Fimage_140x98_69fae601a240a.webp",{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},232,"2026-05-06T12:18:22+05:30","2026-05-08T13:55:54+05:30",{"id":372,"slug":373,"url":374,"title":375,"summary":376,"image":455,"category":456,"author":457,"post_type":25,"views":385,"published_at":386,"updated_at":387},{"big":378,"default":379,"slider":380,"mid":381,"small":382,"alt":375},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":459,"slug":460,"url":461,"title":462,"summary":463,"image":464,"category":470,"author":471,"post_type":25,"views":472,"published_at":473,"updated_at":28},203,"how-to-scrape-yahoo-finance","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fhow-to-scrape-yahoo-finance","How to Scrape Yahoo Finance Data (2026 Guide)","Scrape Yahoo Finance quotes, historical prices, and fundamentals from its hidden JSON API. Crumb and cookie setup, 429 fixes, Python code, and the legal rules.",{"big":465,"default":466,"slider":467,"mid":468,"small":469,"alt":462},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_870x580_6a796b2e7d133.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_870x_6a796b2f09929.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_694x532_6a796b2f6565f.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_430x256_6a796b2fbe29e.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202608\u002Fimage_140x98_6a796b3012561.webp",{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},218,"2026-08-11T11:00:02+05:30",{"id":475,"slug":476,"url":477,"title":478,"summary":479,"image":480,"category":486,"author":487,"post_type":25,"views":488,"published_at":489,"updated_at":490},40,"what-is-an-anonymous-proxy","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Fwhat-is-an-anonymous-proxy","Anonymous Proxy: What It Is, How It Works, and Its Limits","An anonymous proxy hides your IP address from target servers but does not make you invisible. Learn the three proxy anonymity levels, what anonymous proxies can and cannot conceal, and which tier your use case actually requires.",{"big":481,"default":482,"slider":483,"mid":484,"small":485,"alt":478},"https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_870x580_6a1e7730d39bf.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_870x_6a1e77319129b.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_694x532_6a1e773233ca8.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_430x256_6a1e7732b6269.webp","https:\u002F\u002Fblog.cms.sparkproxy.io\u002Fuploads\u002Fimages\u002F202606\u002Fimage_140x98_6a1e7733210b5.webp",{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},212,"2026-06-08T13:28:07+05:30","2026-06-02T11:54:57+05:30",[492,496,500,504,508,512,516,520,524,528,532,536,540,543,546,550,551,555,559,563],{"name":493,"slug":494,"url":495},"web scraping","web-scraping","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fweb-scraping",{"name":497,"slug":498,"url":499},"datacenter proxies","datacenter-proxies","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fdatacenter-proxies",{"name":501,"slug":502,"url":503},"residential proxies","residential-proxies","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fresidential-proxies",{"name":505,"slug":506,"url":507},"antidetect browser","antidetect-browser","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fantidetect-browser",{"name":509,"slug":510,"url":511},"scraping api","scraping-api","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fscraping-api",{"name":513,"slug":514,"url":515},"multi-account management","multi-account-management","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fmulti-account-management",{"name":517,"slug":518,"url":519},"web scraping api","web-scraping-api","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fweb-scraping-api",{"name":521,"slug":522,"url":523},"proxy comparison","proxy-comparison","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fproxy-comparison",{"name":525,"slug":526,"url":527},"proxy buying guide","proxy-buying-guide","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fproxy-buying-guide",{"name":529,"slug":530,"url":531},"proxy pricing","proxy-pricing","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fproxy-pricing",{"name":533,"slug":534,"url":535},"browser fingerprinting","browser-fingerprinting","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fbrowser-fingerprinting",{"name":537,"slug":538,"url":539},"sticky sessions","sticky-sessions","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fsticky-sessions",{"name":541,"slug":541,"url":542},"kameleo","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fkameleo",{"name":544,"slug":544,"url":545},"multilogin","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fmultilogin",{"name":547,"slug":548,"url":549},"dolphin anty","dolphin-anty","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fdolphin-anty",{"name":78,"slug":79,"url":80},{"name":552,"slug":553,"url":554},"proxy types","proxy-types","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fproxy-types",{"name":556,"slug":557,"url":558},"ISP proxies","isp-proxies","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fisp-proxies",{"name":560,"slug":561,"url":562},"IP reputation","ip-reputation","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fip-reputation",{"name":564,"slug":565,"url":566},"mobile proxies","mobile-proxies","https:\u002F\u002Fwww.sparkproxy.io\u002Fblog\u002Ftag\u002Fmobile-proxies",{"ok":5,"data":568},{"featured":569,"recommended":582,"popular":599,"trending":620,"tags":661},[570,574,578],{"id":203,"slug":204,"url":205,"title":206,"summary":207,"image":571,"category":572,"author":573,"post_type":25,"views":182,"published_at":216,"updated_at":217},{"big":209,"default":210,"slider":211,"mid":212,"small":213,"alt":206},{"name":17,"slug":18,"color":19,"url":20},{"name":22,"slug":23,"url":24},{"id":219,"slug":220,"url":221,"title":222,"summary":223,"image":575,"category":576,"author":577,"post_type":25,"views":235,"published_at":236,"updated_at":237},{"big":225,"default":226,"slider":227,"mid":228,"small":229,"alt":222},{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},{"id":239,"slug":240,"url":241,"title":242,"summary":243,"image":579,"category":580,"author":581,"post_type":25,"views":255,"published_at":256,"updated_at":257},{"big":245,"default":246,"slider":247,"mid":248,"small":249,"alt":242},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},[583,587,591,595],{"id":260,"slug":261,"url":262,"title":263,"summary":264,"image":584,"category":585,"author":586,"post_type":25,"views":273,"published_at":274,"updated_at":275},{"big":266,"default":267,"slider":268,"mid":269,"small":270,"alt":263},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":277,"slug":278,"url":279,"title":280,"summary":281,"image":588,"category":589,"author":590,"post_type":25,"views":290,"published_at":291,"updated_at":292},{"big":283,"default":284,"slider":285,"mid":286,"small":287,"alt":280},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":294,"slug":295,"url":296,"title":297,"summary":298,"image":592,"category":593,"author":594,"post_type":25,"views":203,"published_at":307,"updated_at":308},{"big":300,"default":301,"slider":302,"mid":303,"small":304,"alt":297},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":196,"slug":310,"url":311,"title":312,"summary":313,"image":596,"category":597,"author":598,"post_type":25,"views":325,"published_at":326,"updated_at":327},{"big":315,"default":316,"slider":317,"mid":318,"small":319,"alt":312},{"name":321,"slug":322,"color":19,"url":323},{"name":22,"slug":23,"url":24},[600,604,608,612,616],{"id":330,"slug":331,"url":332,"title":333,"summary":334,"image":601,"category":602,"author":603,"post_type":25,"views":343,"published_at":344,"updated_at":345},{"big":336,"default":337,"slider":338,"mid":339,"small":340,"alt":333},{"name":321,"slug":322,"color":19,"url":323},{"name":22,"slug":23,"url":24},{"id":219,"slug":220,"url":221,"title":222,"summary":223,"image":605,"category":606,"author":607,"post_type":25,"views":235,"published_at":236,"updated_at":237},{"big":225,"default":226,"slider":227,"mid":228,"small":229,"alt":222},{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},{"id":239,"slug":240,"url":241,"title":242,"summary":243,"image":609,"category":610,"author":611,"post_type":25,"views":255,"published_at":256,"updated_at":257},{"big":245,"default":246,"slider":247,"mid":248,"small":249,"alt":242},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":355,"slug":356,"url":357,"title":358,"summary":359,"image":613,"category":614,"author":615,"post_type":25,"views":368,"published_at":369,"updated_at":370},{"big":361,"default":362,"slider":363,"mid":364,"small":365,"alt":358},{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},{"id":372,"slug":373,"url":374,"title":375,"summary":376,"image":617,"category":618,"author":619,"post_type":25,"views":385,"published_at":386,"updated_at":387},{"big":378,"default":379,"slider":380,"mid":381,"small":382,"alt":375},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},[621,625,629,633,637,641,645,649,653,657],{"id":182,"slug":390,"url":391,"title":392,"summary":393,"image":622,"category":623,"author":624,"post_type":25,"views":402,"published_at":403,"updated_at":404},{"big":395,"default":396,"slider":397,"mid":398,"small":399,"alt":392},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":330,"slug":331,"url":332,"title":333,"summary":334,"image":626,"category":627,"author":628,"post_type":25,"views":343,"published_at":344,"updated_at":345},{"big":336,"default":337,"slider":338,"mid":339,"small":340,"alt":333},{"name":321,"slug":322,"color":19,"url":323},{"name":22,"slug":23,"url":24},{"id":410,"slug":411,"url":412,"title":413,"summary":414,"image":630,"category":631,"author":632,"post_type":25,"views":423,"published_at":424,"updated_at":425},{"big":416,"default":417,"slider":418,"mid":419,"small":420,"alt":413},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":219,"slug":220,"url":221,"title":222,"summary":223,"image":634,"category":635,"author":636,"post_type":25,"views":235,"published_at":236,"updated_at":237},{"big":225,"default":226,"slider":227,"mid":228,"small":229,"alt":222},{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},{"id":239,"slug":240,"url":241,"title":242,"summary":243,"image":638,"category":639,"author":640,"post_type":25,"views":255,"published_at":256,"updated_at":257},{"big":245,"default":246,"slider":247,"mid":248,"small":249,"alt":242},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":355,"slug":356,"url":357,"title":358,"summary":359,"image":642,"category":643,"author":644,"post_type":25,"views":368,"published_at":369,"updated_at":370},{"big":361,"default":362,"slider":363,"mid":364,"small":365,"alt":358},{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},{"id":189,"slug":439,"url":440,"title":441,"summary":442,"image":646,"category":647,"author":648,"post_type":25,"views":451,"published_at":452,"updated_at":453},{"big":444,"default":445,"slider":446,"mid":447,"small":448,"alt":441},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":372,"slug":373,"url":374,"title":375,"summary":376,"image":650,"category":651,"author":652,"post_type":25,"views":385,"published_at":386,"updated_at":387},{"big":378,"default":379,"slider":380,"mid":381,"small":382,"alt":375},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":459,"slug":460,"url":461,"title":462,"summary":463,"image":654,"category":655,"author":656,"post_type":25,"views":472,"published_at":473,"updated_at":28},{"big":465,"default":466,"slider":467,"mid":468,"small":469,"alt":462},{"name":251,"slug":252,"color":19,"url":253},{"name":22,"slug":23,"url":24},{"id":475,"slug":476,"url":477,"title":478,"summary":479,"image":658,"category":659,"author":660,"post_type":25,"views":488,"published_at":489,"updated_at":490},{"big":481,"default":482,"slider":483,"mid":484,"small":485,"alt":478},{"name":231,"slug":232,"color":19,"url":233},{"name":22,"slug":23,"url":24},[662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681],{"name":493,"slug":494,"url":495},{"name":497,"slug":498,"url":499},{"name":501,"slug":502,"url":503},{"name":505,"slug":506,"url":507},{"name":509,"slug":510,"url":511},{"name":513,"slug":514,"url":515},{"name":517,"slug":518,"url":519},{"name":521,"slug":522,"url":523},{"name":525,"slug":526,"url":527},{"name":529,"slug":530,"url":531},{"name":533,"slug":534,"url":535},{"name":537,"slug":538,"url":539},{"name":541,"slug":541,"url":542},{"name":544,"slug":544,"url":545},{"name":547,"slug":548,"url":549},{"name":78,"slug":79,"url":80},{"name":552,"slug":553,"url":554},{"name":556,"slug":557,"url":558},{"name":560,"slug":561,"url":562},{"name":564,"slug":565,"url":566},["Reactive",683],{"$scolor-mode":684,"$ssite-config":686},{"preference":685,"value":685,"unknown":5,"forced":51},"system",{"env":687,"name":688,"trailingSlash":5,"url":689},"production","sparkproxy-nuxt","https:\u002F\u002Fwww.sparkproxy.io",["Set"],["ShallowReactive",692],{"$f67pYq0kdLAGcy5sgC80vqLM66Ou-XjEZNkH-7pE-f3E":28,"$fgXKV-xG1_uJG0vR6_p2m24KPHRxI0wnInHk_RdZa6fw":28,"$ftobFREwkbef1kEw4fzFEYg3CR0Z57HebcGPY6pmOlQk":28,"blog-sidebar":28},"\u002Fblog\u002Famazon-price-tracker-python"]</script></body></html>