Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

You can build a self-hosted competitor price monitor with no monitoring SaaS subscription: schedule a Python job, fetch each product page with HTTPX, use Playwright only when a page needs browser rendering, validate and save observations in SQLite, then send meaningful price or stock changes to a Discord webhook. The monitor should fail closed when it cannot establish a trustworthy price; a broken selector or unavailable page must not become a false price alert.

How the price-monitoring pipeline works

For each product, the monitor retrieves the page, extracts a price and availability, validates both, stores an observation, compares it with the previous valid observation, and alerts only on a meaningful change. Keep operational failures—timeouts, blocked requests, missing prices, or changed page structure—separate from business events such as a price decrease.

  1. Schedule: start a recurring run with cron or another scheduler. Use a per-site cadence rather than polling every product as quickly as possible.
  2. Retrieve: try a direct HTTP request first. It is usually simpler and lighter than launching a browser.
  3. Render if needed: use Playwright when the product details are populated by JavaScript or require browser interaction.
  4. Extract and validate: read configured price and stock selectors, normalize the values, and reject missing or ambiguous prices.
  5. Persist: save valid observations and run diagnostics in SQLite.
  6. Compare and alert: compare the new valid observation with the previous valid one, then send a concise Discord notification when price or availability changes.

Playwright can observe and modify HTTP and HTTPS traffic, including XHR and fetch requests, and supports request routing and mocking; see its network documentation. Its request lifecycle distinguishes transport failures from HTTP responses: a 404 or 503 is still a completed response, not automatically a network failure. Check response status before trusting page content; see the Request API documentation.

Choose HTTP or a browser for each target

Approach Use it when Trade-off
HTTPX and HTML parsing The page or a permitted product endpoint returns stable product data without browser execution. Fast and resource-light, but it cannot run page JavaScript or perform browser interactions.
Playwright JavaScript rendering, client-side requests, or browser interaction is needed to expose the product data. Can cover dynamic pages, but uses more CPU and memory and needs browser maintenance.

Keep the retrieval mode in each product’s configuration. A tiered design avoids paying the browser cost for pages that work with a direct request. Network observation can help diagnose where a page obtains its product data, but it does not authorize bypassing anti-bot measures, access controls, or a target site’s terms.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (8GB RAM)
  • Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (8GB RAM)
  • Includes 128GB Micro SD Card pre-loaded with 64-bit Raspberry Pi OS, USB MicroSD Card Reader
  • CanaKit Turbine Black Case for the Raspberry Pi 5
  • CanaKit Low Noise Bearing System Fan
  • Mega Heat Sink - Black Anodized

Configure products and parsing rules

Keep target-specific details out of the monitoring logic. Each product entry should identify its store, URL, SKU, currency, retrieval mode, selectors, and number-format conventions. For a stable setup, also define phrases that indicate in-stock and out-of-stock states. Do not infer availability from a missing button unless that behavior has been checked for the particular page.

{
  "store": "Store name",
  "sku": "ITEM-001",
  "url": "YOUR_PRODUCT_PAGE_URL",
  "currency": "USD",
  "browser": false,
  "price_selector": "CSS selector for the displayed price",
  "availability_selector": "CSS selector for stock status",
  "in_stock_text": ["Add to cart", "In stock"],
  "out_of_stock_text": ["Out of stock", "Unavailable"],
  "decimal_separator": ".",
  "thousands_separator": ","
}

Replace the example values with selectors and URL for a page you are permitted to monitor. If the page uses a different number format, configure its separators explicitly; for example, a comma may be the decimal mark in one locale and the thousands separator in another. Avoid guessing when a displayed value is ambiguous.

Rank #2
CanaKit Raspberry Pi 5 16GB Starter Kit PRO - Turbine Black (128GB Edition) (16GB RAM)
  • Includes Raspberry Pi 5 16GB with 2.4Ghz 64-bit quad-core CPU (16GB RAM)
  • Includes 128GB Micro SD Card pre-loaded with 64-bit Raspberry Pi OS, USB MicroSD Card Reader
  • CanaKit Turbine Black Case for the Raspberry Pi 5
  • CanaKit Low Noise Bearing System Fan
  • Mega Heat Sink - Black Anodized

Build a small monitor with SQLite and Discord

The example below uses HTTPX for retrieval, Beautiful Soup to apply CSS selectors to either the response HTML or the rendered HTML, Playwright as an opt-in browser fallback, SQLite for history, and a Discord incoming webhook for notifications. Install the Python packages httpx, beautifulsoup4, and playwright; install the Playwright browser required by your environment if you enable browser mode. Save product entries in a JSON array in targets.json. Set DISCORD_WEBHOOK_URL in the process environment rather than placing the secret in the file or source code.

import json
import os
import re
import sqlite3
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation

import httpx
from bs4 import BeautifulSoup

