Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Ready-to-use automation templates for Stagehand and Browserbase. Each template h
| cerebras-docs-checker | - | [PY](python/cerebras-docs-checker) | - | Crawl documentation sites, discover source repos, and verify docs accuracy against actual codebase |
| company-address-finder | [TS](typescript/company-address-finder) | [PY](python/company-address-finder) | - | Discover company legal information and physical addresses from Terms of Service and Privacy Policy pages |
| company-value-prop-generator | [TS](typescript/company-value-prop-generator) | [PY](python/company-value-prop-generator) | - | Extract and format website value propositions into concise one-liners for email personalization |
| competitor-monitoring | [TS](typescript/competitor-monitoring) | - | - | Visit competitor pricing pages, take full-page screenshots, and extract structured pricing data using Claude |
| context | [TS](typescript/context) | [PY](python/context) | - | Persistent authentication using Browserbase contexts that survive across sessions |
| council-events | [TS](typescript/council-events) | [PY](python/council-events) | - | Automate event information extraction from Philadelphia Council |
| download-financial-statements | [TS](typescript/download-financial-statements) | [PY](python/download-financial-statements) | - | Download Apple's quarterly financial statements (PDFs) from their investor relations site |
Expand Down
8 changes: 8 additions & 0 deletions typescript/competitor-monitoring/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Browserbase Configuration
# Get your API key from: https://www.browserbase.com/settings
BROWSERBASE_API_KEY=your_browserbase_api_key
BROWSERBASE_PROJECT_ID=your_browserbase_project_id

# AI Model Configuration
# Used for pricing page analysis via Claude
MODEL_API_KEY=your_anthropic_api_key
69 changes: 69 additions & 0 deletions typescript/competitor-monitoring/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Playwright + Browserbase + Claude: Competitor Pricing Monitor

## AT A GLANCE

- Goal: visit competitor pricing pages, take full-page screenshots, and extract structured pricing data using Claude vision.
- Browser Automation: uses Playwright + Browserbase to navigate pricing pages in a remote browser.
- AI Analysis: sends screenshots to Claude claude-opus-4-7 which extracts plan names, prices, and key features as JSON.
- Flexible Targets: defaults to Asana, Linear, and Notion; pass any URLs as CLI arguments to override.
Docs → https://docs.browserbase.com

## GLOSSARY

- Browserbase: cloud browser infrastructure for reliable, scalable web automation
Docs → https://docs.browserbase.com
- CDP: Chrome DevTools Protocol — used to connect Playwright to a remote Browserbase session
- Claude Vision: multimodal Claude API that can analyze screenshots and extract structured data

## QUICKSTART

1. cd typescript/competitor-monitoring
2. npm install
3. cp .env.example .env
4. Add BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, and MODEL_API_KEY to .env
5. npm start

To monitor custom URLs instead of the defaults:

```
npm start -- https://example.com/pricing https://other.com/pricing
```

## EXPECTED OUTPUT

- Creates a remote Browserbase browser session
- Visits each competitor pricing page and takes a full-page screenshot
- Saves screenshots to ./screenshots/
- Sends all screenshots to Claude for analysis
- Prints a formatted pricing comparison table to the console
- Saves the comparison as ./comparison.md
- Closes the browser session

## COMMON PITFALLS

- "Cannot find module": ensure npm install completed
- Missing credentials: verify .env contains BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, and MODEL_API_KEY
- JSON parse error: Claude occasionally returns markdown-wrapped JSON; the template strips fences automatically
- Pricing page layout changes: Claude's extraction is prompt-based and may need updates if a site restructures its pricing page

## USE CASES

• Track competitor pricing changes over time by scheduling periodic runs and diffing the output.
• Onboard new competitors quickly by passing their pricing URLs as CLI arguments.
• Feed extracted pricing data into a dashboard or alerting system via the JSON output.

## NEXT STEPS

• Schedule runs with a cron job and diff comparison.md against the previous version to detect price changes.
• Add more competitors by extending DEFAULT_COMPETITORS in index.ts or passing additional URLs at runtime.
• Store results in a database to build a pricing history timeline.

