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

How to Build a Restock Alert Bot With Proxies

Build a restock alert bot in Python: pull the in-stock signal, rotate an IP per poll, jitter checks 1-5 minutes, and fire one alert per restock. With costs.

S SparkProxy 0 25 min read
Share

A restock alert bot has one job: tell you within a few minutes that an item you want has gone from sold out to buyable, and tell you exactly once. The polling part is easy. The hard parts are reading stock status reliably, not getting your IP throttled after a day of checks, and not waking up to forty duplicate pings because a block page looked like "out of stock". This guide builds the whole poll-and-diff pipeline in Python, with SparkProxy rotating proxies or the Scraping API doing the fetching, and ends with what 1,000 checks actually cost.

The poll-and-diff loop

Every restock monitor, from a hobby script watching one graphics card to a reseller tool watching 400 SKUs, runs the same loop:

  1. Pick the next product that is due, based on its own jittered timer.
  2. Fetch it through a proxy, ideally a different exit IP from the last poll.
  3. Extract a stock signal and reduce it to IN, OUT or UNKNOWN.
  4. Compare with the last confirmed state stored for that item.
  5. If the item moved from a confirmed OUT to IN, record the event and send one alert.
  6. Schedule the next check 1 to 5 minutes out.

Nothing in that loop stores page history. You only keep the latest confirmed state per item plus a log of alerts sent, which is what makes this different from general change tracking. If you want field-level diffs, content hashes or 304 handling for whole pages, our guide to incremental web scraping and change detection covers that broader problem. A restock bot needs one bit per item and a memory of when it last flipped.

The data flow is small enough to fit in one SQLite file. Every Python block in this guide uses only the standard library plus requests, and each one compiles under Python 3.14.2 (we checked with py_compile; requests 2.34.2 was the current PyPI release on 2026-10-30).

import sqlite3

SCHEMA = """
CREATE TABLE IF NOT EXISTS items (
    item_key     TEXT PRIMARY KEY,   -- url, or url#variant for one size
    url          TEXT NOT NULL,
    signal       TEXT NOT NULL,      -- 'shopify_js' | 'jsonld' | 'button'
    variant_id   TEXT,               -- only for shopify_js
    country      TEXT DEFAULT 'us'
);
CREATE TABLE IF NOT EXISTS item_state (
    item_key     TEXT PRIMARY KEY REFERENCES items(item_key),
    state        TEXT NOT NULL,      -- last CONFIRMED state: 'IN' or 'OUT'
    since        TEXT NOT NULL,      -- when that state was first seen
    unknown_run  INTEGER DEFAULT 0,  -- consecutive UNKNOWN results
    next_due     REAL NOT NULL       -- unix time of the next poll
);
CREATE TABLE IF NOT EXISTS alerts (
    item_key     TEXT NOT NULL,
    out_since    TEXT NOT NULL,      -- identifies the sold-out period that ended
    detected_at  TEXT NOT NULL,
    sent         INTEGER DEFAULT 0,
    UNIQUE (item_key, out_since)
);
"""

def open_db(path="restock.db"):
    db = sqlite3.connect(path, isolation_level=None)  # explicit transactions below
    db.executescript(SCHEMA)
    return db

The UNIQUE (item_key, out_since) constraint is the deduplication. One sold-out period can end only once, so it can only ever produce one alert row, however many workers or restarts touch it.

A watchlist that knows how to read each page

Store, alongside each URL, the method used to read it. Stores differ, and guessing at runtime wastes requests. A watchlist entry needs four fields: the URL, the signal type, an optional variant ID when you care about one size or colour, and the country the store should see you from.

WATCHLIST = [
    # A Shopify store: the product JSON endpoint, watched at variant level
    {"url": "https://shop.sparkproxy.io/products/trail-runner",
     "signal": "shopify_js", "variant_id": "794864233", "country": "us"},
    # A store that publishes Product JSON-LD
    {"url": "https://store.sparkproxy.io/p/console-bundle-2tb",
     "signal": "jsonld", "variant_id": None, "country": "gb"},
    # A store with neither: read the add-to-cart button
    {"url": "https://outlet.sparkproxy.io/item/88231",
     "signal": "button", "variant_id": None, "country": "us"},
]

def seed(db, now):
    for w in WATCHLIST:
        key = w["url"] + ("#" + w["variant_id"] if w["variant_id"] else "")
        db.execute("INSERT OR IGNORE INTO items VALUES (?,?,?,?,?)",
                   (key, w["url"], w["signal"], w["variant_id"], w["country"]))
        db.execute("INSERT OR IGNORE INTO item_state(item_key, state, since, next_due) "
                   "VALUES (?, 'OUT', 'seed', ?)", (key, now))

Seeding every item as OUT looks like it could fire a false alert on the first poll if the item is already in stock. That is deliberate for a watchlist of sold-out items you are waiting on. If you add items that may already be in stock, seed them as 'IN' so only a future sell-out followed by a restock triggers.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Three places a page admits it has stock

Pick the signal in this order, because each step down is more fragile than the last.

SignalWhere it livesWhat "in stock" looks likeBreaks when
Hidden product APIA JSON endpoint the storefront itself callsA boolean such as `"available": true` per variantRarely; it is a public contract the theme depends on
JSON-LD `offers.availability``