DB = "prices.sqlite3"
TARGETS = "targets.json"


def now():
    return datetime.now(timezone.utc).isoformat()


def init_db():
    with sqlite3.connect(DB) as db:
        db.execute("""CREATE TABLE IF NOT EXISTS observations (
            id INTEGER PRIMARY KEY,
            observed_at TEXT NOT NULL,
            store TEXT NOT NULL,
            sku TEXT NOT NULL,
            url TEXT NOT NULL,
            currency TEXT NOT NULL,
            price TEXT NOT NULL,
            availability TEXT NOT NULL
        )""")
        db.execute("""CREATE TABLE IF NOT EXISTS errors (
            id INTEGER PRIMARY KEY,
            occurred_at TEXT NOT NULL,
            store TEXT NOT NULL,
            sku TEXT NOT NULL,
            message TEXT NOT NULL
        )""")


def clean_price(raw, decimal_separator, thousands_separator):
    text = raw.strip().replace("u00a0", " ")
    # Remove currency labels and symbols; retain digits and configured separators.
    allowed = re.sub(r"[^0-9.,'’-]", "", text)
    if not allowed or allowed.count("-") > 1 or ("-" in allowed and not allowed.startswith("-")):
        raise ValueError("Price is empty or malformed")
    if thousands_separator:
        allowed = allowed.replace(thousands_separator, "")
    if decimal_separator != ".":
        if allowed.count(decimal_separator) > 1:
            raise ValueError("Price has multiple decimal separators")
        allowed = allowed.replace(decimal_separator, ".")
    # Any other punctuation can indicate an unconfigured or ambiguous format.
    if re.search(r"[^0-9.-]", allowed) or allowed.count(".") > 1:
        raise ValueError("Price format is ambiguous; check separator configuration")
    try:
        value = Decimal(allowed)
    except InvalidOperation as exc:
        raise ValueError("Price is not numeric") from exc
    if not value.is_finite() or value <= 0:
        raise ValueError("Price must be a positive finite amount")
    return value


def retrieve(target, client):
    if target.get("browser"):
        from playwright.sync_api import sync_playwright
        with sync_playwright() as p:
            browser = p.chromium.launch(headless=True)
            page = browser.new_page()
            response = page.goto(target["url"], wait_until="domcontentloaded", timeout=30000)
            if response is None or response.status >= 400:
                browser.close()
                raise ValueError(f"Browser navigation returned an unusable response: {getattr(response, 'status', 'none')}")
            html = page.content()
            browser.close()
            return html
    response = client.get(target["url"])
    response.raise_for_status()
    return response.text


def extract(target, html):
    soup = BeautifulSoup(html, "html.parser")
    price_node = soup.select_one(target["price_selector"])
    stock_node = soup.select_one(target["availability_selector"])
    if price_node is None or stock_node is None:
        raise ValueError("Configured price or availability selector did not match")
    price = clean_price(
        price_node.get_text(" ", strip=True),
        target.get("decimal_separator", "."),
        target.get("thousands_separator", ","),
    )
    stock_text = stock_node.get_text(" ", strip=True).casefold()
    in_stock = [s.casefold() for s in target["in_stock_text"]]
    out_stock = [s.casefold() for s in target["out_of_stock_text"]]
    is_in = any(s in stock_text for s in in_stock)
    is_out = any(s in stock_text for s in out_stock)
    if is_in == is_out:
        raise ValueError("Availability is missing or ambiguous")
    return price, "in_stock" if is_in else "out_of_stock"


def previous(db, target):
    return db.execute(
        "SELECT price, availability FROM observations WHERE store=? AND sku=? ORDER BY id DESC LIMIT 1",
        (target["store"], target["sku"]),
    ).fetchone()


def send_alert(client, webhook, target, old, price, availability):
    if old is None:
        return  # First valid observation establishes a baseline, not a change.
    old_price = Decimal(old[0])
    if old_price == price and old[1] == availability:
        return
    delta = price - old_price
    pct = (delta / old_price * Decimal("100")) if old_price else Decimal("0")
    color = 3066993 if delta < 0 else 15158332 if delta > 0 else 3447003
    content = (
        f"{target['store']} — {target['sku']}n"
        f"Price: {old_price} {target['currency']} → {price} {target['currency']} "
        f"({delta:+} {target['currency']}; {pct:+.2f}%)n"
        f"Availability: {old[1]} → {availability}n{target['url']}"
    )
    response = client.post(webhook, json={"embeds": [{"description": content, "color": color}]})
    response.raise_for_status()


