How to Track Competitor Prices Weekly With Screenshots and Google Sheets (No Playwright Required)
I used to do this manually. Every Monday morning I'd open 15 browser tabs, screenshot competitor product pages, drop them into a shared Google Drive folder, and paste prices into a spreadsheet.
It took about 40 minutes. And I did it every single week for four months before I snapped and automated it.
Here's the full setup I built — no Playwright, no Puppeteer, no headless browser maintenance. Just a Node.js script + SnapAPI + a Google Sheet that turns red when prices change.
The Problem With Manual Competitor Tracking
Manual screenshot workflows break in three ways:
- You forget. Miss one week and now you're comparing prices from two weeks ago.
- You lose context. A screenshot tells you what the page looks like — not what changed.
- It doesn't scale. 15 competitors is manageable. 50 is not.
The solution most developers reach for first is Puppeteer. I did too. The problem: Puppeteer requires a working headless Chrome environment, breaks on every OS update, and chokes on sites with bot detection. You end up maintaining your scraper instead of your business.
The alternative I use now is a screenshot API — specifically SnapAPI. One HTTP call, get back a PNG. No Chrome to manage, no timeouts to debug.
The Architecture
Competitor URLs (Google Sheet)
→ Node.js cron script
→ SnapAPI batch screenshots
→ Google Drive storage
→ Price comparison formula in Google Sheets
→ Slack/email alert when delta > 0
Total moving parts: one script (~60 lines), one Sheet, one cron job.
Step 1: Set Up Your Competitor URL List
Create a Google Sheet with three columns:
| Column A | Column B | Column C |
|---|---|---|
| Competitor | URL | Last Price |
| Acme Corp | https://acme.com/pricing | $49 |
| RivalSaaS | https://rivalsaas.io/plans | $79 |
You'll populate "Last Price" manually the first time. After that, the script handles it.
Step 2: Write the Screenshot Script
const https = require("https");
const fs = require("fs");
const path = require("path");
const SNAPAPI_KEY = process.env.SNAPAPI_KEY;
const BASE_URL = "https://snapapi.tech";
const competitors = [
{ name: "AcmeCorp", url: "https://acme.com/pricing" },
{ name: "RivalSaaS", url: "https://rivalsaas.io/plans" },
{ name: "CompetitorX", url: "https://competitorx.com/buy" },
// add as many as you need
];
async function screenshotPage(competitor) {
const params = new URLSearchParams({
url: competitor.url,
format: "png",
fullPage: "false",
width: "1280",
height: "900",
});
const response = await fetch(
`${BASE_URL}/screenshot?${params}`,
{
headers: { "x-api-key": SNAPAPI_KEY },
}
);
if (!response.ok) {
throw new Error(`Screenshot failed for ${competitor.name}: ${response.status}`);
}
const buffer = await response.arrayBuffer();
const dateStamp = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
const filename = `${competitor.name}-${dateStamp}.png`;
const outputPath = path.join("screenshots", filename);
fs.mkdirSync("screenshots", { recursive: true });
fs.writeFileSync(outputPath, Buffer.from(buffer));
console.log(`✓ Saved ${filename}`);
return outputPath;
}
async function runWeeklyCapture() {
console.log(`\n📸 Weekly capture — ${new Date().toDateString()}\n`);
const results = [];
for (const competitor of competitors) {
try {
const filepath = await screenshotPage(competitor);
results.push({ name: competitor.name, file: filepath, status: "ok" });
} catch (err) {
results.push({ name: competitor.name, status: "error", error: err.message });
console.error(`✗ ${competitor.name}: ${err.message}`);
}
}
// Write manifest for this week
const manifest = {
capturedAt: new Date().toISOString(),
results,
};
fs.writeFileSync("screenshots/manifest.json", JSON.stringify(manifest, null, 2));
console.log(`\nDone. ${results.filter((r) => r.status === "ok").length}/${competitors.length} captured.`);
}
runWeeklyCapture();
Run it: SNAPAPI_KEY=your_key node capture.js
You'll get a dated screenshot for every competitor, plus a manifest.json log.
Step 3: Extract Price Data With the Analyze Endpoint
Screenshots tell you visually what changed. But to feed numbers into a spreadsheet, you need the actual text. SnapAPI's analyze endpoint extracts structured data from any page — headings, body text, CTAs, and more.
async function extractPriceData(competitor) {
const params = new URLSearchParams({ url: competitor.url });
const response = await fetch(
`${BASE_URL}/analyze?${params}`,
{
headers: { "x-api-key": SNAPAPI_KEY },
}
);
const data = await response.json();
// Price mentions often appear in headings, CTAs, or body text
// The analyzer returns all of these as structured fields
const priceSignals = [
...(data.headings || []),
...(data.buttons || []),
]
.join(" ")
.match(/\$[\d,]+(?:\.\d{2})?(?:\/mo(?:nth)?|\/yr(?:ear)?)?/gi) || [];
return {
name: competitor.name,
url: competitor.url,
extractedPrices: priceSignals,
capturedAt: new Date().toISOString(),
};
}
This gives you an array of price strings like ["$49/mo", "$490/yr"] — much easier to diff than raw HTML.
Step 4: Build the Google Sheets Price Diff
In your Google Sheet, add a column D called "This Week" and column E called "Changed?".
In E2, paste this formula:
=IF(D2="","",IF(D2<>C2,"⚠️ CHANGED: was "&C2&", now "&D2,"✓ Same"))
Copy it down for every row.
Now your weekly script just needs to write to column D. You can do this with the Google Sheets API, or — simpler — just export a CSV and import it manually. Either works.
For full automation, here's how to push to Sheets via the API:
const { google } = require("googleapis");
async function updateSheet(priceData) {
const auth = new google.auth.GoogleAuth({
keyFile: "service-account.json",
scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});
const sheets = google.sheets({ version: "v4", auth });
// Build rows: [competitor name, current price]
const values = priceData.map((d) => [
d.name,
d.extractedPrices[0] || "not found",
]);
await sheets.spreadsheets.values.update({
spreadsheetId: process.env.SHEET_ID,
range: "Sheet1!A2:D" + (priceData.length + 1),
valueInputOption: "RAW",
requestBody: { values },
});
console.log("Sheet updated.");
}
Step 5: Add Slack Alerts for Price Changes
Once you have last week's prices and this week's prices in the sheet, you want to know immediately when something changes — not Monday when you remember to check.
async function sendSlackAlert(changes) {
if (changes.length === 0) return;
const lines = changes.map(
(c) => `*${c.name}*: was ${c.oldPrice} → now ${c.newPrice}`
);
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `🚨 *Competitor price change detected*\n\n${lines.join("\n")}`,
}),
});
}
Call this after comparing last week vs. this week's data.
Step 6: Schedule It With a Cron Job
On Linux/Mac:
# Edit crontab
crontab -e
# Add this line — runs every Monday at 8am
0 8 * * 1 cd /path/to/your/script && SNAPAPI_KEY=xxx node capture.js >> capture.log 2>&1
On Render.com or Railway, you can add a cron service that triggers your script weekly. Total infrastructure cost: whatever you're paying for compute (often free tier works fine for a 60-second weekly script).
Scaling Up: Batch Mode for 50+ Competitors
When you're tracking more than 10 URLs, run them in parallel. SnapAPI supports batch requests:
async function batchCapture(competitors) {
const batchSize = 10;
const results = [];
for (let i = 0; i < competitors.length; i += batchSize) {
const batch = competitors.slice(i, i + batchSize);
const promises = batch.map((c) => screenshotPage(c).catch((err) => ({
name: c.name,
status: "error",
error: err.message,
})));
const batchResults = await Promise.all(promises);
results.push(...batchResults);
// Small delay between batches — good API citizenship
if (i + batchSize < competitors.length) {
await new Promise((r) => setTimeout(r, 1000));
}
}
return results;
}
100 competitors captured in about 2 minutes. One cron job. Zero maintenance.
What This Setup Actually Tells You
After running this for a few weeks, you start noticing patterns:
- Competitors who raise prices in Q1 (preparing for annual contract renewals)
- A/B tests on pricing pages — different prices on different captures
- Competitive responses to your own price changes
- New pricing tiers that don't exist yet on their marketing site but appear when you hit
/pricingdirectly
The screenshots are the receipt. The price diff is the signal. The Slack alert means you react in hours, not weeks.
Full Script: capture.js (60 lines)
const fs = require("fs");
const path = require("path");
const SNAPAPI_KEY = process.env.SNAPAPI_KEY;
const BASE_URL = "https://snapapi.tech";
const competitors = JSON.parse(
fs.readFileSync("competitors.json", "utf8")
);
async function screenshotPage({ name, url }) {
const params = new URLSearchParams({ url, format: "png", width: "1280", height: "900" });
const res = await fetch(`${BASE_URL}/screenshot?${params}`, {
headers: { "x-api-key": SNAPAPI_KEY },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const buf = await res.arrayBuffer();
const date = new Date().toISOString().split("T")[0];
const file = path.join("screenshots", `${name}-${date}.png`);
fs.mkdirSync("screenshots", { recursive: true });
fs.writeFileSync(file, Buffer.from(buf));
return file;
}
async function analyzePage({ name, url }) {
const params = new URLSearchParams({ url });
const res = await fetch(`${BASE_URL}/analyze?${params}`, {
headers: { "x-api-key": SNAPAPI_KEY },
});
const data = await res.json();
const allText = [...(data.headings || []), ...(data.buttons || [])].join(" ");
const prices = allText.match(/\$[\d,]+(?:\.\d{2})?(?:\/mo(?:nth)?)?/gi) || [];
return { name, url, prices };
}
(async () => {
console.log(`Weekly capture — ${new Date().toDateString()}`);
for (const c of competitors) {
try {
const [file, priceData] = await Promise.all([
screenshotPage(c),
analyzePage(c),
]);
console.log(`✓ ${c.name}: ${priceData.prices.join(", ") || "no prices found"} → ${file}`);
} catch (err) {
console.error(`✗ ${c.name}: ${err.message}`);
}
}
})();
And competitors.json:
[
{ "name": "AcmeCorp", "url": "https://acme.com/pricing" },
{ "name": "RivalSaaS", "url": "https://rivalsaas.io/plans" }
]
One More Thing: BusinessPulse Does This + AI Summaries
If you want the screenshots + price diffs + a plain-English brief written by Claude — all in your inbox every Monday — that's what BusinessPulse does. You drop in your competitor URLs, it runs this whole pipeline weekly, and Claude writes you a summary like: "Competitor X raised prices 20% this week and added an enterprise tier."
But even if you never touch BusinessPulse, the script above is a full working solution. Copy it, set your SNAPAPI_KEY, point it at your competitors, and your Monday mornings get 40 minutes back.
SnapAPI handles the screenshots and page analysis. Docs here — the screenshot and analyze endpoints are what power everything in this tutorial.
Top comments (0)