Skip to main content
Every 15 minutes a sweep opens each watched page in headless Chromium, pulls out its readable text and compares it with the previous check. When the text differs, an OpenAI model decides whether a person would care and writes a one-line summary, and changes that matter post to Slack. A small HTTP API adds, lists and removes the pages.

How it fits together

Two maps hold the watch list and the last text seen for each page; each entry is small and keyed by watch, so a map fits better than a volume. The OPENAI_API_KEY and SLACK_WEBHOOK_URL secrets reach only check-page. Between sweeps nothing runs, and the model call happens only when a page’s text changed.

Run it

1

Download

The CLI installs once with uv tool install lazycloud-client. The commands below download a standalone project and install its dependencies; no repository checkout is needed. The guide then runs from that directory through uv run, which uses the project’s pinned SDK.
Playwright, trafilatura and the OpenAI client sit in the check dependency group, which only the remote image installs.
2

Configure

The script asks for an OpenAI API key and a Slack incoming webhook URL and stores both as secrets.
3

Deploy

The deploy builds both images, registers the schedule and prints the watches URL. The browser image takes longer the first time because it installs Chromium.
4

Use

Requests to the API need an access token for the workspace. With LAZYCLOUD_TOKEN set and the printed URL in place of <watches-url>:
The first sweep stores a baseline for a new page. To check now instead of waiting for the next quarter hour:
The result lists each page’s outcome, one of baseline, unchanged, minor_change and alerted, and the pages that failed. uv run lazycloud task list --app site_monitor shows each page’s logs and retries.

A schedule that fans out with .map

A schedule is a function with cron; .map yields None for a page whose retries ran out, and a sweep still running at the next quarter hour delays that run rather than overlapping it.
site_monitor/app.py

A browser function with an autoscaler

The autoscaler adds a container per four queued pages, up to five, so at most 20 browsers run at once, and max_pending_tasks leaves room for a retried sweep that submits every page again.
site_monitor/app.py

Maps for watches and snapshots

A map key expires after seven days unless set with ttl=0, so watches get ttl=0 and snapshots keep the default, which lets a snapshot left by a removed watch expire on its own.
site_monitor/api.py

One alert per run across retries

A retried call reruns from the top, so the snapshot carries the run’s ID; a retry in the same run finds its own snapshot, resends an alert that failed to deliver and skips one already sent.
site_monitor/checks.py

An ASGI app behind platform auth

An ASGI app is authorized by default, so a plain FastAPI app bound to a module attribute needs no auth code of its own.
site_monitor/app.py

Adapt it

  • A selector such as #pricing-table watches one element, and focus tells the judge what counts, for example “new job postings in Europe”.
  • cron sets the interval; "0 * * * *" checks hourly and cuts the cost fourfold. MODEL in judge.py picks the OpenAI model.
  • notify can be any function that takes a watch and a summary, such as an email or a ticket in your issue tracker.

Clean up

deployment stop sweep pauses only the schedule and keeps the API and the stored pages.