Tutorial2 min readUpdated Mar 22, 2026

Build a Price Monitoring Dashboard

TL;DR

Combine PriceFetch API with Flask and Chart.js to build a self-hosted price monitoring dashboard. Track products, view history, spot trends.

Dashboard Architecture

The dashboard has three layers: a data collector (cron job that fetches prices from PriceFetch API), a SQLite database (stores price history), and a Flask web app (renders charts and tables).

This is intentionally simple. No React, no build step, no complex state management. Flask serves HTML pages with Chart.js for graphs. You can run the whole thing on a $5 VPS or your local machine.

Try it yourself — 500 free API credits, no credit card required.

Start Free

Project Setup

Install the dependencies and create the project structure. Flask handles the web layer, httpx talks to PriceFetch, and Jinja2 (included with Flask) handles templates.

bash
pip install flask httpx

mkdir -p templates static
touch app.py collector.py

Price Data Collector

The collector runs on a schedule and populates the database. It's separate from the web app so you can run it via cron without starting the Flask server. Each product URL gets one PriceFetch API call, and the result is stored with a timestamp.

This is the same pattern from the price tracking tutorial, packaged as a standalone module.

python
# collector.py
import httpx
import sqlite3
import os
from datetime import datetime

API_KEY = os.environ["PRICEFETCH_API_KEY"]

def init_db():
    conn = sqlite3.connect("dashboard.db")
    conn.execute("""CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY, url TEXT UNIQUE, name TEXT, created_at TEXT DEFAULT (datetime('now'))
    )""")
    conn.execute("""CREATE TABLE IF NOT EXISTS snapshots (
        id INTEGER PRIMARY KEY, product_id INTEGER, price REAL, currency TEXT,
        in_stock INTEGER, fetched_at TEXT DEFAULT (datetime('now')),
        FOREIGN KEY (product_id) REFERENCES products(id)
    )""")
    conn.commit()
    return conn

def collect_prices():
    conn = init_db()
    products = conn.execute("SELECT id, url FROM products").fetchall()
    for pid, url in products:
        resp = httpx.get(
            "https://api.pricefetch.dev/v1/price",
            params={"url": url},
            headers={"X-API-Key": API_KEY},
            timeout=15.0,
        )
        data = resp.json()
        if data["success"]:
            d = data["data"]
            conn.execute(
                "INSERT INTO snapshots (product_id, price, currency, in_stock) VALUES (?,?,?,?)",
                (pid, d["price"], d["currency"], int(d["in_stock"])),
            )
    conn.commit()
    conn.close()

if __name__ == "__main__":
    collect_prices()

Flask Web Application

The Flask app serves two pages: a product list with current prices, and a detail page with a price history chart. The chart data comes from SQLite and gets rendered by Chart.js on the client side.

The `/api/history/<id>` endpoint returns JSON that Chart.js consumes directly. This keeps the server-side rendering simple while still having interactive charts.

python
# app.py
from flask import Flask, render_template, jsonify, request, redirect
import sqlite3

app = Flask(__name__)

def get_db():
    conn = sqlite3.connect("dashboard.db")
    conn.row_factory = sqlite3.Row
    return conn

@app.route("/")
def index():
    conn = get_db()
    products = conn.execute("""
        SELECT p.id, p.name, p.url, s.price, s.currency, s.in_stock, s.fetched_at
        FROM products p
        LEFT JOIN snapshots s ON s.id = (
            SELECT id FROM snapshots WHERE product_id = p.id ORDER BY fetched_at DESC LIMIT 1
        )
    """).fetchall()
    return render_template("index.html", products=products)

@app.route("/product/<int:product_id>")
def product_detail(product_id):
    conn = get_db()
    product = conn.execute("SELECT * FROM products WHERE id = ?", (product_id,)).fetchone()
    return render_template("product.html", product=product)

@app.route("/api/history/<int:product_id>")
def price_history_api(product_id):
    conn = get_db()
    rows = conn.execute(
        "SELECT price, currency, fetched_at FROM snapshots WHERE product_id = ? ORDER BY fetched_at",
        (product_id,),
    ).fetchall()
    return jsonify({
        "labels": [r["fetched_at"] for r in rows],
        "prices": [r["price"] for r in rows],
        "currency": rows[0]["currency"] if rows else "USD",
    })

@app.route("/add", methods=["POST"])
def add_product():
    url = request.form["url"]
    name = request.form.get("name", url)
    conn = get_db()
    conn.execute("INSERT OR IGNORE INTO products (url, name) VALUES (?, ?)", (url, name))
    conn.commit()
    return redirect("/")

if __name__ == "__main__":
    app.run(debug=True, port=5000)

Price Chart with Chart.js

The product detail page uses Chart.js to render a line chart of price history. The data loads from the Flask JSON endpoint, so the page renders instantly and the chart fills in asynchronously.

This is a minimal template — customize the colors, add tooltips, or switch to a different chart library as needed. The key point is that the data pipeline (PriceFetch API -> SQLite -> Flask JSON -> Chart.js) is clean and each layer is replaceable.

html
<!-- templates/product.html -->
<!DOCTYPE html>
<html>
<head>
  <title>{{ product.name }} - Price History</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body style="font-family: monospace; max-width: 800px; margin: 0 auto; padding: 2rem;">
  <h1>{{ product.name }}</h1>
  <p><a href="{{ product.url }}" target="_blank">{{ product.url }}</a></p>
  <canvas id="priceChart" height="300"></canvas>
  <script>
    fetch("/api/history/{{ product.id }}")
      .then(r => r.json())
      .then(data => {
        new Chart(document.getElementById("priceChart"), {
          type: "line",
          data: {
            labels: data.labels,
            datasets: [{
              label: `Price (${data.currency})`,
              data: data.prices,
              borderColor: "#3b82f6",
              tension: 0.1
            }]
          }
        });
      });
  </script>
</body>
</html>

Frequently asked questions

Related Retailers

Start fetching prices — 500 free credits

Sign up in 30 seconds. No credit card required. One credit per successful API call.