Home Lab · Automation

A Discord bot that watches retail stock so you don't have to

Five retailers, two completely different scraping strategies depending on how hard each site fights back, marketplace-seller filtering so an alert actually means MSRP, and a watchlist that manages its own size and freshness without manual cleanup.

Stack  Python, nextcord, curl_cffi, Playwright (partial) Runs on  A Raspberry Pi 5, systemd-managed Version  v14
OVERVIEW

What this actually is

A Discord bot that polls product pages across multiple retailers and pings a channel the moment something's worth acting on — back in stock, a real price change, or a Pokemon Center queue going live.

It started as a single-product Target stock checker and grew into a proper multi-retailer system, currently on v14. Target, Walmart, and Pokemon Center get full active monitoring; Amazon and Best Buy sit in a second tier built for sites that need heavier tooling to read reliably. The whole thing is alert-only — no automated add-to-cart, no queue-joining, no purchasing. It tells you something changed; you still do the buying.

  • Pokemon CenterQueue-detection, header/cookie/redirect/keyword based
  • TargetActive monitoring — stock, price, seller
  • WalmartActive monitoring — stock, price, seller
  • AmazonSecond tier — real-browser monitor
  • Best BuySecond tier — real-browser monitor
  • ARCHITECTURE

    Why this is two bots, not one

    Target and Walmart both embed their product data as plain, server-rendered JSON right there in the first HTTP response — Target's page includes a standard application/ld+json block with availability and price, Walmart's includes a __NEXT_DATA__ script tag with the same. A regular HTTP request with realistic browser headers (and, for one site that actively challenges bots, TLS fingerprint spoofing via curl_cffi) gets everything needed. No JavaScript execution, no real browser. That's what makes it cheap enough to run this bot 24/7 on a Raspberry Pi with basically no resource cost.

    Amazon doesn't play along the same way. It's known to be considerably more aggressive about blocking anything that isn't a real browser. Getting reliable stock/price data back means actually rendering the page — which is a completely different weight class: a browser process spun up per check, not a lightweight GET request.

    i

    So the design splits in two "Bot A" is the always-on headless bot covering everything plain HTTP can reach. "Bot B" is a separate, real-browser process (Playwright) reserved for sites that need it. They share one Discord webhook so alerts land in the same place either way — the split is purely about how each check happens, not where the results go, and any future retailer joins whichever tier its own page actually requires.

    HOW EACH SITE IS READ

    The actual signal each retailer gives up

    Every one of these was found by opening a real product page and reading what came back — not from a public API, not from documentation. Retail sites don't publish this.

    RetailerWhere the data livesWhat's extracted
    Target<script type="application/ld+json">Availability, price, seller (schema.org Offer fields)
    Walmart<script id="__NEXT_DATA__">Availability, price, seller name
    Pokemon CenterResponse headers, cookies, redirect target, HTML keywords — checked in that priority orderQueue-it bot-wall state (not a per-product check)
    Amazon (Bot B)Full rendered page text — no structured data block at allStock phrase, exact remaining-count phrase, buy-box presence, seller

    The Amazon row is the interesting one, since there's no JSON to just parse — the signal is whatever a human would visually read off the page. A few of the real phrases that turned out to be reliable:

    • "Only N left in stock - order soon." — genuinely in stock, but low enough to be worth its own urgency tier, not just a flat in/out flag.
    • "Currently unavailable." paired with "We don't know when or if this item will be back in stock." — a real, non-transient out-of-stock, not a blip.
    • "See All Buying Options" with no "Add to cart" anywhere on the page — the listing loads fine (a normal HTTP 200) but isn't actually purchasable through the main listing. A naive "page loaded, so it's in stock" check gets this one wrong.
    A REAL PROBLEM

    Not every "in stock" alert should fire

    Target, Walmart, and Amazon all allow third-party marketplace sellers on their platforms — effectively scalpers, frequently priced well above MSRP. A stock alert with no seller distinction is worthless if half of them are for a listing marked up 3x.

    Each retailer's check also extracts a seller field alongside availability and price. When a listing is confirmed sold by someone other than the retailer itself, the loud stock-drop alert gets suppressed — the item is still tracked and visible on request, it just doesn't interrupt anyone for a price nobody wants to pay. An absent seller field is treated as "sold by the retailer" rather than "unknown," so the design leans toward not missing genuine restocks over aggressively filtering.

    i

    Two different ways of knowing a field is real Amazon's seller text ("Ships from Amazon / Sold by X") comes from directly reading real rendered product pages — it's exactly what a person sees on screen. Target and Walmart's seller fields come from the same structured data blocks already used for availability and price, following the shape those blocks are generally documented to have. Same outcome, two different kinds of evidence behind it.

    USER-FACING

    A watchlist that manages itself

    Adding something to watch is just posting a link in a dedicated Discord channel — no rigid syntax required for the common case. The bot detects the retailer from an explicit prefix (walmart: <url>) or, failing that, just sniffs it out of the URL's domain. Every message gets an explicit reply, confirmation or otherwise — nothing about this fails silently.

    Two lifecycle rules keep a list from just growing forever:

    • A hard cap per retailer. Once a list hits its limit, adding one more evicts the single oldest entry automatically. The confirmation message says exactly what got dropped.
    • A 30-day full reset per retailer. Independent of the cap — a list that never gets full could otherwise sit there indefinitely with stale entries nobody's thought about in weeks. Every 30 days since it was last touched, a retailer's whole list clears itself and posts a notice explaining why.