Build a Price Monitoring Dashboard for E-Commerce Products
Build an automated price monitoring dashboard with the Price Tracker API. Python pipeline with scheduled runs, historical storage, and change detection.

Thirdwatch's Price Tracker provides the data layer for building a production price-monitoring dashboard. Extract prices from Amazon, Shopify, and any JSON-LD/OpenGraph site via a single API call, store snapshots in a database, compute change deltas, and surface trends in a lightweight dashboard. Built for developers building internal pricing tools, e-commerce engineers maintaining competitive intelligence feeds, and data teams who need a reliable extraction layer without maintaining per-site parsers.
Why build a price monitoring dashboard
Manual price checking does not scale past 20 products. According to a 2025 McKinsey report on retail pricing, companies with automated pricing intelligence respond to competitor moves 3.2x faster than those relying on manual spot-checks. According to Profitero's 2025 Dynamic Pricing Index, the average e-commerce product experiences 2.3 price changes per week, with electronics and supplements seeing daily fluctuations that manual workflows miss entirely.
The architecture is simple: a scheduled data-collection layer (the actor) feeds a time-series store (database), which powers a query layer (dashboard or BI tool). Most teams overcomplicate step one by building custom scrapers for each retailer. The Price Tracker normalizes extraction across Amazon, Shopify, and any site with structured product data into a single schema -- productName, price, currency, availability, brand, rating, platform -- which means your database schema and dashboard queries work identically regardless of source.
This guide walks through the full pipeline: scheduled actor runs, SQLite storage with price history, change detection queries, and a minimal Streamlit dashboard.
How does this compare to the alternatives?
Three paths to a price-monitoring dashboard:
| Approach | Data collection | Storage + dashboard | Setup time | Ongoing cost model |
|---|---|---|---|---|
| SaaS platform (Prisync, Price2Spy) | Vendor-managed | Vendor dashboard | Hours | Monthly subscription, per-SKU pricing |
| DIY scraping + custom dashboard | You build per-site scrapers | You build storage + UI | Weeks | Server + maintenance time |
| Thirdwatch Price Tracker + your dashboard | Pay-per-result API | You own storage + UI | 1-2 hours | Pay per extraction, own your infra |
The SaaS path is fastest if your team has budget but no engineering bandwidth. The DIY path gives full control but requires maintaining parsers that break on site changes. The Price Tracker path splits the difference -- reliable extraction via API, full ownership of storage and presentation.
How to build a price monitoring dashboard in 6 steps
Step 1: How do I set up the project?
Initialize a Python project with the required dependencies.
mkdir price-dashboard && cd price-dashboard
python -m venv venv && source venv/bin/activate
pip install apify-client pandas streamlit
export APIFY_TOKEN="apify_api_xxxxxxxxxxxxxxxx"Step 2: How do I define the product watchlist?
Store your product URLs in a configuration file. The actor accepts URLs from any supported site in a single run.
# config.py
WATCHLIST = [
"https://www.amazon.com/dp/B09V3KXJPB",
"https://www.amazon.com/dp/B0BSHF7WHW",
"https://www.amazon.com/dp/B0CHX3PNKZ",
"https://gymshark.com/products/vital-seamless-leggings",
"https://www.allbirds.com/products/mens-tree-runners",
"https://ridgewallet.com/products/titanium",
"https://www.on-running.com/en-us/products/cloudmonster",
"https://www.stanley1913.com/products/quencher-h2-0-flowstate-tumbler-40-oz",
]
ACTOR_ID = "thirdwatch/price-tracker"
MAX_RETRIES = 3
PROXY_COUNTRY = "US"
DB_PATH = "prices.db"Step 3: How do I collect prices and store them in SQLite?
Run the actor and append results to a time-series table.
# collect.py
import os, sqlite3, datetime, json
from apify_client import ApifyClient
from config import WATCHLIST, ACTOR_ID, MAX_RETRIES, PROXY_COUNTRY, DB_PATH
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor(ACTOR_ID).call(run_input={
"urls": WATCHLIST,
"maxRetries": MAX_RETRIES,
"proxyCountry": PROXY_COUNTRY,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
ts = datetime.datetime.utcnow().isoformat()
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
url TEXT NOT NULL,
product_name TEXT,
price REAL,
currency TEXT,
availability TEXT,
brand TEXT,
rating REAL,
platform TEXT,
success INTEGER
)
""")
for item in items:
conn.execute(
"INSERT INTO price_history (timestamp, url, product_name, price, currency, "
"availability, brand, rating, platform, success) VALUES (?,?,?,?,?,?,?,?,?,?)",
(ts, item["url"], item.get("productName"), item.get("price"),
item.get("currency"), item.get("availability"), item.get("brand"),
item.get("rating"), item.get("platform"), int(item.get("success", False))),
)
conn.commit()
conn.close()
print(f"Stored {len(items)} records at {ts}")Schedule this script via cron (0 8 * * * for daily at 8 AM UTC) or use Apify's scheduler to trigger the actor directly with a webhook that calls your storage endpoint.
Step 4: How do I compute price changes and trends?
Query the database for day-over-day deltas and rolling statistics.
# analysis.py
import sqlite3, pandas as pd
from config import DB_PATH
conn = sqlite3.connect(DB_PATH)
df = pd.read_sql("""
SELECT url, product_name, price, currency, availability, timestamp,
platform, brand
FROM price_history
WHERE success = 1
ORDER BY url, timestamp
""", conn)
df["timestamp"] = pd.to_datetime(df["timestamp"])
df["date"] = df["timestamp"].dt.date
# Day-over-day change per product
latest = df.groupby("url").last().reset_index()
previous = df.groupby("url").nth(-2).reset_index()
merged = latest.merge(previous, on="url", suffixes=("_now", "_prev"))
merged["price_delta"] = merged["price_now"] - merged["price_prev"]
merged["pct_change"] = (merged["price_delta"] / merged["price_prev"] * 100).round(1)
movers = merged[merged["pct_change"].abs() >= 2.0].sort_values("pct_change")
print("Price movers (2%+ change):")
print(movers[["product_name_now", "price_prev", "price_now", "pct_change"]])
# 7-day rolling stats
weekly = df.groupby("url").apply(
lambda g: g.set_index("timestamp")["price"].resample("D").last().rolling(7).agg(
["mean", "min", "max", "std"]
)
).reset_index()
print("\n7-day rolling stats (last row per product):")
print(weekly.groupby("url").last())Step 5: How do I build a minimal Streamlit dashboard?
Create a single-page dashboard that surfaces price history charts and current status.
# dashboard.py
import sqlite3, pandas as pd, streamlit as st
from config import DB_PATH
st.set_page_config(page_title="Price Monitor", layout="wide")
st.title("Price Monitoring Dashboard")
conn = sqlite3.connect(DB_PATH)
df = pd.read_sql("""
SELECT url, product_name, price, currency, availability,
timestamp, platform, brand
FROM price_history WHERE success = 1
ORDER BY timestamp
""", conn)
df["timestamp"] = pd.to_datetime(df["timestamp"])
# Current prices table
latest = df.groupby("url").last().reset_index()
st.subheader("Current Prices")
st.dataframe(
latest[["product_name", "brand", "price", "currency",
"availability", "platform", "timestamp"]],
use_container_width=True,
)
# Price history chart
st.subheader("Price History")
selected = st.selectbox("Select product", latest["product_name"].tolist())
product_url = latest[latest["product_name"] == selected]["url"].iloc[0]
history = df[df["url"] == product_url][["timestamp", "price"]].set_index("timestamp")
st.line_chart(history)
# Availability summary
st.subheader("Availability Status")
avail = latest["availability"].value_counts()
st.bar_chart(avail)Run with streamlit run dashboard.py. The dashboard auto-refreshes when the database updates.
Step 6: How do I automate the full pipeline?
Wire collection, analysis, and alerting into a single scheduled workflow.
# pipeline.py
import subprocess, sys
# Step 1: Collect fresh prices
subprocess.run([sys.executable, "collect.py"], check=True)
# Step 2: Detect changes and alert
import sqlite3, pandas as pd, requests
from config import DB_PATH
conn = sqlite3.connect(DB_PATH)
df = pd.read_sql("""
SELECT url, product_name, price, currency, availability, timestamp
FROM price_history WHERE success = 1
ORDER BY url, timestamp
""", conn)
latest = df.groupby("url").last().reset_index()
previous = df.groupby("url").nth(-2).reset_index()
if len(previous) > 0:
merged = latest.merge(previous, on="url", suffixes=("_now", "_prev"))
merged["pct_change"] = (
(merged["price_now"] - merged["price_prev"]) / merged["price_prev"] * 100
).round(1)
alerts = merged[merged["pct_change"].abs() >= 5.0]
for _, row in alerts.iterrows():
direction = "dropped" if row["pct_change"] < 0 else "increased"
msg = (f"*{row['product_name_now']}* {direction} {abs(row['pct_change'])}% "
f"({row['currency_prev']} {row['price_prev']} -> {row['price_now']})")
requests.post("https://hooks.slack.com/services/.../...",
json={"text": msg}, timeout=10)Schedule pipeline.py via cron at your preferred cadence. The dashboard reads from the same SQLite file and always shows the latest state.
Sample output
A single price check returns records like these. Three records weigh approximately 1 KB.
[
{
"url": "https://www.amazon.com/dp/B0CHX3PNKZ",
"productName": "Sony WH-1000XM5 Wireless Headphones",
"price": 278.00,
"currency": "USD",
"availability": "In Stock",
"brand": "Sony",
"rating": 4.6,
"platform": "amazon",
"success": true
},
{
"url": "https://ridgewallet.com/products/titanium",
"productName": "The Titanium Ridge Wallet",
"price": 145.00,
"currency": "USD",
"availability": "In Stock",
"brand": "Ridge",
"rating": 4.8,
"platform": "shopify",
"success": true
}
]The platform field lets your dashboard group and filter by retailer type. The success boolean ensures your analysis queries only process clean extractions, preventing null prices from corrupting trend calculations.
Common pitfalls
Four things go wrong when building price-monitoring dashboards. Schema drift in the watchlist — product URLs get delisted, redirected, or restructured over time. Build a weekly audit that flags URLs returning success: false for 3+ consecutive runs and surfaces them for replacement. Without this, your dashboard silently loses coverage. Timezone-naive timestamps — if your collection script runs at 8 AM UTC but your dashboard renders in local time, day-over-day comparisons misalign at month boundaries. Store all timestamps in UTC and convert at the display layer only. Missing baseline for new products — products added mid-stream have no historical data, which breaks percentage-change calculations (division by zero or misleading first-day deltas). Initialize new products with a one-day grace period before including them in change-detection alerts.
A fourth issue specific to multi-currency dashboards: exchange rate fluctuations masquerade as price changes. A product priced at 50 GBP looks 2% cheaper on Tuesday because the pound weakened, not because the retailer changed the price. If your watchlist spans currencies, either track prices in their native currency and convert only at display time, or pin a fixed exchange rate per analysis window and flag currency-driven movements separately. The currency field in the actor's output makes this grouping straightforward. A fifth consideration: the rating field can fluctuate by 0.1 points between runs as new reviews land. If your dashboard tracks rating alongside price, apply a smoothing window (7-day average) to avoid noisy rating alerts drowning out meaningful price signals.
Related use cases
Frequently asked questions
What database should I use for price history?
SQLite is the fastest path for under 50,000 product-day records. Above that, PostgreSQL with a TimescaleDB extension gives you time-series queries (rolling averages, gap detection) out of the box. DuckDB is a strong middle ground — embeddable like SQLite but with columnar storage that handles analytical queries on millions of rows without degradation. For cloud-native setups, BigQuery or Snowflake work but are overkill for most price-monitoring use cases.
How do I handle sites where the actor returns success: false?
The success field distinguishes clean extraction from degraded or failed pages. Log all success: false records separately and review weekly. Common causes: the product was delisted (permanent, remove from watchlist), the page redesigned its structured data (temporary, usually resolves within days as the actor adapts), or a regional block returned a different page. Set maxRetries to 3 for production runs — this handles transient failures without manual intervention.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.