## HELPFUL RESOURCES

📚 Browserbase Docs: https://docs.browserbase.com
🤖 Claude API: https://docs.anthropic.com
🎮 Browserbase: https://www.browserbase.com
💡 Try it out: https://www.browserbase.com/playground
🔧 Templates: https://www.browserbase.com/templates
📧 Need help? support@browserbase.com
💬 Discord: http://stagehand.dev/discord
209 changes: 209 additions & 0 deletions typescript/competitor-monitoring/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import "dotenv/config";
import fs from "fs";
import path from "path";
import { URL } from "url";
import Browserbase from "@browserbasehq/sdk";
import { chromium } from "playwright-core";
import Anthropic from "@anthropic-ai/sdk";

const SCREENSHOTS_DIR = "./screenshots";
const COMPARISON_FILE = "./comparison.md";

interface Target {
name: string;
url: string;
}

interface PricingPlan {
name: string;
price: string;
features: string[];
}

interface CompetitorData {
name: string;
plans: PricingPlan[];
}

interface AnalysisResult {
competitors: CompetitorData[];
}

const DEFAULT_COMPETITORS: Target[] = [
{ name: "Asana", url: "https://asana.com/pricing" },
{ name: "Linear", url: "https://linear.app/pricing" },
{ name: "Notion", url: "https://www.notion.com/pricing" },
];

function nameFromUrl(rawUrl: string): string {
const hostname = new URL(rawUrl).hostname.replace(/^www\./, "");
return hostname.split(".")[0] ?? hostname;
}

function resolveTargets(): Target[] {
const args = process.argv.slice(2).filter((a) => a.startsWith("http"));
if (args.length === 0) return DEFAULT_COMPETITORS;
return args.map((u) => ({ name: nameFromUrl(u), url: u }));
}

async function main(): Promise<void> {
console.log("=== Competitor Pricing Monitor ===\n");

const targets = resolveTargets();
const usingDefaults = targets === DEFAULT_COMPETITORS;
console.log(
usingDefaults
? `Using default competitors: ${targets.map((t) => t.name).join(", ")}`
: `Custom URLs provided: ${targets.map((t) => t.url).join(", ")}`,
);
console.log();

fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });

const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });

console.log("Creating Browserbase remote browser session...");
const session = await bb.sessions.create({
projectId: process.env.BROWSERBASE_PROJECT_ID!,
});
console.log(`Session created: ${session.id}\n`);

const connectUrl = `wss://connect.browserbase.com?apiKey=${process.env.BROWSERBASE_API_KEY}&sessionId=${session.id}`;
const browser = await chromium.connectOverCDP(connectUrl);
console.log("Playwright connected to remote browser via CDP.\n");

const screenshots: Array<{ name: string; base64: string }> = [];
const savedFiles: string[] = [];

try {
for (const target of targets) {
console.log(`[${target.name}] Navigating to ${target.url} ...`);
const page = await browser.newPage();

await page.goto(target.url, { waitUntil: "domcontentloaded", timeout: 30_000 });

await page
.waitForSelector('main, [role="main"], #main, .pricing', { timeout: 10_000 })
.catch(() => {
console.log(` [${target.name}] Main selector not found — continuing anyway.`);
});

await page.waitForTimeout(2000);

console.log(` [${target.name}] Page loaded. Taking screenshot...`);
const buffer = await page.screenshot({ type: "png", fullPage: true });
const base64 = buffer.toString("base64");

const filename = `${target.name.toLowerCase()}.png`;
const filepath = path.join(SCREENSHOTS_DIR, filename);
fs.writeFileSync(filepath, buffer);
savedFiles.push(filepath);

screenshots.push({ name: target.name, base64 });
console.log(
` [${target.name}] Screenshot saved → ${filepath} (${Math.round(buffer.length / 1024)} KB)\n`,
);

await page.close();
}
} finally {
console.log("Closing browser session...");
await browser.close();
console.log("Browser session closed.\n");
}

const anthropic = new Anthropic({ apiKey: process.env.MODEL_API_KEY });

