Guide #7 Use Case — Strategy 20 min read March 2026

The AI Competitive Intelligence Analyst: Daily Briefing on Autopilot

Every morning at 6am, agents fan out across 30 competitor websites using browser containers. They detect visual changes that DOM diffing would miss, track pricing shifts, monitor product launches, and compile a briefing posted to Slack before standup. Replaces a part-time analyst. Cost: $1.50/month.

Why competitive intelligence is broken

Every product and strategy team wants competitive intelligence. Few have it. The reason: gathering CI is tedious, repetitive, and expensive. Someone has to visit 20–50 competitor websites, read their pricing pages, check for new features, screenshot their homepages, and synthesize it into something useful. That someone is usually a product manager doing it at 11pm before a board meeting, or a junior analyst doing it once a quarter when leadership asks.

The result is competitive intelligence that's stale, incomplete, and reactive. You find out about a competitor's pricing change when a prospect mentions it in a sales call. You discover a new feature launch when your support team gets asked "do you have this like [competitor] does?" You learn about a positioning shift when a blog post goes viral and your CEO forwards it with "thoughts?"

Manual competitive intelligence

  • Updated quarterly (if someone remembers)
  • Covers 5–10 competitors at best
  • Static spreadsheet that's stale by the time it's shared
  • Misses visual changes (new CTAs, redesigns, social proof)
  • No historical record of what changed when
  • $2,000–5,000/quarter in analyst time

AI-powered daily monitoring

  • Runs every morning at 6am, 365 days/year
  • Covers 30+ competitors across every key page
  • Daily Slack briefing: "Here's what changed overnight"
  • Vision-based change detection catches what DOM diffs miss
  • Full screenshot archive: see any competitor's page on any date
  • $1.50/month total cost
$1.50
Monthly cost
6am
Briefing delivered daily
30+
Competitors tracked

How it works

The CI agent runs on a cron schedule. Every morning, a terminal sandbox orchestrates the entire pipeline: launch 30 browser containers (one per competitor), visit their key pages, screenshot everything, compare against yesterday's screenshots using Claude's vision model, and compile a change report.

Cron: 6:00 AM UTC daily
Terminal sandbox (orchestrator)
↓ creates 30 browsers
30 browser containers (parallel)
Chrome #1
Competitor A
homepage + pricing
Chrome #2
Competitor B
homepage + pricing
Chrome #30
Competitor Z
homepage + pricing
Visit pages → screenshot → extract text → return data
↓ 60+ screenshots + extracted data
Compare today vs yesterday (Claude vision)
Post Slack briefing + archive screenshots to S3

The three detection layers

The agent doesn't just compare HTML. It uses three layers of change detection, each catching different types of changes:

Layer 1: Visual change detection (screenshots + Claude vision)

The agent screenshots each page today and compares it to yesterday's screenshot using Claude's vision model. This catches changes that DOM diffing misses: new hero images, redesigned pricing cards, changed CTAs, new customer logos, testimonial updates, banner additions. The vision model describes what changed in natural language: "The pricing page now shows a 'Most Popular' badge on the Business tier, and the Enterprise tier price changed from 'Contact Sales' to '$49/user/month.'"

Layer 2: Text extraction and structured diff

The agent extracts key text from each page — pricing numbers, feature lists, CTA copy — and compares against yesterday's extraction. This catches specific data changes that vision might describe vaguely: a price going from $29 to $39, a feature being added to or removed from a tier, a "Coming Soon" badge appearing on a new capability.

Layer 3: New page detection

The agent checks for new pages that didn't exist yesterday: new blog posts, new case studies, new "vs" comparison pages, new integration announcements. It follows the competitor's sitemap or blog RSS feed and flags new URLs.

Vision catches what text diffing misses

A competitor redesigns their pricing page. The prices didn't change, the feature list didn't change, but the layout now emphasizes the enterprise tier with a larger card and a "Most Popular" badge. A text diff shows zero changes. A vision diff catches the positioning shift instantly. For competitive intelligence, the visual framing is often more important than the raw data.

Configuring the competitor watchlist