def run_one(target, client, webhook):
    try:
        html = retrieve(target, client)
        price, availability = extract(target, html)
        with sqlite3.connect(DB) as db:
            old = previous(db, target)
            db.execute(
                "INSERT INTO observations(observed_at,store,sku,url,currency,price,availability) VALUES(?,?,?,?,?,?,?)",
                (now(), target["store"], target["sku"], target["url"], target["currency"], str(price), availability),
            )
        send_alert(client, webhook, target, old, price, availability)
    except Exception as exc:
        with sqlite3.connect(DB) as db:
            db.execute(
                "INSERT INTO errors(occurred_at,store,sku,message) VALUES(?,?,?,?)",
                (now(), target["store"], target["sku"], str(exc)[:1000]),
            )
        print(f"Monitor error for {target.get('store')} / {target.get('sku')}: {exc}")


def main():
    webhook = os.environ.get("DISCORD_WEBHOOK_URL")
    if not webhook:
        raise RuntimeError("Set DISCORD_WEBHOOK_URL in the environment")
    init_db()
    with open(TARGETS, encoding="utf-8") as f:
        targets = json.load(f)
    timeout = httpx.Timeout(20.0, connect=10.0)
    with httpx.Client(timeout=timeout, follow_redirects=True) as client:
        for target in targets:
            run_one(target, client, webhook)


if __name__ == "__main__":
    main()

This starter deliberately records a failed extraction in a separate errors table and does not save it as a price observation. The first valid reading becomes a baseline, so starting the monitor does not generate a misleading change alert. For production use, add structured logs, a per-target error budget, and an operational alert for repeated failures. Also consider retaining response status and a safe diagnostic snapshot when debugging selector changes; avoid storing credentials or unnecessary personal data.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #3
CanaKit Raspberry Pi 5 Essentials Starter Kit (4GB RAM)
  • CanaKit Raspberry Pi 5 Essentials Starter Kit

Make comparisons useful rather than noisy

The example alerts on any price or stock-state difference. For a larger monitor, define what counts as meaningful for each product: a minimum absolute price change, a percentage threshold, or a rule for stock transitions. Keep the underlying observations even when a change does not meet the alert threshold so you retain the history needed to assess trends.

  • Compare the current valid price with the previous valid price; calculate both absolute and percentage deltas.
  • Compare availability as a state transition, such as in_stock to out_of_stock.
  • Track the time since the last successful observation so an old reading is not mistaken for a current one.
  • Include store, SKU, source URL, currency, old and new prices, delta, and stock state in an alert.
  • Send extraction failures to an operational channel or log, not as if they were competitor price changes.

Never substitute a missing price with zero, reuse a stale value as the latest reading, or interpret an HTTP error page as product content. Those shortcuts turn collection failures into false business signals.

Rank #4
SANOOV Raspberry Pi 5 4GB Kit, 4GB RAM Single Board Computer with Active Cooler and ABS Case, Complete Raspberry Pi 5 Starter Kit for IoT Robotics Retro Gaming
  • All-in-One Complete Kit: This SANOOV RPi 5 bundle comes with Raspberry Pi 5 4GB RAM single board, active cooler, durable ABS case and screwdriver. No extra parts needed, ready to use right out of the box for beginners and hobbyists
  • Powerful Single Board Computer: Equipped with 4GB RAM and high-performance processor, delivers fast running speed for 4K playback, AI projects, programming and daily computing tasks. SANOOV for raspberry pi 5 4GB is equipped with broadcom 64 quad-core Arm Cortex A76 processor with gigabit ethernet and upgraded with IEEE 802.11ac Wi-Fi, Bluetooth 5.0 dual-band 2.4Ghz and 5Ghz and Power Over Ethernet (POE). Upgrading delivers 2-3 x speed vs Pi 4, redefining the experience
  • Efficient Active Cooler: Effectively lowers operating temperature and prevents performance throttling. Runs quietly even under long-time heavy load, ensures stable operation all day long. SANOOV RPi 5 4GB kit offer an active cooler, which combines an aluminium heatsink with a high-performance PWM fan. Active cooler is fully compatible with the Pi OS, which can effectively reduce the temperature of RPi5 and ensure its good performance during long-term high load operation
  • Sturdy ABS Protective Case: Well-fitted for Raspberry Pi 5 board, can be secured with 4 screws to effectively protect the Pi 5 motherboard from damage, reserves full access to all ports and buttons. SANOOV uses ABS material to produce the case, which has a softer texture and feel. Meanwhile, SANOOV case adopts a layered design for easy disassembly and installation. (Tip: The Case cannot install M.2 HAT Add on Board and Solid State Drive!)
  • Wide Application & Full Compatibility: Seamlessly compatible with official OS and mainstream peripheral accessories for Raspberry Pi 5. Whether you are a beginner, student, electronics hobbyist or professional developer, this all-in-one kit meets your diverse needs. It excels in IoT projects, robotics design, retro gaming devices, home media servers and other DIY creations. Backed by a large global community, you can easily find guides, technical support and shared projects online
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Schedule responsibly and maintain the monitor

