Monitor Competitor Prices with Node.js (2026 Guide)
Keeping track of competitor pricing is a competitive advantage — but manually checking dozens of pages every day is impractical. This guide builds a complete price monitoring system: scrape prices automatically, detect changes, store historical data, and push Slack and email alerts when prices move.
System Architecture
The monitor has four components: a scraper to fetch prices, a SQLite database for price history, a change detector to compare against previous values, and alerting via Slack and email.
npm install axios cheerio better-sqlite3 node-cron nodemailer
Price Scraper
const axios = require('axios');
const cheerio = require('cheerio');
const HEADERS = {
'User-Agent': 'Mozilla/5.0 (compatible; PriceBot/1.0)',
'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
};
/**
* Extract a price from a URL using a CSS selector.
* Returns null if the price element is not found.
*/
async function scrapePrice(url, selector) {
try {
const { data } = await axios.get(url, { headers: HEADERS, timeout: 15000 });
const $ = cheerio.load(data);
const priceText = $(selector).first().text().trim();
if (!priceText) return null;
// Parse price: "$1,299.99" → 1299.99
const parsed = parseFloat(priceText.replace(/[^0-9.]/g, ''));
return isNaN(parsed) ? null : parsed;
} catch (err) {
console.error(`Error scraping ${url}: ${err.message}`);
return null;
}
}
// Define your tracked products
const TRACKED = [
{ id: 'competitor-starter', url: 'https://competitor.com/pricing', selector: '.plan-starter .price', label: 'Competitor Starter Plan' },
{ id: 'competitor-pro', url: 'https://competitor.com/pricing', selector: '.plan-pro .price', label: 'Competitor Pro Plan' },
{ id: 'amz-widget-a', url: 'https://amazon.com/dp/B0XXXXX', selector: '#priceblock_ourprice', label: 'Widget A (Amazon)' },
];
Price History with SQLite
const Database = require('better-sqlite3');
const db = new Database('./prices.db');
db.exec(`
CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id TEXT NOT NULL,
price REAL,
url TEXT,
checked_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_product_checked ON price_history(product_id, checked_at DESC);
`);
const insertPrice = db.prepare(
'INSERT INTO price_history (product_id, price, url) VALUES (?, ?, ?)'
);
function getLatestPrice(productId) {
return db.prepare(
'SELECT price, checked_at FROM price_history WHERE product_id = ? ORDER BY checked_at DESC LIMIT 1'
).get(productId);
}
function getPriceHistory(productId, days = 30) {
return db.prepare(
`SELECT price, checked_at FROM price_history
WHERE product_id = ? AND checked_at >= datetime('now', '-${days} days')
ORDER BY checked_at ASC`
).all(productId);
}
Change Detection and Alerts
const nodemailer = require('nodemailer');
const mailer = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
});
async function sendEmailAlert({ label, oldPrice, newPrice, url, direction }) {
const change = ((newPrice - oldPrice) / oldPrice * 100).toFixed(1);
const emoji = direction === 'down' ? '📉' : '📈';
const subject = `${emoji} Price ${direction}: ${label} (${change > 0 ? '+' : ''}${change}%)`;
await mailer.sendMail({
from: process.env.SMTP_FROM,
to: process.env.ALERT_EMAIL,
subject,
text: [
`Price change detected for: ${label}`,
`URL: ${url}`,
`Old price: $${oldPrice.toFixed(2)}`,
`New price: $${newPrice.toFixed(2)}`,
`Change: ${change > 0 ? '+' : ''}${change}% (${direction})`,
].join('\n')
});
}
async function sendSlackAlert({ label, oldPrice, newPrice, url, direction }) {
const change = ((newPrice - oldPrice) / oldPrice * 100).toFixed(1);
await fetch(process.env.SLACK_WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `*Price ${direction}* for ${label}`,
blocks: [{
type: 'section',
text: { type: 'mrkdwn', text: [
`*${label}*`,
`Old: $${oldPrice.toFixed(2)} → New: $${newPrice.toFixed(2)} (${change > 0 ? '+' : ''}${change}%)`,
`<${url}|View product>`
].join('\n') }
}]
})
});
}
/**
* Check one product: scrape, store, detect change, alert.
*/
async function checkProduct({ id, url, selector, label }) {
const price = await scrapePrice(url, selector);
if (price === null) {
console.warn(`Could not scrape price for ${label}`);
return;
}
const previous = getLatestPrice(id);
insertPrice.run(id, price, url);
if (!previous) {
console.log(`[NEW] ${label}: $${price}`);
return;
}
const priceDiff = price - previous.price;
if (Math.abs(priceDiff) < 0.01) {
console.log(`[SAME] ${label}: $${price}`);
return;
}
const direction = priceDiff < 0 ? 'down' : 'up';
console.log(`[CHANGE] ${label}: $${previous.price} → $${price} (${direction})`);
const alertData = { label, oldPrice: previous.price, newPrice: price, url, direction };
await Promise.all([sendEmailAlert(alertData), sendSlackAlert(alertData)]);
}
Scheduling with node-cron
const cron = require('node-cron');
// Run all checks every hour at :00
cron.schedule('0 * * * *', async () => {
console.log(`[${new Date().toISOString()}] Running price checks…`);
for (const product of TRACKED) {
await checkProduct(product);
await new Promise(r => setTimeout(r, 2000)); // polite delay between sites
}
console.log('Done.');
});
// Also run immediately on start
(async () => {
console.log('Running initial price check…');
for (const product of TRACKED) await checkProduct(product);
})();
console.log('Price monitor running. Press Ctrl+C to stop.');
Monitoring Bot-Protected Pricing Pages with SnapAPI
Many SaaS pricing pages and e-commerce sites serve dynamic content or block scrapers. SnapAPI's /v1/extract with stealth mode handles these cases reliably:
const axios = require('axios');
async function scrapePriceWithAPI(url, productName) {
const { data } = await axios.post('https://api.snapapi.pics/v1/extract', {
url,
schema: {
plans: {
type: 'array',
description: 'All pricing plans on the page',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Plan name (e.g. Starter, Pro)' },
monthlyPrice: { type: 'number', description: 'Monthly price in USD' },
annualPrice: { type: 'number', description: 'Annual/yearly price in USD if shown' },
features: { type: 'array', items: { type: 'string' } }
}
}
}
},
stealth: true
}, { headers: { 'X-Api-Key': process.env.SNAPAPI_KEY } });
return data.data.plans;
}
// Monitor a competitor's full pricing page
async function checkCompetitorPricing(url, competitorName) {
const plans = await scrapePriceWithAPI(url, competitorName);
plans.forEach(plan => {
const key = `${competitorName}-${plan.name.toLowerCase()}`;
const prev = getLatestPrice(key);
insertPrice.run(key, plan.monthlyPrice, url);
if (prev && Math.abs(plan.monthlyPrice - prev.price) > 0.01) {
console.log(`PRICE CHANGE: ${competitorName} ${plan.name}: $${prev.price} → $${plan.monthlyPrice}`);
sendSlackAlert({ label: `${competitorName} ${plan.name}`, oldPrice: prev.price, newPrice: plan.monthlyPrice, url,
direction: plan.monthlyPrice < prev.price ? 'down' : 'up' });
}
});
return plans;
}
Monitor any pricing page — protected or not
SnapAPI's /extract returns structured pricing data as typed JSON, even from bot-protected pages. 200 free requests/month.
Try SnapAPI Free →