json — competitors.json
[
  {
    "name": "Linear",
    "pages": [
      {"url": "https://linear.app", "label": "homepage"},
      {"url": "https://linear.app/pricing", "label": "pricing"},
      {"url": "https://linear.app/changelog", "label": "changelog"},
      {"url": "https://linear.app/blog", "label": "blog"}
    ],
    "extract": {
      "pricing": ["tier names", "prices", "feature lists"],
      "homepage": ["hero headline", "CTA text", "customer logos"]
    }
  },
  {
    "name": "Notion",
    "pages": [
      {"url": "https://notion.so", "label": "homepage"},
      {"url": "https://notion.so/pricing", "label": "pricing"},
      {"url": "https://notion.so/releases", "label": "releases"}
    ],
    "extract": {
      "pricing": ["tier names", "prices", "feature lists", "AI add-on pricing"],
      "homepage": ["hero headline", "social proof numbers"]
    }
  }
]

Comparing screenshots with Claude vision

python — compare.py
import anthropic, base64

client = anthropic.Anthropic()

async def compare_pages(
    yesterday_screenshot: bytes,
    today_screenshot: bytes,
    competitor: str,
    page_label: str,
) -> dict:
    """Compare two screenshots and describe changes."""
    b64_old = base64.b64encode(yesterday_screenshot).decode()
    b64_new = base64.b64encode(today_screenshot).decode()

    response = client.messages.create(
        model="claude-sonnet-4-6-20250514",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": f"""Compare these two screenshots of {competitor}'s {page_label} page.
The first image is yesterday. The second is today.

Describe any meaningful changes:
- Pricing changes (amounts, tier names, feature additions/removals)
- Visual changes (layout shifts, new badges, redesigned cards)
- Copy changes (headlines, CTAs, value propositions)
- New content (logos, testimonials, banners, announcements)

If the pages look identical, respond with exactly: NO_CHANGES
Otherwise, describe each change concisely. Be specific about what changed."""},
                {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": b64_old}},
                {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": b64_new}},
            ],
        }],
    )

    text = response.content[0].text.strip()
    has_changes = text != "NO_CHANGES"

    return {
        "competitor": competitor,
        "page": page_label,
        "has_changes": has_changes,
        "description": text if has_changes else None,
    }

The daily orchestrator

python — ci_monitor.py (runs daily at 6am)
import asyncio, json, time, os, httpx
from datetime import datetime
from pathlib import Path
from playwright.async_api import async_playwright

SANDBOX_URL = os.environ["SANDBOX_URL"]
API_KEY = os.environ["SANDBOX_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
TODAY = datetime.utcnow().strftime("%Y-%m-%d")
ARCHIVE = Path("screenshots")


async def monitor_competitor(client, competitor: dict) -> list:
    """Visit all pages for one competitor, screenshot each."""
    name = competitor["name"]

    # Create browser
    browser_data = (await client.post(
        f"{SANDBOX_URL}/api/browsers", headers=HEADERS,
        json={"browser_type": "chrome", "enable_debug_port": True, "idle_timeout_minutes": 10}
    )).json()
    cid = browser_data["container_id"]

    try:
        # Wait for browser
        for _ in range(40):
            s = (await client.get(f"{SANDBOX_URL}/api/containers/{cid}/status", headers=HEADERS)).json()
            if s["state"] == "assigned": break
            await asyncio.sleep(3)

        results = []
        async with async_playwright() as p:
            browser = await p.chromium.connect_over_cdp(s["cdp_url"])
            page = await browser.contexts[0].new_page()
            await page.set_viewport_size({"width": 1280, "height": 720})

            for pg in competitor["pages"]:
                try:
                    await page.goto(pg["url"], wait_until="networkidle", timeout=20000)
                    await page.wait_for_timeout(2000)

                    # Dismiss cookie banners
                    for sel in ["button:has-text('Accept')", "[class*='cookie'] button"]:
                        try:
                            btn = page.locator(sel).first
                            if await btn.is_visible(timeout=1000):
                                await btn.click()
                                break
                        except: pass

                    # Screenshot
                    img = await page.screenshot(full_page=True)
                    save_path = ARCHIVE / name / TODAY / f"{pg['label']}.png"
                    save_path.parent.mkdir(parents=True, exist_ok=True)
                    save_path.write_bytes(img)

                    # Extract key text
                    text = await page.evaluate("() => document.body.innerText")

                    results.append({
                        "competitor": name, "page": pg["label"],
                        "url": pg["url"], "screenshot_path": str(save_path),
                        "text_excerpt": text[:3000], "status": "ok",
                    })
                except Exception as e:
                    results.append({"competitor": name, "page": pg["label"], "status": "error", "error": str(e)})

        return results

    finally:
        await client.post(f"{SANDBOX_URL}/api/containers/{cid}/release", headers=HEADERS)


async def main():
    with open("competitors.json") as f:
        competitors = json.load(f)

    print(f"Monitoring {len(competitors)} competitors...")

    # Phase 1: Screenshot all competitors in parallel
    async with httpx.AsyncClient(timeout=300) as client:
        tasks = [monitor_competitor(client, c) for c in competitors]
        all_results = await asyncio.gather(*tasks, return_exceptions=True)

    # Flatten results
    pages = [r for batch in all_results if isinstance(batch, list) for r in batch]

    # Phase 2: Compare against yesterday
    changes = []
    for page in pages:
        if page["status"] != "ok": continue
        yesterday_path = page["screenshot_path"].replace(TODAY, "YESTERDAY_DATE")
        # In production, compute yesterday's date and check if file exists
        # If it does, compare using compare_pages() from compare.py
        # Collect changes

    # Phase 3: Build and post Slack briefing
    briefing = build_slack_briefing(changes, pages)
    post_to_slack(briefing)

    print(f"Done. {len(changes)} changes detected across {len(pages)} pages.")

asyncio.run(main())

Handling login walls and authenticated content

Some competitors gate content behind logins (product dashboards, documentation, community forums). Each browser container has its own cookie jar, so you can authenticate once and the session persists for the duration of the monitoring run.

python
# For competitors that require login
{
  "name": "CompetitorX",
  "auth": {
    "login_url": "https://app.competitorx.com/login",
    "username": "${COMPETITORX_USER}",
    "password": "${COMPETITORX_PASS}"
  },
  "pages": [
    {"url": "https://app.competitorx.com/changelog", "label": "changelog"},
    {"url": "https://app.competitorx.com/integrations", "label": "integrations"}
  ]
}

The daily Slack briefing

The output is a structured Slack message that the product team reads over morning coffee. No changes means a quiet morning. Changes trigger discussion.

python — slack_briefing.py
def build_slack_briefing(changes: list, all_pages: list) -> str:
    date = datetime.utcnow().strftime("%B %d, %Y")
    total = len(all_pages)
    changed = len([c for c in changes if c["has_changes"]])

    msg = f"""*Competitive Intelligence Briefing — {date}*
Monitored: {total} pages across {len(set(c['competitor'] for c in changes))} competitors
Changes detected: {changed}
"""

    if changed == 0:
        msg += "\n_No changes detected. All quiet._"
    else:
        msg += "\n---\n"
        for c in changes:
            if c["has_changes"]:
                msg += f"\n*{c['competitor']}* — {c['page']}\n{c['description']}\n"

    msg += f"\n_Full screenshots archived. Run cost: ~$0.05._"
    return msg

What it costs

ItemQuantityUnit costDaily total
Terminal sandbox (orchestrator)1 × ~5 min$0.01/hr$0.001
Browser containers30 × ~3 min avg~$0.03/hr$0.045
Claude Sonnet (comparisons)~60 image pairs~$0.01/pair$0.60 (if all compared)
Daily total~$0.05 (quiet day) — $0.65 (full comparison)
Monthly total$1.50 — $19.50
Optimize: only compare pages that changed at the text layer

The expensive part is the Claude vision comparison ($0.01/pair). Optimize by first doing a cheap text diff on the extracted innerText. If the text hasn't changed, skip the vision comparison — the page probably looks the same. Only call Claude vision on pages where the text changed or on a weekly full-comparison run. This drops the daily cost to ~$0.05 on quiet days.

Production tips

Archive screenshots to S3

Every screenshot is a point-in-time record. Archive them to S3 organized by competitor/date/page.png. Six months later, you can pull up any competitor's pricing page from any date and see exactly what it looked like.

Track changes over time with a simple database

Store each change detection result (date, competitor, page, description) in a SQLite database or DynamoDB table. Query it to answer questions like "when did Linear last change their pricing?" or "how many times has Notion updated their homepage this quarter?"

Use persistent cookies for authenticated monitoring

For competitors that require a login, save the browser's cookies after authentication and inject them into the next day's browser container. This avoids re-authenticating every day and reduces the chance of triggering rate limits or MFA.

Add RSS/blog monitoring for announcements

Most competitors have a blog or changelog with an RSS feed. Add a simple RSS check to the daily run — it's free (no browser needed) and catches product announcements, funding news, and marketing campaigns that might not appear on the homepage.


What's next

Know what your competitors did yesterday

30 competitors. 60 pages. Every morning. A Slack briefing before standup. $1.50/month.

Get Started Free