How to Build a Price Tracker with JavaScript and APIs
Price tracking is essential for e-commerce monitoring, deal hunting, and competitive analysis. This tutorial shows you how to build a price tracker using JavaScript (Node.js) and SearchHive's ScrapeForge API -- with working code that fetches prices, stores history, and alerts you to drops.
Key Takeaways
- A price tracker needs three components: data collection (scraping), storage (database), and alerting (notifications)
- SearchHive's ScrapeForge API renders JavaScript-heavy product pages and returns clean data
- Node.js with cron expression generator scheduling runs the tracker automatically at regular intervals
- SQLite stores price history locally without requiring a database server
- The complete tracker works with any e-commerce site and sends alerts via console or webhooks
Prerequisites
- Node.js 18 or later
- SearchHive API key (free tier -- 500 credits)
- npm init -y && npm install node-fetch better-sqlite3 node-cron
mkdir price-tracker && cd price-tracker
npm init -y
npm install node-fetch better-sqlite3 node-cron
Step 1: Set Up the Project Structure
Create a clean project structure:
price-tracker/
index.js # Main entry point
scraper.js # Price fetching logic
database.js # SQLite storage
tracker.js # Scheduling and alerts
config.js # API keys and settings
package.json
Step 2: Configure API Keys
// config.js
module.exports = {
SEARCHHIVE_API_KEY: process.env.SEARCHHIVE_API_KEY || "YOUR_API_KEY",
DB_PATH: "./prices.db",
CHECK_INTERVAL: "0 */6 * * *", // Every 6 hours (cron syntax)
ALERT_THRESHOLD: 0.05, // Alert on 5%+ price drop
WEBHOOK_URL: process.env.WEBHOOK_URL || null // Optional Slack/Discord webhook
};
Step 3: Build the Price Scraper
SearchHive's ScrapeForge API fetches product pages with JavaScript rendering enabled. This handles product pages that load prices dynamically via AJAX or React/Vue:
// scraper.js
const API_KEY = require("./config").SEARCHHIVE_API_KEY;
async function fetchPageContent(url) {
const response = await fetch("https://api.searchhive.dev/v1/scrape", {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
url: url,
format: "markdown",
render_js: true
})
});
if (!response.ok) {
throw new Error(`ScrapeForge error: ${response.status}`);
}
const data = await response.json();
return data.content.markdown || data.content || "";
}
async function extractPrice(content) {
// Extract price patterns from scraped content
const pricePatterns = [
/\$(\d{1,3}(?:,\d{3})*(?:\.\d{2}))/g,
/(\d{1,3}(?:,\d{3})*(?:\.\d{2}))/g,
/price[:\s]*\$?(\d{1,3}(?:,\d{3})*(?:\.\d{2}))/gi
];
let prices = [];
for (const pattern of pricePatterns) {
const matches = content.match(pattern);
if (matches) {
prices = matches.map(p => {
const cleaned = p.replace(/[$,]/g, "");
const num = parseFloat(cleaned);
return num;
}).filter(p => p > 0 && p < 100000);
}
if (prices.length > 0) break;
}
if (prices.length === 0) return null;
// Return the lowest reasonable price (likely the product price)
return Math.min(...prices);
}
async function scrapePrice(url) {
const content = await fetchPageContent(url);
const price = await extractPrice(content);
return {
url,
price,
scrapedAt: new Date().toISOString(),
contentLength: content.length
};
}
module.exports = { scrapePrice, fetchPageContent, extractPrice };
The render_js: true option is critical -- most modern e-commerce sites load prices via JavaScript after the initial page load. Without headless rendering, you get empty price containers.
Step 4: Set Up SQLite Storage
Store price history in SQLite for trend analysis and drop detection:
// database.js
const Database = require("better-sqlite3");
const path = require("path");
const dbPath = require("./config").DB_PATH;
const db = new Database(path.resolve(dbPath));
// Create tables
db.exec(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT UNIQUE NOT NULL,
name TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id INTEGER NOT NULL,
price REAL NOT NULL,
scraped_at TEXT NOT NULL,
FOREIGN KEY (product_id) REFERENCES products(id)
);
CREATE INDEX IF NOT EXISTS idx_price_product ON price_history(product_id);
CREATE INDEX IF NOT EXISTS idx_price_date ON price_history(scraped_at);
`);
function getProduct(url) {
return db.prepare("SELECT * FROM products WHERE url = ?").get(url);
}
function addProduct(url, name) {
return db.prepare("INSERT OR IGNORE INTO products (url, name) VALUES (?, ?)").run(url, name);
}
function recordPrice(productId, price, scrapedAt) {
return db.prepare("INSERT INTO price_history (product_id, price, scraped_at) VALUES (?, ?, ?)").run(productId, price, scrapedAt);
}
function getPriceHistory(productId, limit = 30) {
return db.prepare("SELECT * FROM price_history WHERE product_id = ? ORDER BY scraped_at DESC LIMIT ?").all(productId, limit);
}
function getLastPrice(productId) {
return db.prepare("SELECT price FROM price_history WHERE product_id = ? ORDER BY scraped_at DESC LIMIT 1").get(productId);
}
function getAllProducts() {
return db.prepare("SELECT * FROM products").all();
}
module.exports = { getProduct, addProduct, recordPrice, getPriceHistory, getLastPrice, getAllProducts };
Step 5: Build the Tracker with Alerts
Combine scraping, storage, and alerting into a scheduled tracker:
// tracker.js
const cron = require("node-cron");
const { scrapePrice } = require("./scraper");
const db = require("./database");
const config = require("./config");
const trackedProducts = [
{ url: "https://www.example.com/product-1", name: "Example Product 1" },
{ url: "https://www.example.com/product-2", name: "Example Product 2" },
];
async function checkProduct(product) {
try {
// Ensure product is in database
db.addProduct(product.url, product.name);
const record = db.getProduct(product.url);
const lastPrice = db.getLastPrice(record.id);
// Scrape current price
const result = await scrapePrice(product.url);
if (result.price === null) {
console.log(`[${product.name}] Could not extract price`);
return;
}
// Save to history
db.recordPrice(record.id, result.price, result.scrapedAt);
// Check for price drop
if (lastPrice) {
const change = (result.price - lastPrice.price) / lastPrice.price;
if (change <= -config.ALERT_THRESHOLD) {
const drop = Math.abs(change * 100).toFixed(1);
const message = `PRICE DROP: ${product.name} is now $${result.price.toFixed(2)} (was $${lastPrice.price.toFixed(2)}, -${drop}%)`;
console.log(`ALERT: ${message}`);
sendAlert(message);
} else {
const direction = change >= 0 ? "+" : "";
console.log(`[${product.name}] $${result.price.toFixed(2)} (${direction}${(change * 100).toFixed(1)}%)`);
}
} else {
console.log(`[${product.name}] First price recorded: $${result.price.toFixed(2)}`);
}
} catch (error) {
console.error(`[${product.name}] Error: ${error.message}`);
}
}
async function sendAlert(message) {
if (config.WEBHOOK_URL) {
try {
await fetch(config.WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: message })
});
} catch (e) {
console.error("Webhook failed:", e.message);
}
}
}
async function checkAll() {
console.log(`
Price check at ${new Date().toISOString()}`);
for (const product of trackedProducts) {
await checkProduct(product);
}
}
function startScheduler() {
// Check immediately on start
checkAll();
// Schedule recurring checks
cron.schedule(config.CHECK_INTERVAL, checkAll);
console.log(`Scheduler started. Checking every: ${config.CHECK_INTERVAL}`);
}
module.exports = { checkAll, startScheduler, trackedProducts };
Step 6: Run the Tracker
// index.js
const { startScheduler } = require("./tracker");
console.log("Price Tracker starting...");
startScheduler();
Run it:
SEARCHHIVE_API_KEY=your_key node index.js
Output:
Price Tracker starting...
Price check at 2024-01-15T10:00:00.000Z
[Example Product 1] First price recorded: $299.99
[Example Product 2] First price recorded: $149.99
Scheduler started. Checking every: 0 */6 * * *
After the first run establishes a baseline, subsequent runs will compare prices and alert on drops exceeding the threshold.
Step 7: View Price History
Query the database to see price trends:
// Run in Node.js REPL or a separate script
const db = require("./database");
const products = db.getAllProducts();
for (const product of products) {
const history = db.getPriceHistory(product.id, 10);
console.log(`
${product.name}:`);
for (const entry of history) {
console.log(` ${entry.scraped_at}: $${entry.price.toFixed(2)}`);
}
}
Handling Common Issues
Price extraction fails. Some sites use non-standard price formats, images for prices, or obfuscation techniques. If the regex tester patterns in extractPrice() don't match, add site-specific patterns. For AI-powered extraction, use SearchHive's DeepDive API:
async function extractPriceAI(content) {
const response = await fetch("https://api.searchhive.dev/v1/deepdive", {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
content: content,
schema: {
type: "object",
properties: {
price: { type: "string", description: "Current product price" },
original_price: { type: "string", description: "Original price before discount" },
currency: { type: "string" }
}
}
})
});
const data = await response.json();
return data.data;
}
CAPTCHAs and bot detection. E-commerce sites are aggressive about blocking scrapers. SearchHive's proxy rotation handles most cases. If you still get blocked, reduce your check frequency or use longer intervals between requests for multiple products.
Dynamic URLs. Some product pages change URLs based on variants (color, size). Track the base product URL and let ScrapeForge handle redirects.
Stale data. If a scrape returns cached data, add cache-busting parameters or ensure ScrapeForge makes a fresh request each time.
Complete Project Files
The full project is 4 files totaling under 200 lines. Here's a summary of the architecture:
- config.js -- API key, database path, schedule, alert threshold
- scraper.js -- Fetches pages via ScrapeForge, extracts prices with regex
- database.js -- SQLite schema, CRUD operations for products and price history
- tracker.js -- Scheduling with node-cron, price comparison, alert dispatch
- index.js -- Entry point, starts the scheduler
Next Steps
- Add more products: Extend the
trackedProductsarray or load from a config file - Email alerts: Integrate Nodemailer for email notifications on price drops
- Dashboard: Build a simple web UI with Express to display price charts
- Multi-region tracking: Track the same product across different Amazon/e-commerce domains
- Historical analysis: Calculate average price, standard deviation, and best-ever price
Get started with SearchHive's free tier -- 500 credits, no credit card required. Check the API docs for the full reference.
See also: /blog/how-to-monitor-competitor-prices-with-python-automated-system and /blog/how-to-scrape-e-commerce-pricing-data-with-python.