If you buy a lot on Walmart.com, you've seen those orange Rollback tags — a temporary price cut that Walmart itself slaps on an item, usually a few dollars to a few tens of percent off. There's no newsletter listing every active Rollback across the catalog, and the "deals" pages are curated and partial. If you want them all in one table, you have to scrape search.
This post shows how to pull every Rollback deal from a Walmart search in Python, filter by discount percent, and track the drops day over day with a 60-line SQLite logger.
Code is in github.com/huynhtanan/walmart-deals-tracker (MIT). Disclosure: I built the Apify scraper the examples call; it's pay-per-result at $1 per 1,000 products and new Apify accounts get free monthly credit covering tens of thousands of items.
Setup
pip install "apify-client>=3"
export APIFY_TOKEN=... # https://console.apify.com/settings/integrations (free account)
1. All Rollback items on one search page, with discount %
Walmart's search results already include the strikethrough price when there is one. The scraper exposes it as wasPrice, so computing the discount is (wasPrice - price) / wasPrice.
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("zet46/walmart-search-scraper").call(run_input={
"queries": ["coffee maker", "headphones", "vacuum"],
"maxItemsPerQuery": 30,
"includeSponsored": False,
})
items = list(client.dataset(run.default_dataset_id).iterate_items())
rollbacks = [i for i in items
if i.get("badge") == "Rollback" and i.get("wasPrice") and i.get("price")]
rollbacks.sort(key=lambda x: -(x["wasPrice"] - x["price"]) / x["wasPrice"])
for r in rollbacks[:10]:
pct = (r["wasPrice"] - r["price"]) / r["wasPrice"] * 100
print(f'-{pct:5.1f}% {r["query"]:12} {r["name"][:50]:50} '
f'${r["price"]:>6.2f} (was ${r["wasPrice"]:>6.2f})')
Real output (ran today, 90 products across the three queries, 13 of them Rollbacks):
- 81.6% headphones 2 Pack iPhone wired earbuds USB C Headphones Wired $ 13.99 (was $ 75.99)
- 44.6% vacuum Eureka MaxSwivel Deluxe Upright Vacuum with Swivel $ 49.86 (was $ 89.99)
- 34.1% vacuum Shark Navigator Lift-Away Upright Vacuum, Pet Frie $118.00 (was $179.00)
- 31.8% vacuum BISSELL PowerForce Helix Turbo Pet, Bagless, Uprig $ 74.97 (was $109.99)
- 28.0% vacuum Shark Upright Vacuum, Rotator Pet, Pet Friendly, P $183.00 (was $254.00)
- 25.2% headphones Skullcandy Smokin' Buds XT True Wireless in-Ear Ea $ 14.88 (was $ 19.88)
- 23.8% headphones Apple AirPods 4 $ 99.00 (was $129.99)
- 21.0% headphones Skullcandy Crusher XT Wireless Foldable over-Ear H $ 79.00 (was $100.00)
- 15.6% vacuum BISSELL PowerClean FurFinder 200W Self-Standing Co $168.00 (was $199.00)
- 15.5% coffee maker Keurig K-Duo Essentials Hot & Iced Single Serve K- $109.00 (was $129.00)
A few things that are easy to miss unless you look at the data:
-
Not every strikethrough is a Rollback. 46 of the 90 items in the run had a
wasPrice, but only 13 carried theRollbackbadge. The rest are "clearance", seller-promoted "reduced price" or marketplace resets. If you only want Walmart's own Rollbacks, filter onbadge == "Rollback"; if you want all discounts, filter onwasPrice is not None. -
The top deal is almost always a marketplace seller, not Walmart.com. The 81% off wired earbuds at $13.99 is sold by a third-party store. That's a signal worth filtering on —
seller == "Walmart.com"keeps you in Walmart-fulfilled stock.
The scraper also returns rating, reviewCount, fulfillment (SHIPPING / STORE / PICKUP), availability, sponsored, category (Walmart's own product-type leaf, e.g. Upright Vacuums) and the usItemId you need for the /ip/<usItemId> canonical URL.
2. Daily rollback tracker in SQLite
What you actually want for a watchlist is "same item, cheaper than last time". That takes a tiny SQLite table and a JOIN on usItemId.
# track_rollbacks.py — github.com/huynhtanan/walmart-deals-tracker/blob/main/track_rollbacks.py
import os, sqlite3
from datetime import date
from pathlib import Path
from apify_client import ApifyClient
QUERIES = ["coffee maker", "headphones", "vacuum", "monitor", "office chair"]
MAX_PER_QUERY = 60
DB = Path("walmart_rollbacks.sqlite3")
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("zet46/walmart-search-scraper").call(run_input={
"queries": QUERIES,
"maxItemsPerQuery": MAX_PER_QUERY,
"includeSponsored": False,
})
items = list(client.dataset(run.default_dataset_id).iterate_items())
today = date.today().isoformat()
with sqlite3.connect(DB) as con:
con.execute("""CREATE TABLE IF NOT EXISTS prices (
run_date TEXT, us_item_id TEXT, query TEXT,
name TEXT, price REAL, was_price REAL, badge TEXT, seller TEXT,
rating REAL, review_count INTEGER, url TEXT,
PRIMARY KEY (run_date, us_item_id))""")
for it in items:
if it.get("sponsored"):
continue
con.execute("INSERT OR REPLACE INTO prices VALUES (?,?,?,?,?,?,?,?,?,?,?)",
(today, it["usItemId"], it.get("query"), it["name"],
it.get("price"), it.get("wasPrice"), it.get("badge"),
it.get("seller"), it.get("rating"),
it.get("reviewCount"), it["url"]))
print(f"Rollbacks today ({today}):")
for row in con.execute("""
SELECT name, price, was_price,
ROUND(100.0 * (was_price - price) / was_price, 1) AS pct,
seller
FROM prices
WHERE run_date = ? AND badge = 'Rollback' AND price < was_price
ORDER BY pct DESC LIMIT 20""", (today,)):
print(f" -{row[3]:>5}% {row[0][:60]:60} ${row[1]:>7.2f} (was ${row[2]:>7.2f}) {row[4]}")
3. "Deeper than yesterday" alerts
After two runs, this one SQL query gives you every item whose Rollback got deeper:
WITH deals AS (
SELECT us_item_id, name, price, was_price, badge, run_date,
LAG(price) OVER (PARTITION BY us_item_id ORDER BY run_date) AS prev_price,
LAG(badge) OVER (PARTITION BY us_item_id ORDER BY run_date) AS prev_badge
FROM prices)
SELECT run_date, name, prev_price, price, was_price,
ROUND(100.0 * (was_price - price) / was_price, 1) AS pct_today,
ROUND(100.0 * (prev_price - price) / prev_price, 1) AS dropped_today
FROM deals
WHERE badge = 'Rollback'
AND prev_price IS NOT NULL
AND price < prev_price
ORDER BY dropped_today DESC;
And "new Rollback today" (wasn't on sale yesterday):
WITH t AS (
SELECT us_item_id, badge, run_date,
LAG(badge) OVER (PARTITION BY us_item_id ORDER BY run_date) AS prev_badge
FROM prices)
SELECT p.name, p.price, p.was_price, p.url
FROM t JOIN prices p USING (us_item_id, run_date)
WHERE t.badge = 'Rollback'
AND (t.prev_badge IS NULL OR t.prev_badge != 'Rollback')
AND p.run_date = DATE('now');
The repo has a one-file wrapper that does both and prints them in a terminal-friendly table.
4. Fetch a specific product by URL
If you already have the /ip/<slug>/<usItemId> URL (from an email, a share link, a competitor's page), the scraper enriches it with brand, category, shortDescription, seller, rating, reviewCount and both prices:
run = client.actor("zet46/walmart-search-scraper").call(run_input={
"productUrls": [
"https://www.walmart.com/ip/BISSELL-PowerForce-Helix-Bagless-Upright-Vacuum/55251527",
],
})
Point this at a competitor's hero product and you have a one-call price watcher.
Cost
$1 per 1,000 products returned. Watching the top 60 of five queries daily is 300 products = $0.30/day = $9/month. Running once a week is $1.20/month.
Only billed on items the scraper actually returned. Skipped items (sponsored, deduplicated by usItemId across pages, below minRating, etc.) are free.
A few things that bit me while building this
-
Walmart only ships prices to US IPs. From anywhere else, the
__NEXT_DATA__block arrives withprice: null. The scraper pins traffic through a US Apify data-center proxy by default; you don't have to configure that unless you want residential. -
priceLinesis an array, not an object. The current SSR shape ispriceDetails.priceLines[0].values[{lineType: "COMPARISON", key: "WAS_PRICE", price, ...}]. If you're parsing__NEXT_DATA__yourself and following old blog posts that read.priceDetails.wasPrice.pricedirectly, you'll getNoneon every row — Walmart changed this in late 2024. -
Category is nested on /ip/ detail pages. Search results give you a flat
categoryleaf name (e.g. "Laptop Computers"). The product page gives it as a breadcrumb path; the scraper walks to the deepest node and returns the same string, so you can mix search and detail data in onecategorycolumn. -
The
Rollbackbadge disappears when the sale ends — thewasPricedisappears with it, not just the badge. Keep historical rows so you can tell "the sale ended" apart from "the item was replaced".
Feedback welcome
If you'd like a specific feature — Target / Costco side-by-side, Discord/Slack webhook alerts, a flat-file CSV mode, filtering by brand or price band — the repo issues are open and I ship fixes within a day.
Code and more examples: github.com/huynhtanan/walmart-deals-tracker. Scraper: apify.com/zet46/walmart-search-scraper.
Related tutorials
- Track Amazon Best Sellers in 17 country stores with Python and SQLite — companion tutorial on the Amazon side.
- How to get Google Hotels prices for any dates with Python — Apify API primer with a hotel-pricing vertical.
- Daily price tracking for competitor hotels, with Python and SQLite — same SQLite-diff pattern applied to hotels.
- Add Google Hotels to Claude Desktop as an MCP server (2-minute setup) — expose an Apify scraper to Claude as a natural-language tool.
Top comments (0)