Neural Tech Daily
ai-tutorials

Build a Price-Tracker AI Agent: Claude, Playwright, and Email Alerts in Python

Build a Python agent that scrapes Amazon prices with Playwright, classifies deals with Claude, emails alerts via Resend, and runs on a Fly.io cron.

Updated ~14 min read
Share
GitHub social card for microsoft/playwright-python — the Python binding used by the price-tracker agent to render product pages

Image: microsoft/playwright-python on GitHub, used for editorial coverage of the Python binding driving this agent.

What you’ll build

A small Python agent that watches a list of Amazon product URLs, renders each page in headless Chromium via Playwright, asks Claude to read the rendered page and decide whether the current price clears your target, sends an email alert through Resend when it does, and runs on a Fly.io scheduled Machine every few hours. The full project fits in roughly 200 lines of Python plus a fly.toml and a Dockerfile.

The pipeline reads as a chain of four cited sources. Per the Playwright for Python installation guide, a single API drives Chromium, Firefox, and WebKit with built-in auto-waiting for actionable elements, 1 which handles Amazon’s JavaScript-heavy product pages without brittle selector logic. Per the Anthropic vision documentation, all current Claude models accept an image block alongside a text block in the same Messages-API call, 2 so the agent can hand Claude a screenshot of the rendered listing and ask for a structured verdict. Per the Resend Python quickstart, the resend package sends transactional email through a single Emails.send(...) call. 3 Per the Fly.io scheduled-Machines guide, a Machine created with --schedule hourly (or daily, weekly, monthly) runs the container on the requested cadence without a long-running process consuming reserved resources. 4

The differentiator over a selector-based price-watch script is robustness against layout drift. Amazon re-skins product pages routinely, and a #priceblock_ourprice CSS-selector script breaks the day the class renames. A multimodal-LLM pipeline reads the visual the way a buyer does, identifying the displayed price even if the surrounding markup changes, and the email side is decoupled from the scraping side so a render failure doesn’t silently swallow alerts.

Prerequisites

You’ll need:

  • Python 3.10 or newer.
  • An Anthropic API key. Per the Anthropic pricing page, Claude Sonnet 4.6 is billed at $3 per million input tokens and $15 per million output tokens, with image input metered against the input-token bucket; 5 a single product-page screenshot plus a short classification prompt typically runs well under a cent.
  • A Resend account and an API key. The free tier covers low-volume personal alerting at the time of writing per Resend’s pricing page.
  • A Fly.io account, with flyctl installed locally.
  • A verified sender domain on Resend (the free tier allows sending from onboarding@resend.dev for testing).

Set the three secrets in your local environment for the development run:

export ANTHROPIC_API_KEY="sk-ant-..."
export RESEND_API_KEY="re_..."
export ALERT_TO_EMAIL="you@example.com"

The Anthropic Python SDK reads ANTHROPIC_API_KEY from the environment when you instantiate the client without arguments, per its PyPI page. 6 The Resend SDK reads RESEND_API_KEY the same way. 3

Step 1: Install dependencies and define the watch list

Create a project directory and a virtual environment:

mkdir price-tracker && cd price-tracker
python -m venv .venv
source .venv/bin/activate
pip install playwright anthropic resend pydantic
playwright install chromium

The post-install playwright install chromium step downloads only the Chromium binary, which keeps the container image small later; per the Playwright installation guide, the full playwright install pulls Chromium, Firefox, and WebKit. 1

Define the watch list as a JSON file so adding a product later is a one-line edit. Create watchlist.json:

[
  {
    "name": "Sony WH-1000XM5",
    "url": "https://www.amazon.com/dp/B09XS7JWHH",
    "target_price_usd": 299.00
  },
  {
    "name": "Logitech MX Master 3S",
    "url": "https://www.amazon.com/dp/B09HM94VDS",
    "target_price_usd": 79.00
  }
]

target_price_usd is the ceiling: an alert fires when the live price is at or below this value. Keep the file small in development (two or three entries) so a single agent run completes in under a minute.

GitHub social card for microsoft/playwright — the upstream Playwright framework the Python binding wraps

Image: microsoft/playwright on GitHub, used for editorial coverage of the upstream framework.

Step 2: Capture a product-page screenshot

Create tracker.py. The first function loads a URL in headless Chromium and writes a full-page screenshot to disk:

from playwright.sync_api import sync_playwright


def capture_product_page(url: str, output_path: str) -> str:
    """Render a product page and save a full-page screenshot."""
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            viewport={"width": 1280, "height": 1600},
            user_agent=(
                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/124.0.0.0 Safari/537.36"
            ),
        )
        page = context.new_page()
        page.goto(url, wait_until="networkidle", timeout=30000)
        page.screenshot(path=output_path, full_page=True)
        browser.close()
    return output_path