Cron is sufficient for a small self-hosted deployment on an always-on workstation, home server, or VPS. For example, a 30-minute schedule can be expressed as */30 * * * *, but that is only an example, not a universally appropriate interval. Set cadence per site and product based on the site’s terms, the required freshness, and the load a request creates. Add modest jitter when monitoring multiple products, bounded retries with exponential backoff, and request timeouts.

*/30 * * * * cd /path/to/monitor && /path/to/venv/bin/python monitor.py

For reliability, record last-success and last-error times, limit repeated retries, and alert separately when selector failures persist. Review selectors when a product page changes and verify a changed extraction against the rendered page before accepting it. The operational guidance in this practical scraping guide recommends respectful delays, checking robots.txt, and realistic polling intervals.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
RasTech Raspberry Pi 5 8GB Kit with Active Cooler and Pi5 Case
  • 【What you Get】You will get 1*Pi 5 8GB Single Board,1*RasTech Case,1*Active Cooler,1*Screwdriver,1*Installation instructions,12-month free warranty, lifetime service, 24-hour prompt and friendly response.
  • 【More Connectors】There are two USB 3.0 ports(5Gbps simultaneously) and two USB 2.0 ports, which triple total bandwidth ,support any combination of up to two cameras or displays. Peak SD card performance is doubled through support for the SDR104 high-speed mode. It provides a smooth desktop experience for you. Offer Gigabit Ethernet and a PCIe interface, along with dual-band Wi-Fi and Bluetooth 5.0/BLE wireless capability. The RasTech Pi 5 Kit use the new 27W 5.1V 5A USB-C power connector.
  • 【 Support Dual 4Kp60 Display 】Each of the two microHDMI sockets can control a 4K display at 60 Hertz, now support HDR, offering super HD video for media streaming projects. RPi 5 is the first RPi model that comes with a PCI Express port (PCIe 2.0 x1 with 500 MB/s) to attach SSDs (requires separate M.2 HAT).
  • 【 Excellent Chips And Applications】Pi 5 is a full-size Pi computer using silicon built in-house at Pi. The RP1 “southbridge” provides the bulk of the I/O capabilities for Pi 5. Pi 5 is more friendly and convenient in the development of Internet of Things, Web development, machine identification, automatic control and other electronic equipment applications and network.
  • 【 Faster CPU, Better GPU 】 Pi 5 features a Broadcom BCM2712 64-bit quad-core Arm Cortex-A76 processor running at 2.4GHz, it delivers a 2–3× increase in CPU performance relative to RaspberryPi 4. The 800MHz VideoCore VII GPU is compatible to OpenGL ES 3.1 and Vulkan 1.2, substantial uplift in graphics performance. Pi 5 Offers lightning-fast CPU speed, a PCI Express interface, a Real Time Clock (RTC) and a power button and runs significantly cooler than Pi 4.

Respect target-site and Discord rules

A self-hosted script does not make collection unrestricted. Check the target site’s terms and robots.txt, use reasonable intervals, and stop if the site denies access or requires controls you are not authorized to bypass. Playwright is a rendering and inspection tool, not a license to circumvent anti-bot systems or security measures.

Use Discord as the notification destination for information gathered from permitted target sites; do not scrape Discord itself or automate around its security controls. Discord’s Developer Terms prohibit use that exceeds API rate, call, or usage limits, and webhook delivery does not remove that obligation. Follow the Discord Developer Terms and Developer Policy, protect the webhook URL as a secret, and rotate it if exposed.

This design avoids a monitoring SaaS subscription, but it still requires a machine to run the job and time to maintain selectors, parsing rules, and site-specific policies. SQLite is a sensible local history store for a small single-process monitor; a shared database becomes relevant if multiple workers or users need coordinated access.

Quick Recap

Bestseller No. 1
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (8GB RAM)
CanaKit Raspberry Pi 5 Starter Kit PRO - Turbine Black (128GB Edition) (8GB RAM)
Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (8GB RAM); CanaKit Turbine Black Case for the Raspberry Pi 5
$259.95
Bestseller No. 2
CanaKit Raspberry Pi 5 16GB Starter Kit PRO - Turbine Black (128GB Edition) (16GB RAM)
CanaKit Raspberry Pi 5 16GB Starter Kit PRO - Turbine Black (128GB Edition) (16GB RAM)
Includes Raspberry Pi 5 16GB with 2.4Ghz 64-bit quad-core CPU (16GB RAM); CanaKit Turbine Black Case for the Raspberry Pi 5
$419.99
Bestseller No. 3
CanaKit Raspberry Pi 5 Essentials Starter Kit (4GB RAM)
CanaKit Raspberry Pi 5 Essentials Starter Kit (4GB RAM)
CanaKit Raspberry Pi 5 Essentials Starter Kit
$189.99

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.