Build a price monitoring agent
Price trackers break the moment a retailer changes their HTML. CSS selectors rot, sites add anti-scraping, and every store lays out its price differently. This guide takes a different approach: scrape each product page to clean Markdown, then hand it to an LLM to read the price — no brittle selectors, works across any store. We chain /search (find product URLs), /scrape (clean Markdown), and an LLM (parse the price) into a scheduled agent that emails or Slacks you when a price drops.
1. What you'll build
A Python agent that:
- Takes a list of product URLs (or product names to search for)
- Scrapes each page to clean Markdown
- Uses an LLM to extract the current price from the Markdown
- Compares against a stored baseline price
- Sends an alert if the price drops below a threshold
Because the price comes out of an LLM reading Markdown — not a hardcoded selector — the same code works on Amazon, a Shopify store, or a SaaS pricing page without modification.
2. Setup
pip install openai requests python-dotenv
Create a .env file with your two keys:
SUPERHIGHWAY_API_KEY=your_key_here
OPENAI_API_KEY=your_key_here
3. Find product listing URLs with /search
If you already have the product URL, skip this. But often you only know the product name. /search finds the listing page — we bias toward results that look like product pages.
import requests, os, json
SUPERHIGHWAY_KEY = os.getenv("SUPERHIGHWAY_API_KEY")
BASE = "https://superhighway.walls.sh"
def find_product_url(product_name: str, store: str = "") -> str | None:
"""Search for a product listing page."""
query = f"{product_name} {store} buy price" if store else f"{product_name} buy price"
r = requests.get(
f"{BASE}/search",
params={"q": query, "limit": 5},
headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
)
results = r.json().get("results", [])
# Return first result that looks like a product page
for result in results:
url = result.get("url", "")
if any(kw in url for kw in ["amazon", "shop", "store", "product", "item"]):
return url
return results[0]["url"] if results else None
4. Scrape the product page to Markdown
/scrape returns the page as clean Markdown — no nav, scripts, or cookie banners — so the LLM sees only content. We truncate to keep the prompt small; the price is almost always near the top of a product page.
def scrape_product_page(url: str) -> dict:
"""Scrape a product page and return clean markdown."""
r = requests.get(
f"{BASE}/scrape",
params={"url": url},
headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"}
)
data = r.json()
return {
"url": url,
"title": data.get("title", ""),
"markdown": data.get("markdown", "")[:3000],
}
5. Extract the price with an LLM
This is the part that makes the agent robust. Instead of parsing HTML, we ask an LLM to read the Markdown and return just the current selling price as a number. It handles currency symbols, thousands separators, "was $X now $Y" sale formats, and per-store layout differences — all the things that break selector-based scrapers.
from openai import OpenAI
llm = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def extract_price(product_info: dict) -> float | None:
"""Use an LLM to extract the current price from markdown."""
response = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "Extract the current selling price from product page content. Return ONLY a number (no currency symbol, no commas). If no price found, return null."
},
{
"role": "user",
"content": f"Product: {product_info['title']}\n\nPage content:\n{product_info['markdown']}"
}
],
response_format={"type": "json_object"}
)
try:
result = json.loads(response.choices[0].message.content)
price_str = str(result.get("price", "")).replace(",", "").replace("$", "")
return float(price_str)
except (ValueError, KeyError):
return None
6. Store and compare prices
To detect a drop you need a baseline. We keep a simple JSON file mapping each URL to its baseline and last-seen price. The first time we see a product, its baseline is the current price; after that we compare every check against that baseline.
from datetime import datetime
PRICES_FILE = "price_history.json"
def load_price_history() -> dict:
if os.path.exists(PRICES_FILE):
with open(PRICES_FILE) as f:
return json.load(f)
return {}
def save_price_history(history: dict):
with open(PRICES_FILE, "w") as f:
json.dump(history, f, indent=2)
def check_price_drop(url: str, current_price: float, threshold_pct: float = 5.0) -> dict:
"""Compare current price to historical baseline."""
history = load_price_history()
result = {
"url": url,
"current_price": current_price,
"drop_detected": False,
"drop_pct": 0.0,
}
if url in history:
baseline = history[url]["baseline_price"]
drop_pct = ((baseline - current_price) / baseline) * 100
result["baseline_price"] = baseline
result["drop_pct"] = round(drop_pct, 2)
result["drop_detected"] = drop_pct >= threshold_pct
# Update history
history[url] = {
"baseline_price": history.get(url, {}).get("baseline_price", current_price),
"last_price": current_price,
"last_checked": datetime.now().isoformat(),
}
save_price_history(history)
return result
7. Send price drop alerts
The default alert just prints, which is enough to verify the agent works. Uncomment the Slack block (or swap in email / a webhook) to get notified wherever you actually watch for deals.
def send_alert(product_title: str, result: dict):
"""Print alert (extend to email/Slack/webhook as needed)."""
print(f"\n🔔 PRICE DROP ALERT: {product_title}")
print(f" URL: {result['url']}")
print(f" Baseline: ${result['baseline_price']:.2f}")
print(f" Current: ${result['current_price']:.2f}")
print(f" Drop: {result['drop_pct']}%")
# Add Slack webhook (optional):
# slack_url = os.getenv("SLACK_WEBHOOK_URL")
# if slack_url:
# requests.post(slack_url, json={"text": f"Price drop: {product_title} is ${result['current_price']:.2f} (was ${result['baseline_price']:.2f})"})
8. The full monitoring pipeline
Now wire it together. Each product can be a URL you already have, or just a name and store to search for. The loop scrapes, extracts, compares, and alerts.
def monitor_products(products: list[dict], alert_threshold_pct: float = 5.0):
"""
products: list of {"name": "...", "url": "...", "store": "..."} dicts
url is optional — if missing, we search for it
"""
print(f"Checking {len(products)} products...")
for product in products:
name = product["name"]
url = product.get("url") or find_product_url(name, product.get("store", ""))
if not url:
print(f" Could not find URL for: {name}")
continue
# Scrape the page
page = scrape_product_page(url)
# Extract price with LLM
price = extract_price(page)
if price is None:
print(f" Could not extract price for: {name}")
continue
print(f" {name}: ${price:.2f}")
# Check for drops
result = check_price_drop(url, price, alert_threshold_pct)
if result["drop_detected"]:
send_alert(page["title"], result)
else:
print(f" No significant drop (baseline: ${result.get('baseline_price', price):.2f})")
if __name__ == "__main__":
PRODUCTS = [
{"name": "Sony WH-1000XM5 headphones", "store": "amazon.com"},
{"name": "iPad Air M2", "store": "apple.com"},
{"name": "Kindle Paperwhite", "url": "https://www.amazon.com/dp/B09TMF6742"},
]
monitor_products(PRODUCTS, alert_threshold_pct=5.0)
9. Schedule it (cron or GitHub Actions)
A price monitor is only useful if it runs on its own. GitHub Actions is the zero-infrastructure option: a daily cron, your keys as secrets, and the price history persisted as a build artifact.
# .github/workflows/price-monitor.yml
name: Price Monitor
on:
schedule:
- cron: '0 9 * * *' # Daily at 9 AM UTC
workflow_dispatch:
jobs:
monitor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: pip install openai requests python-dotenv
- run: python price_monitor.py
env:
SUPERHIGHWAY_API_KEY: ${{ secrets.SUPERHIGHWAY_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- uses: actions/upload-artifact@v4
with:
name: price-history
path: price_history.json
10. What to monitor
Because the LLM reads Markdown rather than a fixed selector, the same agent works far beyond retail product pages:
- E-commerce — product pages on any retailer;
/scrapereturns clean text and the LLM handles varied price formats. - SaaS pricing pages — get notified when a vendor changes their pricing tiers.
- Travel — flight or hotel price trackers.
- Job boards — track when a freelance rate or salary band changes.
- Real estate — listing price changes.
11. Extending the agent
- Store history in SQLite or a hosted database for long-term trend analysis.
- Add a
/search?q=product+name+price+drop+couponstep to find discount codes when a price does drop. - Use
/newsto get context on why a price changed — a product discontinuation, sale event, or supply shock. - Build a comparison report across multiple stores using the competitor analysis guide pattern.
12. Getting your API key
Grab a free Superhighway key at /pricing (1,000 calls/month, no credit card). For an agent that provisions its own access, skip the key entirely with x402: it pays $0.002 per call in USDC on Base — no signup, no key management. See the x402 pay-per-call guide for the wallet setup.
From here, the web change detection guide generalizes this into monitoring any page for any change, and the search-and-read guide goes deeper on combining /search and /scrape.