Three details earn their place. wait_until="networkidle" blocks until the page has had no network activity for 500 ms; the Playwright actionability documentation describes this as the standard wait condition for JS-heavy pages. 7 The custom user_agent reduces the chance of Amazon serving a captcha or a stripped mobile layout to the default Playwright UA string. full_page=True tells Playwright to scroll and stitch the result; per the Playwright screenshots reference, the default captures only the viewport. 8

If the target page returns a captcha or an “are you human” interstitial, the screenshot will capture that page instead of the product. The classifier step in the next section detects this and skips the alert; a production-shaped version would add a proxy rotation or use a paid scraping API for sites that aggressively gate headless traffic.

Step 3: Ask Claude for a structured verdict

The classifier sends the screenshot to Claude with a prompt that asks for a single JSON object: the observed price, a boolean for whether the page rendered cleanly, and a one-line rationale. Pydantic validates the result.

Add to tracker.py:

import base64
import json
from pathlib import Path
from anthropic import Anthropic
from pydantic import BaseModel, Field, ValidationError

client = Anthropic()


class PriceVerdict(BaseModel):
    observed_price_usd: float | None = Field(
        description="The displayed price in USD, or null if not visible."
    )
    page_rendered_cleanly: bool = Field(
        description="True if a product page rendered, false on captcha or error."
    )
    rationale: str = Field(description="One short sentence of reasoning.")


CLASSIFIER_PROMPT = """\
Look at this screenshot of an Amazon product page and return ONLY a JSON object
that matches this schema, with no markdown fences and no extra commentary:

{
  "observed_price_usd": <number or null>,
  "page_rendered_cleanly": <true or false>,
  "rationale": "<one short sentence>"
}

Rules:
- If the page shows a captcha, an error, or "Page Not Found", set
  page_rendered_cleanly to false and observed_price_usd to null.
- If multiple prices are shown (list price vs deal price), use the active
  selling price the buyer would pay today, not the strikethrough MRP.
- Use a plain decimal for the price (e.g. 299.00), never a currency symbol.
"""


def classify_price(image_path: str) -> PriceVerdict:
    image_b64 = base64.standard_b64encode(Path(image_path).read_bytes()).decode("utf-8")
    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "source": {
                            "type": "base64",
                            "media_type": "image/png",
                            "data": image_b64,
                        },
                    },
                    {"type": "text", "text": CLASSIFIER_PROMPT},
                ],
            }
        ],
    )
    raw = message.content[0].text
    try:
        return PriceVerdict.model_validate_json(raw)
    except ValidationError as exc:
        raise RuntimeError(f"Claude returned non-conforming JSON: {raw!r}") from exc

A few choices worth narrating. claude-sonnet-4-6 is the canonical Sonnet model ID per the Anthropic models overview; 9 Sonnet is the right register for a classification task because it’s faster and cheaper than Opus while still strong on vision and structured output. max_tokens=512 keeps the response tight: a verdict object never needs more, and a low cap keeps cost predictable. The prompt is explicit about no markdown fences; Claude will sometimes wrap JSON in triple backticks, and that wrapper trips model_validate_json.

GitHub social card for anthropics/anthropic-sdk-python — the official Python SDK used to call Claude's Messages API

Image: anthropics/anthropic-sdk-python on GitHub, used for editorial coverage of the official Python client.

Step 4: Send the alert through Resend

When the verdict comes back with a clean render and a price at or below the target, send an email. Per the Resend Python quickstart, the SDK exposes resend.Emails.send(...) which takes a dictionary with from, to, subject, and html fields and returns the created email’s ID. 3 The full API reference covers the rest of the supported fields including reply_to, cc, and attachments. 10

Add to tracker.py:

import os
import resend


resend.api_key = os.environ["RESEND_API_KEY"]
ALERT_TO = os.environ["ALERT_TO_EMAIL"]
ALERT_FROM = os.environ.get("ALERT_FROM_EMAIL", "onboarding@resend.dev")


def send_alert(product_name: str, url: str, observed: float, target: float) -> str:
    html = f"""
    <h2>Price drop: {product_name}</h2>
    <p>Current: <strong>\\${observed:.2f}</strong>
       (your target: \\${target:.2f})</p>
    <p><a href="{url}">View on Amazon</a></p>
    """
    result = resend.Emails.send(
        {
            "from": ALERT_FROM,
            "to": [ALERT_TO],
            "subject": f"Price drop: {product_name} at \\${observed:.2f}",
            "html": html,
        }
    )
    return result["id"]

