Manually checking competitor prices is a losing game. By the time you open 20 tabs, copy-paste numbers into a spreadsheet, and spot a change, your competitor already changed prices again. The only way to win is automation.
In this guide, I'll show you how to build a price monitoring system that scrapes competitor product pages, detects price changes, and alerts you — all running on a schedule with zero manual effort.
A Node.js script that:
Start with a focused list. 5-10 competitor products is enough to prove value before scaling:
// targets.json
[
{ "id": "comp-a-widget", "name": "Widget Pro", "competitor": "CompA", "url": "https://competitor-a.com/products/widget-pro" },
{ "id": "comp-b-widget", "name": "Widget Pro", "competitor": "CompB", "url": "https://competitor-b.com/products/widget-pro" },
{ "id": "comp-a-gadget", "name": "Super Gadget", "competitor": "CompA", "url": "https://competitor-a.com/products/super-gadget" }
]
Instead of building and maintaining a headless browser infrastructure, use ExtractAPI. One POST request per URL — it handles browser rendering, JavaScript execution, and returns structured JSON:
const EXTRACT_API_KEY = process.env.EXTRACT_API_KEY;
async function extractPrice(target) {
const res = await fetch('https://extractapi.app/v1/extract', {
method: 'POST',
headers: {
'Authorization': `Bearer ${EXTRACT_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: target.url,
selector: '.product-price, .price, [data-price]',
waitFor: '.product-price, .price',
javascript: true
})
});
const data = await res.json();
if (data.status !== 'ok') throw new Error(`Extraction failed: ${data.error}`);
// Parse price from text (handles "$49.99", "49,99 €", "$ 49.99", etc.)
const priceText = data.data.selectorText || data.data.h1 || '';
const priceMatch = priceText.match(/[\d,.]+/);
return {
id: target.id,
name: target.name,
competitor: target.competitor,
price: priceMatch ? parseFloat(priceMatch[0].replace(',', '')) : null,
currency: priceText.includes('$') ? 'USD' : priceText.includes('€') ? 'EUR' : 'unknown',
title: data.data.title,
extractedAt: new Date().toISOString()
};
}
Store previous prices in a simple JSON file and compare:
const fs = require('fs/promises');
async function detectChanges(currentPrices) {
let previous = {};
try {
previous = JSON.parse(await fs.readFile('price-history.json', 'utf-8'));
} catch { /* first run, no history */ }
const changes = [];
for (const item of currentPrices) {
const prev = previous[item.id];
if (!prev) {
changes.push({ ...item, change: 'new', previousPrice: null });
} else if (prev.price !== item.price) {
const direction = item.price < prev.price ? 'decrease' : 'increase';
const pctChange = ((item.price - prev.price) / prev.price * 100).toFixed(1);
changes.push({ ...item, change: direction, previousPrice: prev.price, pctChange });
}
}
// Save new prices
const newHistory = {};
for (const item of currentPrices) newHistory[item.id] = item;
await fs.writeFile('price-history.json', JSON.stringify(newHistory, null, 2));
return changes;
}
When prices change, you want to know immediately. Email, Slack, or Telegram are all good options:
async function sendAlert(changes) {
if (changes.length === 0) {
console.log('No price changes detected.');
return;
}
const message = changes.map(c =>
`${c.change === 'decrease' ? '🔻' : '🔺'} ${c.competitor} — ${c.name}\n` +
` ${c.change === 'decrease' ? 'Dropped' : 'Increased'} from $${c.previousPrice} to $${c.price} (${c.pctChange}%)\n` +
` URL: ${c.url}`
).join('\n\n');
// Send to Slack webhook (or Telegram, email, Discord — your choice)
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: `📊 Price Changes Detected\n\n${message}` })
});
console.log(`Alert sent: ${changes.length} price changes`);
}
Wire it up to run on a schedule:
// monitor.js — the full script combined
const TARGETS = require('./targets.json');
async function main() {
console.log(`Starting price check: ${new Date().toISOString()}`);
const currentPrices = [];
for (const target of TARGETS) {
try {
const result = await extractPrice(target);
currentPrices.push(result);
console.log(` ${target.competitor} — ${target.name}: $${result.price}`);
} catch (err) {
console.error(` Failed: ${target.competitor} — ${target.name}: ${err.message}`);
}
// Rate limit: 1 request/second to stay within free tier limits
await new Promise(r => setTimeout(r, 1000));
}
const changes = await detectChanges(currentPrices);
await sendAlert(changes);
}
main().catch(console.error);
Add to crontab to run twice daily:
# crontab -e
0 8,20 * * * cd /app && node monitor.js >> /var/log/price-monitor.log 2>&1
| Approach | Build Time | Maintenance | Reliability |
|---|---|---|---|
| DIY headless browser | Weeks | High (browser updates, anti-bot changes) | Medium |
| Scraping API (ExtractAPI) | Hours | Low (API handles rendering) | High |
| Manual checking | 0 | Extreme (hours/week) | Low (missed changes) |
Real-world product pages throw curveballs. Here's how to handle the common ones:
.sale-price and .regular-price selectors. Compare against the regular price separately to track discounts.null with availability: "out_of_stock" rather than treating it as a price change.