Build a price monitoring agent

Superhighway guides

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:

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:

11. Extending the agent

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.