Two notes. The from address must be on a domain verified in your Resend dashboard; the default onboarding@resend.dev works for testing but lands in the spam folder for most providers, so configure a real sender before relying on alerts. The HTML body uses inline <strong> rather than a full email template, which keeps the agent compact, and a transactional alert benefits from looking like a quick note rather than a marketing email.

Step 5: Wire the agent loop

The orchestration reads the watch list, runs each entry through capture-then-classify, and dispatches an alert when the verdict crosses the target. Add to tracker.py:

def run_agent(watchlist_path: str = "watchlist.json") -> None:
    items = json.loads(Path(watchlist_path).read_text())
    for item in items:
        name = item["name"]
        url = item["url"]
        target = float(item["target_price_usd"])
        screenshot_path = f"/tmp/{name.replace(' ', '_')}.png"
        try:
            capture_product_page(url, screenshot_path)
            verdict = classify_price(screenshot_path)
        except Exception as exc:
            print(f"[skip] {name}: {exc}")
            continue
        if not verdict.page_rendered_cleanly:
            print(f"[skip] {name}: page did not render cleanly ({verdict.rationale})")
            continue
        if verdict.observed_price_usd is None:
            print(f"[skip] {name}: no price detected")
            continue
        observed = verdict.observed_price_usd
        print(f"[seen] {name}: \\${observed:.2f} (target \\${target:.2f})")
        if observed <= target:
            message_id = send_alert(name, url, observed, target)
            print(f"[alert] {name}: sent email {message_id}")


if __name__ == "__main__":
    run_agent()

The loop is intentionally simple: per-item failures log and continue rather than halt the run, so a single product with a captcha doesn’t block alerts on the other entries. State is in-memory only; the next section adds a deployment that re-runs the agent on a schedule rather than persisting state across runs.

GitHub social card for resend/resend-python — the official Python SDK used to dispatch transactional alert emails

Image: resend/resend-python on GitHub, used for editorial coverage of the Resend Python SDK.

Run the agent locally end-to-end:

python tracker.py

If a watched item is currently below its target, you’ll see an [alert] line and an email lands in the configured inbox within a few seconds.

Step 6: Containerise for Fly.io

Fly.io runs Docker images, so the deployment needs a Dockerfile. The Playwright Python team publishes an official base image with the browser binaries pre-installed, which keeps the container build fast and avoids a custom playwright install step in CI.

Create Dockerfile:

FROM mcr.microsoft.com/playwright/python:v1.45.0-jammy

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY tracker.py watchlist.json ./

CMD ["python", "tracker.py"]

Create requirements.txt:

anthropic>=0.40
playwright>=1.45
pydantic>=2.7
resend>=2.0

The mcr.microsoft.com/playwright/python base image is maintained by Microsoft and ships with Chromium already installed at a pinned version, which removes the slow first-run download from the deployment path.

Initialise the Fly.io app. flyctl launch --no-deploy writes a starter fly.toml, which you then edit to remove the HTTP service block (the agent has no web surface) and to declare the scheduled Machine. Final fly.toml:

app = "price-tracker"
primary_region = "iad"

[build]
  dockerfile = "Dockerfile"

[[vm]]
  memory = "512mb"
  cpu_kind = "shared"
  cpus = 1

Push the three secrets to Fly.io. Per the flyctl secrets reference, flyctl secrets set stages encrypted values and a deploy applies them: 11

flyctl secrets set \
    ANTHROPIC_API_KEY="sk-ant-..." \
    RESEND_API_KEY="re_..." \
    ALERT_TO_EMAIL="you@example.com" \
    ALERT_FROM_EMAIL="alerts@yourdomain.com"

Build and push the image without starting a long-running Machine:

flyctl deploy --no-public-ips

The --no-public-ips flag skips IP allocation since the agent doesn’t serve HTTP.

Step 7: Create the scheduled Machine

Per the Fly.io scheduled-Machines guide, a Machine created with flyctl machine run --schedule <cadence> runs the container on the requested schedule without a continuously-billed instance. 4 Supported schedules at the time of writing are hourly, daily, weekly, and monthly; an hourly run is a reasonable default for low-volume price watching.

Create the scheduled Machine pointing at the image just deployed:

flyctl machine run \
    --schedule hourly \
    --vm-memory 512 \
    registry.fly.io/price-tracker:latest

The Machine stops between runs and only consumes billable time during the run window. List scheduled Machines and inspect the most recent run with:

flyctl machine list
flyctl logs --instance <machine-id>