const imageBlocks = screenshots.map((s) => ({
type: "image" as const,
source: { type: "base64" as const, media_type: "image/png" as const, data: s.base64 },
}));

const labelBlock = {
type: "text" as const,
text:
`The screenshots below are (in order): ${screenshots.map((s) => s.name).join(", ")}.\n\n` +
"You are a pricing analyst. Here are screenshots of competitor pricing pages. " +
"Extract the plan names, prices, and key features for each competitor. " +
"Return ONLY valid JSON matching this exact structure — no markdown fences, no extra text:\n" +
"{\n" +
' "competitors": [\n' +
" {\n" +
' "name": "CompanyName",\n' +
' "plans": [\n' +
' { "name": "Plan Name", "price": "$X/mo", "features": ["feature1", "feature2"] }\n' +
" ]\n" +
" }\n" +
" ]\n" +
"}",
};

console.log(`Sending ${screenshots.length} screenshot(s) to Claude for analysis...`);

const response = await anthropic.messages.create({
model: "claude-opus-4-7",
max_tokens: 4096,
messages: [{ role: "user", content: [labelBlock, ...imageBlocks] }],
});

const rawText = response.content
.filter((b) => b.type === "text")
.map((b) => (b as { type: "text"; text: string }).text)
.join("\n")
.replace(/^```(?:json)?\s*/i, "")
.replace(/\s*```$/, "")
.trim();

let data: AnalysisResult | null = null;
try {
data = JSON.parse(rawText) as AnalysisResult;
} catch {
console.error("Could not parse Claude response as JSON — printing raw output:\n");
console.log(rawText);
}

console.log("Claude analysis complete.\n");
console.log("=== Pricing Comparison ===");

const mdLines = [`# Competitor Pricing Comparison\n\n_Generated: ${new Date().toISOString()}_\n`];

if (data?.competitors) {
const COL_PLAN = 22;
const COL_PRICE = 16;
const DIVIDER = "─".repeat(COL_PLAN + COL_PRICE + 40);
const pad = (str: unknown, len: number): string => String(str ?? "").padEnd(len);

for (const competitor of data.competitors) {
console.log(`\n${competitor.name.toUpperCase()}`);
console.log(DIVIDER);
console.log(`${pad("PLAN", COL_PLAN)}${pad("PRICE", COL_PRICE)}KEY FEATURES`);
console.log(DIVIDER);

const mdTableRows = ["| Plan | Price | Key Features |", "|------|-------|--------------|"];

for (const plan of competitor.plans ?? []) {
const features = (plan.features ?? []).join(", ");
console.log(`${pad(plan.name, COL_PLAN)}${pad(plan.price, COL_PRICE)}${features}`);
mdTableRows.push(`| ${plan.name} | ${plan.price} | ${features} |`);
}

console.log(DIVIDER);
mdLines.push(`## ${competitor.name}\n\n${mdTableRows.join("\n")}\n`);
}
} else {
mdLines.push(rawText);
}

fs.writeFileSync(COMPARISON_FILE, mdLines.join("\n"));

console.log("\n=== Summary ===");
console.log(`Pages visited: ${targets.length} (${targets.map((t) => t.name).join(", ")})`);
console.log(`Session ID: ${session.id}`);
console.log(`Screenshots: ${savedFiles.join(", ")}`);
console.log(`Comparison saved: ${COMPARISON_FILE}`);
}

main().catch((err: unknown) => {
console.error("Fatal error:", err instanceof Error ? err.message : String(err));
process.exit(1);
});
22 changes: 22 additions & 0 deletions typescript/competitor-monitoring/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "competitor-monitoring",
"version": "1.0.0",
"description": "Playwright + Browserbase + Claude: Competitor Pricing Monitor",
"type": "module",
"main": "index.ts",
"scripts": {
"start": "tsx index.ts",
"dev": "tsx watch index.ts"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.54.0",
"@browserbasehq/sdk": "^2.9.0",
"dotenv": "^16.4.5",
"playwright-core": "^1.49.0"
},
"devDependencies": {
"@types/node": "^20.14.0",
"tsx": "^4.16.0",
"typescript": "^5.5.0"
}
}