Landing Page Tracker
A weekly cron agent that visits competitor URLs with headless Chromium, screenshots each page, diffs against last week's screenshots with perceptual hashing, and uses the AI to describe what changed. Weeks of screenshots accumulate into a competitive intelligence timeline.
This cookbook assumes you've already installed the CLI and set up an API key.
1. Scaffold the Agent
bash
swarmlord init landing-page-tracker \
-s "0 9 * * 1" \
-p "Run the weekly competitive landing page audit." \
-m "google/gemini-3-flash-preview"2. Configure Tools and Schedule
Replace swarmlord.jsonc — this agent needs the browser, bash (for image diffing), and write access, but doesn't need web search:
jsonc
{
"$schema": "https://swarmlord.ai/config.json",
"name": "landing-page-tracker",
"model": "google/gemini-3-flash-preview",
"schedule": {
"cron": "0 9 * * 1",
"prompt": "Run the weekly competitive landing page audit.",
},
"tools": {
"bash": true,
"browser": true,
"read": true,
"write": true,
"edit": true,
"glob": true,
"grep": true,
"skill": true,
"websearch": false,
"webfetch": false,
},
"permission": { "*": "allow" },
}Key choices:
browser: true— headless Chromium for full-page screenshots.schedule.cron: "0 9 * * 1"— fires every Monday at 9 AM UTC.websearch: false— the agent doesn't need to search the web, just visit known URLs.
3. Write the Instructions
Edit AGENTS.md:
markdown
# Landing Page Tracker
You are a competitive intelligence agent. Every week you screenshot competitor
landing pages, diff them against last week's screenshots, and produce a changelog
describing what changed.
## Data Layout
- `/workspace/competitors.json` — list of competitor URLs to track
- `/workspace/persist/screenshots/` — organized by date: `persist/screenshots/2026-03-24/example.com.png`
- `/workspace/persist/changelog.md` — append-only changelog with dated entries
Everything under `/workspace/persist/` survives between runs — see
[Persistent Storage](/cli#persist) for details.
## Workflow
1. Read `/workspace/competitors.json` to get the URL list
2. Determine today's date and create a folder: `/workspace/persist/screenshots/YYYY-MM-DD/`
3. Load the **image-diffing** skill
4. For each competitor URL:
a. Use the browser tool to take a full-page screenshot
b. Save it to `/workspace/persist/screenshots/YYYY-MM-DD/<domain>.png`
c. Find last week's screenshot at the most recent prior date folder
d. If a prior screenshot exists, diff the two images using the perceptual
hashing technique from the skill
e. If the diff score exceeds the threshold, analyze both images and describe
what changed in plain English
5. Append a dated section to `/workspace/persist/changelog.md` with all findings
6. Write a summary to `/workspace/latest-report.md`
## Screenshot Guidelines
- Use the browser `screenshot` action with full-page capture
- Wait for network idle before capturing — pages may lazy-load content
- Use a 1280px viewport width for consistency
## Changelog Format
Each weekly entry in changelog.md should follow this structure:
## 2026-03-24
### example.com
- **Changed:** Hero headline updated from "Ship faster" to "Ship smarter"
- **Changed:** New pricing tier ("Enterprise") added to pricing table
- **Diff score:** 0.14
### competitor.io
- **No changes detected** (diff score: 0.01)
## Guidelines
- Always load the image-diffing skill before comparing screenshots
- If a competitor URL fails to load, log it and continue with the rest
- Include diff scores so trends are trackable over time
- When describing changes, be specific: quote old vs new copy, note layout shifts, mention added/removed sections4. Add the Image Diffing Skill
Create skills/image-diffing/SKILL.md:
markdown
---
name: image-diffing
description: Perceptual hashing technique for comparing screenshots and detecting visual changes
---
# Image Diffing with Perceptual Hashing
Compare two screenshots to detect meaningful visual changes using Python's
`imagehash` library. This runs in the sandbox — install dependencies first.
## Setup
```bash
pip install Pillow imagehash
```
## Diffing Two Images
```python
from PIL import Image
import imagehash
def diff_screenshots(path_a: str, path_b: str, threshold: float = 0.08) -> dict:
"""Compare two screenshots. Returns diff score and whether they changed."""
img_a = Image.open(path_a)
img_b = Image.open(path_b)
hash_a = imagehash.phash(img_a, hash_size=16)
hash_b = imagehash.phash(img_b, hash_size=16)
# Normalized hamming distance (0.0 = identical, 1.0 = completely different)
max_bits = 16 * 16
distance = (hash_a - hash_b) / max_bits
return {
"score": round(distance, 4),
"changed": distance > threshold,
"hash_a": str(hash_a),
"hash_b": str(hash_b),
}
```
## Threshold Guidance
| Score Range | Interpretation |
| ----------- | --------------------------------------------------- |
| 0.00 – 0.03 | Identical or near-identical (compression artifacts) |
| 0.03 – 0.08 | Minor changes (time-sensitive content, ads) |
| 0.08 – 0.20 | Meaningful changes (copy, layout, images) |
| 0.20+ | Major redesign or completely different page |
Use `0.08` as the default threshold. Adjust per-competitor if a site has
rotating banners or live data that causes false positives.
## Generating a Visual Diff
To produce a side-by-side comparison image:
```python
from PIL import Image, ImageDraw
def create_side_by_side(path_a: str, path_b: str, output_path: str):
"""Create a labeled side-by-side comparison."""
img_a = Image.open(path_a)
img_b = Image.open(path_b)
# Normalize heights
target_h = max(img_a.height, img_b.height)
if img_a.height != target_h:
img_a = img_a.resize((int(img_a.width * target_h / img_a.height), target_h))
if img_b.height != target_h:
img_b = img_b.resize((int(img_b.width * target_h / img_b.height), target_h))
gap = 20
combined = Image.new("RGB", (img_a.width + img_b.width + gap, target_h + 40), "#0F172A")
combined.paste(img_a, (0, 40))
combined.paste(img_b, (img_a.width + gap, 40))
draw = ImageDraw.Draw(combined)
draw.text((10, 10), "Previous", fill="#94A3B8")
draw.text((img_a.width + gap + 10, 10), "Current", fill="#94A3B8")
combined.save(output_path)
```
Use the side-by-side images in the changelog when a meaningful change is detected —
save them alongside the screenshots in the date folder.5. Add the Competitor List
Create files/competitors.json:
json
[
{
"name": "Acme Corp",
"url": "https://acme.example.com",
"notes": "Main competitor — watch pricing page closely"
},
{
"name": "Widgets Inc",
"url": "https://widgets.example.com",
"notes": "New entrant, track messaging changes"
},
{
"name": "Foo SaaS",
"url": "https://foo-saas.example.com/pricing",
"notes": "Only tracking their pricing page"
}
]This file lands at /workspace/competitors.json in the sandbox. Add, remove, or update competitors here — the agent reads it fresh each week.
6. Seed the Changelog
Create files/persist/changelog.md so the agent has a file to append to from day one. Files in files/persist/ are pre-loaded into /workspace/persist/ on the first run, then the agent's own writes take over on subsequent runs (see Persistent Storage):
markdown
# Competitive Landing Page Changelog
Visual changelog generated weekly by the landing-page-tracker agent.
---7. Final Project Structure
landing-page-tracker/
├── swarmlord.jsonc
├── AGENTS.md
├── skills/
│ └── image-diffing/
│ └── SKILL.md
├── files/
│ ├── competitors.json
│ └── persist/
│ └── changelog.md
└── .gitignore8. Test It Locally
Before deploying, do a dry run with swarmlord run:
bash
cd landing-page-tracker
swarmlord run "Run the weekly competitive landing page audit."The agent will:
- Read the competitor list
- Load the image-diffing skill and install
Pillow+imagehash - Screenshot each competitor URL with headless Chromium
- Since there are no prior screenshots, skip diffing and save the baselines
- Write an initial changelog entry noting "first capture, no prior data to compare"
Check the downloaded artifacts to verify screenshots were captured correctly.
9. Deploy
bash
swarmlord deploy✓ Bundled landing-page-tracker (1 skill, 2 files)
✓ landing-page-tracker deployed
Schedule: 0 9 * * 1 (next: Mon Mar 30 09:00 UTC)The agent now runs every Monday at 9 AM UTC. Each session picks up where the last left off — everything under /workspace/persist/ is automatically backed up after each run and restored into the next one, so screenshots and the changelog accumulate week over week.
10. Inspect Results
After a few weeks of runs, pull the changelog and screenshots from the SDK:
typescript
import { createClient } from "swarmlord";
const client = createClient();
const agent = client.agent("landing-page-tracker");
const sessions = await agent.listSessions({ limit: 5 });
const latest = sessions[0];
const changelog = await latest.getFileText("/workspace/persist/changelog.md");
console.log(changelog);
const screenshot = await latest.getFileBuffer("/workspace/persist/screenshots/2026-03-24/acme.example.com.png");After a few weeks, changelog.md looks like:
markdown
# Competitive Landing Page Changelog
Visual changelog generated weekly by the landing-page-tracker agent.
---
## 2026-03-24
### acme.example.com
- **First capture** — baseline screenshot saved, no prior data to compare.
### widgets.example.com
- **First capture** — baseline screenshot saved, no prior data to compare.
### foo-saas.example.com
- **First capture** — baseline screenshot saved, no prior data to compare.
---
## 2026-03-31
### acme.example.com
- **Changed:** Hero headline updated from "Ship faster" to "Ship smarter with AI"
- **Changed:** New testimonial section added below the fold
- **Diff score:** 0.14
### widgets.example.com
- **No changes detected** (diff score: 0.02)
### foo-saas.example.com
- **Changed:** New "Enterprise" pricing tier added at $299/mo
- **Changed:** "Start free" CTA button changed to "Book a demo"
- **Diff score:** 0.21
---
## 2026-04-07
### acme.example.com
- **No changes detected** (diff score: 0.01)
### widgets.example.com
- **Changed:** Complete hero redesign — new illustration, updated tagline
- **Diff score:** 0.34
### foo-saas.example.com
- **Changed:** Enterprise tier price changed from $299/mo to $249/mo
- **Diff score:** 0.09Extending This Agent
A few ideas for taking this further:
- Add more URLs — drop them into
competitors.jsonany time, no redeploy needed. - Slack notifications — add an
outputsblock to post the weekly summary to a channel. - Track specific selectors — use the browser's
scrapeaction to extract pricing tables or CTAs as structured data, not just screenshots. - Trend analysis — add a skill that reads historical diff scores and flags competitors with accelerating change rates.