If a run logs [alert] lines but no email arrives, check the Resend dashboard for delivery status; bounced emails from unverified sender domains land there with a reason code.

GitHub social card for superfly/flyctl — the Fly.io CLI used to deploy and schedule the price-tracker Machine

Image: superfly/flyctl on GitHub, used for editorial coverage of the Fly.io CLI driving the deployment.

Hardening checklist

A toy agent runs once an hour against three URLs without trouble. A production-shaped one earns a few more guards.

  • Persist alert history. The current loop has no memory, so a product that stays below target sends an alert every run. Add a small SQLite file mounted on a Fly.io volume (or a Resend webhook check) and only alert on the first cross from above-target to at-or-below.
  • Retry the vision call on transient errors. Per the Anthropic Messages API reference, the SDK raises typed exceptions for rate-limit and server errors; 12 wrap client.messages.create(...) in a backoff loop for unattended runs.
  • Cap the screenshot size. Per the Anthropic vision documentation, the per-image dimension cap is 8000x8000 pixels; 2 Amazon product pages with long review sections can exceed that. Pass clip={"x": 0, "y": 0, "width": 1280, "height": 4000} to page.screenshot(...) for very long pages.
  • Respect retailer terms. Amazon’s terms of service restrict automated access; this tutorial demonstrates the technical pattern, and any real deployment should review the target retailer’s policy before scaling. The same pattern works against retailers with public price APIs, where the API replaces the scraping step.
  • Pin the model ID. claude-sonnet-4-6 is a pinned snapshot per the Anthropic models overview; 9 pinning the ID rather than relying on an evergreen alias keeps the verdict shape stable across SDK upgrades.

Where to take it next

A few natural extensions sit one step away from the working agent:

  • Multi-retailer watch. Add a retailer field to the watch list and a per-retailer screenshot routine; the classifier prompt and Resend dispatch stay unchanged because they only depend on the rendered visual.
  • Price history charts. Persist each verdict to a tiny SQLite database and graph the trend with a follow-up Fly.io Machine that renders a weekly summary email.
  • Multi-channel alerts. Swap the Resend dispatch for a Discord webhook or a Slack incoming-webhook URL when the alert target is a team channel rather than a personal inbox.
  • Per-product target rules. Replace the single target_price_usd ceiling with a small rules engine — percentage drop from a 30-day rolling average, absolute floor below MSRP, or a window of dates during which alerts are paused.

The full source for the walkthrough is the code blocks above, dropped in order into tracker.py alongside watchlist.json, Dockerfile, requirements.txt, and fly.toml. The hourly Fly.io schedule runs the agent end-to-end on the cadence configured in Step 7, and any price that clears the configured target arrives as an email in the alert inbox.

How this article was made: an autonomous AI pipeline researched, drafted, fact-checked, and reviewed this piece, aggregating publicly-available information from the sources consulted below. AI (artificial intelligence) can make mistakes, so please cross-check the consulted sources before acting on anything here. Neural Tech Daily is not liable for decisions or outcomes based on this article.

Sources consulted

Cited Sources

  1. 1. Playwright for Python — Installation guide (pip install playwright plus playwright install) (accessed )
  2. 2. Anthropic — Vision capabilities documentation (image content blocks, 8000x8000 pixel cap) (accessed )
  3. 3. Resend — Send your first email (Python quickstart with RESEND_API_KEY and Emails.send) (accessed )
  4. 4. Fly.io — Run user code on a schedule (machines created with --schedule hourly / daily / weekly / monthly) (accessed )
  5. 5. Anthropic — API pricing (Claude Sonnet 4.6 input and output token rates; image input metered against input tokens) (accessed )
  6. 6. Anthropic — Python SDK on PyPI (ANTHROPIC_API_KEY environment variable behaviour) (accessed )
  7. 7. Playwright for Python — Auto-waiting and actionability (networkidle wait condition) (accessed )
  8. 8. Playwright for Python — Screenshots API reference (full_page and clip options) (accessed )
  9. 9. Anthropic — Models overview (claude-sonnet-4-6 canonical model ID) (accessed )
  10. 10. Resend — API reference, Send email (full request schema for Emails.send) (accessed )
  11. 11. Fly.io — flyctl secrets reference (set, list, unset for encrypted environment variables) (accessed )
  12. 12. Anthropic — Messages API reference (typed exceptions for rate-limit and server errors) (accessed )

Anonymous · no cookies set

Report a problem with this article

Articles are produced by an autonomous AI pipeline; mistakes do happen. Tell us what's wrong and the editorial review will revisit the claim.

Category

Found this useful? Share it.