DEV Community

Boehner
Boehner

Posted on

How to Track Competitor Prices Weekly With Screenshots and Google Sheets (No Playwright Required)

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:

  1. You forget. Miss one week and now you're comparing prices from two weeks ago.
  2. You lose context. A screenshot tells you what the page looks like — not what changed.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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(),
  };
}
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

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.");
}
Enter fullscreen mode Exit fullscreen mode

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")}`,
    }),
  });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 /pricing directly

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}`);
    }
  }
})();
Enter fullscreen mode Exit fullscreen mode

And competitors.json:

[
  { "name": "AcmeCorp", "url": "https://acme.com/pricing" },
  { "name": "RivalSaaS", "url": "https://rivalsaas.io/plans" }
]
Enter fullscreen mode Exit fullscreen mode

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)