Price Monitoring Node.js Automation April 5, 2026

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.');
Frequency tip: Checking pricing pages once per hour is reasonable for SaaS tools. For flash-sale e-commerce, check every 5–15 minutes. Avoid checking more than once per minute — you'll get rate-limited or blocked.

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 →