Featured image of post Building a Competitor Monitoring SaaS with DuckDB: Complete Guide from Data Collection to Automated Reports

Building a Competitor Monitoring SaaS with DuckDB: Complete Guide from Data Collection to Automated Reports

Learn how to build a profitable competitor monitoring SaaS using DuckDB. Automatic data collection, SQL-powered analysis, intelligent alerts, and report generation — with full code and monetization strategy.

Building a Competitor Monitoring SaaS with DuckDB: Complete Guide from Data Collection to Automated Reports

In e-commerce and SaaS, whoever controls competitive intelligence controls pricing power. But in reality, most small and medium sellers and entrepreneurs can only track competitors through manual screenshots and Excel spreadsheets — inefficient, error-prone, and slow to act.

Today, we’ll build a complete competitor monitoring system using DuckDB: automatic data collection → local storage → SQL analysis → intelligent alerts → report generation. You can use this system yourself, or package it as a SaaS product charging ¥299-999/month per customer.

Competitor Monitoring System Architecture


Why Competitor Monitoring Is Profitable

Market Pain Points

When making pricing decisions and feature planning, small and medium businesses most often reference their competitors. But traditional approaches have three fatal flaws:

  1. Fragmented information: Competitor prices are scattered across official websites, e-commerce pages, and third-party platforms — no unified view
  2. Frequent changes: Competitors adjust prices, launch new products, and change strategies constantly — manual tracking can’t keep up
  3. High analysis cost: Even when you have the data, comparing and finding trends takes significant time

Commercial tools (like JADU, Price2Spy) cost ¥5,000-20,000/year with limited data sources and poor customization. A self-built system has near-zero marginal cost.

Monetization Paths

ModelPricingTarget CustomersMonthly Revenue
Personal useFreeYourselfSave ¥5,000+/year in tool fees
Small business subscription¥299/moSmall e-commerce sellers10 clients = ¥3,000
Enterprise custom¥999/moBrand owners, agencies5 clients = ¥5,000
SaaS product¥199/mo+Mass replication50 clients = ¥10,000+

System Architecture

The entire system has four layers:

Data Collection → Storage & Compute → Analysis Engine → Output & Delivery
       ↓                ↓                  ↓                ↓
   Scrapers/API    DuckDB Database     SQL Aggregation   Reports/API/Dashboard

Why DuckDB?

DimensionPython + PandasDuckDB
Memory UsageLoads everything into RAMColumnar compression, reads on demand
SQL CapabilityNeeds loops and conditionalsDirect SQL aggregation and JOINs
File Format SupportRequires extra librariesNative CSV/JSON/Parquet
Dirty Data HandlingProne to batch failuresRETURN_NULL_ON_ERROR graceful fallback
Deployment CostNeeds Python environmentSingle binary, zero dependencies

DuckDB’s core advantage: you can complete 80% of data cleaning and aggregation with SQL alone, at blazing speed.


Data Layer: Collection and Storage

3.1 Data Collection (Simulation + Real Solutions)

In real projects, you can use scrapers or APIs to collect data. Here we start with simulated data to validate the logic:

import duckdb
import pandas as pd
from pathlib import Path
from datetime import datetime, timedelta
import random

# Create project directories
Path("competitor_monitor/data").mkdir(parents=True, exist_ok=True)

def generate_competitor_data(days: int = 90):
    """Generate simulated competitor monitoring data"""
    
    # Competitor A: Premium brand, small price fluctuations
    comp_a_prices = []
    base_price_a = 1299
    for day in range(days):
        date = (datetime.now() - timedelta(days=day)).strftime("%Y-%m-%d")
        price = base_price_a + random.randint(-50, 50)
        if day % 14 == 0:  # Bi-weekly promotion
            price = base_price_a - 200
        comp_a_prices.append({
            "competitor": "CompetitorA",
            "date": date,
            "product": "Pro",
            "price": price,
            "url": "https://competitor-a.com/pricing",
            "stock_status": "In Stock" if random.random() > 0.1 else "Out of Stock"
        })
    
    # Competitor B: Mid-range brand, frequent price changes
    comp_b_prices = []
    base_price_b = 799
    for day in range(days):
        date = (datetime.now() - timedelta(days=day)).strftime("%Y-%m-%d")
        price = base_price_b + random.randint(-100, 100)
        if day % 7 == 0:
            price -= 50  # Weekly discount
        comp_b_prices.append({
            "competitor": "CompetitorB",
            "date": date,
            "product": "Standard",
            "price": price,
            "url": "https://competitor-b.com/pricing",
            "stock_status": "In Stock"
        })
    
    # Competitor C: New entrant, aggressive pricing
    comp_c_prices = []
    base_price_c = 499
    for day in range(days):
        date = (datetime.now() - timedelta(days=day)).strftime("%Y-%m-%d")
        price = max(base_price_c - (days - day) // 10, 299)
        comp_c_prices.append({
            "competitor": "CompetitorC",
            "date": date,
            "product": "Basic",
            "price": price,
            "url": "https://competitor-c.com/pricing",
            "stock_status": "In Stock"
        })
    
    # Save to CSV
    all_prices = comp_a_prices + comp_b_prices + comp_c_prices
    pd.DataFrame(all_prices).sort_values("date").to_csv(
        "competitor_monitor/data/competitor_prices.csv", 
        index=False, 
        encoding="utf-8-sig"
    )
    
    print(f"✅ Price data generated: {len(all_prices)} records")
    return "competitor_monitor/data/competitor_prices.csv"

generate_competitor_data()

3.2 DuckDB Database Schema

import duckdb
from pathlib import Path

class CompetitorMonitor:
    """Competitor monitoring and analysis engine"""
    
    def __init__(self, db_path: str = "competitor_monitor/monitor.db"):
        self.con = duckdb.connect(db_path)
        self._setup_schema()
    
    def _setup_schema(self):
        """Create database schema"""
        self.con.execute("""
            CREATE TABLE IF NOT EXISTS competitor_prices (
                competitor VARCHAR,
                date DATE,
                product VARCHAR,
                price DECIMAL(10,2),
                url VARCHAR,
                stock_status VARCHAR
            )
        """)
        
        self.con.execute("""
            CREATE TABLE IF NOT EXISTS feature_updates (
                competitor VARCHAR,
                update_date DATE,
                update_type VARCHAR,
                description VARCHAR,
                source VARCHAR
            )
        """)
        
        self.con.execute("""
            CREATE TABLE IF NOT EXISTS reviews (
                competitor VARCHAR,
                date DATE,
                platform VARCHAR,
                rating INTEGER,
                sentiment VARCHAR,
                summary VARCHAR
            )
        """)

3.3 Data Ingestion

def ingest_data(self, csv_path: str):
    """Import CSV data into DuckDB"""
    self.con.execute(f"""
        COPY competitor_prices 
        FROM '{csv_path}' 
        (FORMAT CSV, HEADER, DELIMITER ',')
    """)
    count = self.con.execute('SELECT COUNT(*) FROM competitor_prices').fetchone()[0]
    print(f"✅ Imported {count} price records")

Analysis Engine: SQL-Powered Deep Analysis

4.1 Price Trend Views

def _setup_views(self):
    """Create analysis views"""
    # Price aggregation view
    self.con.execute("""
        CREATE OR REPLACE VIEW v_price_analysis AS
        SELECT 
            competitor,
            product,
            COUNT(*) AS data_points,
            ROUND(AVG(price), 2) AS avg_price,
            ROUND(MIN(price), 2) AS min_price,
            ROUND(MAX(price), 2) AS max_price,
            ROUND(STDDEV(price), 2) AS price_volatility,
            ROUND(
                100.0 * (MAX(price) - MIN(price)) / NULLIF(AVG(price), 0), 
                2
            ) AS price_variance_pct,
            MAX(date) AS latest_date
        FROM competitor_prices
        GROUP BY competitor, product
    """)
    
    # Daily change view with window functions
    self.con.execute("""
        CREATE OR REPLACE VIEW v_price_daily AS
        SELECT 
            competitor,
            date,
            product,
            price,
            LAG(price) OVER (
                PARTITION BY competitor, product 
                ORDER BY date
            ) AS prev_price,
            price - LAG(price) OVER (
                PARTITION BY competitor, product 
                ORDER BY date
            ) AS price_change,
            ROUND(
                100.0 * (price - LAG(price) OVER (
                    PARTITION BY competitor, product 
                    ORDER BY date
                )) / NULLIF(LAG(price) OVER (
                    PARTITION BY competitor, product 
                    ORDER BY date
                ), 0),
                2
            ) AS price_change_pct
        FROM competitor_prices
    """)
    
    # Moving average view
    self.con.execute("""
        CREATE OR REPLACE VIEW v_price_trend AS
        SELECT 
            competitor,
            date,
            product,
            price,
            ROUND(AVG(price) OVER (
                PARTITION BY competitor, product 
                ORDER BY date 
                ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
            ), 2) AS ma_7day,
            ROUND(AVG(price) OVER (
                PARTITION BY competitor, product 
                ORDER BY date 
                ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
            ), 2) AS ma_30day
        FROM competitor_prices
    """)

4.2 Real-time Query Examples

# View competitor price statistics
print("📊 Competitor Price Overview:")
print(self.con.execute("SELECT * FROM v_price_analysis ORDER BY avg_price").fetchdf())

# View today's price changes
print("\n📈 Today's Price Changes:")
today = datetime.now().strftime("%Y-%m-%d")
print(self.con.execute(f"""
    SELECT competitor, product, price, price_change_pct
    FROM v_price_daily 
    WHERE date = '{today}' 
    AND price_change != 0
    ORDER BY ABS(price_change_pct) DESC
""").fetchdf())

# View 7-day moving average trend
print("\n📉 Price Trend (7-day MA):")
print(self.con.execute(f"""
    SELECT competitor, date, product, price, ma_7day
    FROM v_price_trend 
    WHERE date >= '{(datetime.now()-timedelta(days=7)).strftime("%Y-%m-%d")}'
    ORDER BY competitor, date
""").fetchdf())

4.3 Advanced Analysis: Anomaly Detection

def detect_price_anomalies(self, threshold_pct: float = 5.0):
    """Detect unusual price fluctuations"""
    result = self.con.execute(f"""
        SELECT 
            competitor,
            date,
            product,
            price,
            price_change_pct,
            'Price Anomaly' AS alert_type
        FROM v_price_daily
        WHERE ABS(price_change_pct) >= {threshold_pct}
        ORDER BY ABS(price_change_pct) DESC
    """).fetchdf()
    return result

# Usage example
anomalies = monitor.detect_price_anomalies(5.0)
if len(anomalies) > 0:
    print(f"⚠️ Found {len(anomalies)} price anomalies:")
    print(anomalies.head(10))

Alerts and Report Generation

5.1 Intelligent Alert System

def check_alerts(self) -> list:
    """Check and generate alerts"""
    alerts = []
    
    # Price drop alert
    price_drop = self.con.execute("""
        SELECT competitor, date, product, price, price_change_pct
        FROM v_price_daily
        WHERE date = (SELECT MAX(date) FROM v_price_daily)
        AND price_change_pct <= -5
    """).fetchall()
    
    for row in price_drop:
        alerts.append({
            "type": "PRICE_DROP",
            "competitor": row[0],
            "date": row[1],
            "product": row[2],
            "price": row[3],
            "change_pct": row[4],
            "message": f"⚠️ {row[0]}'s {row[2]} dropped {abs(row[4]):.1f}%, now at ${row[3]}"
        })
    
    # Out of stock alert
    out_of_stock = self.con.execute("""
        SELECT competitor, date, product
        FROM competitor_prices
        WHERE stock_status = 'Out of Stock'
        AND date = (SELECT MAX(date) FROM competitor_prices)
    """).fetchall()
    
    for row in out_of_stock:
        alerts.append({
            "type": "OUT_OF_STOCK",
            "competitor": row[0],
            "date": row[1],
            "product": row[2],
            "message": f"📦 {row[0]}'s {row[2]} is out of stock"
        })
    
    return alerts

5.2 Report Generation

def generate_report(self, output_path: str = "report.html"):
    """Generate HTML analysis report"""
    from jinja2 import Template
    
    template_str = """
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>Competitor Monitor Daily Report - {{ date }}</title>
        <style>
            body { font-family: -apple-system, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; }
            h1 { color: #1a1a2e; border-bottom: 3px solid #4facfe; padding-bottom: 10px; }
            h2 { color: #16213e; margin-top: 30px; }
            table { width: 100%; border-collapse: collapse; margin: 15px 0; }
            th { background: #1a1a2e; color: white; padding: 10px; text-align: left; }
            td { padding: 8px 10px; border-bottom: 1px solid #ddd; }
            tr:hover { background: #f5f5f5; }
            .alert { background: #fff3cd; border-left: 4px solid #ffc107; padding: 10px; margin: 10px 0; }
            .good { color: #28a745; }
            .bad { color: #dc3545; }
            .neutral { color: #6c757d; }
        </style>
    </head>
    <body>
        <h1>📊 Competitor Monitor Daily Report - {{ date }}</h1>
        
        <h2>🔔 Alerts</h2>
        {% for alert in alerts %}
        <div class="alert">{{ alert.message }}</div>
        {% endfor %}
        {% if not alerts %}
        <p class="good">✅ No alerts today</p>
        {% endif %}
        
        <h2>📈 Price Overview</h2>
        <table>
            <tr><th>Competitor</th><th>Product</th><th>Avg Price</th><th>Min</th><th>Max</th><th>Variance</th></tr>
            {% for row in price_analysis %}
            <tr>
                <td>{{ row[0] }}</td>
                <td>{{ row[1] }}</td>
                <td>${{ row[2] }}</td>
                <td>${{ row[3] }}</td>
                <td>${{ row[4] }}</td>
                <td>{{ row[7] }}%</td>
            </tr>
            {% endfor %}
        </table>
        
        <h2>📉 Latest Changes</h2>
        <table>
            <tr><th>Competitor</th><th>Date</th><th>Product</th><th>Current</th><th>Change</th></tr>
            {% for row in daily_changes %}
            <tr>
                <td>{{ row[0] }}</td>
                <td>{{ row[1] }}</td>
                <td>{{ row[2] }}</td>
                <td>${{ row[3] }}</td>
                <td class="{% if row[5] < 0 %}bad{% elif row[5] > 0 %}good{% else %}neutral{% endif %}">
                    {{ '%+.2f%%'|format(row[5]) }}
                </td>
            </tr>
            {% endfor %}
        </table>
        
        <p style="color: #999; font-size: 12px; margin-top: 30px;">
            Generated by DuckDB Competitor Monitor | Data from public sources
        </p>
    </body>
    </html>
    """
    
    template = Template(template_str)
    today = datetime.now().strftime("%Y-%m-%d")
    
    alerts = self.check_alerts()
    price_analysis = self.con.execute("SELECT * FROM v_price_analysis ORDER BY avg_price").fetchall()
    daily_changes = self.con.execute(f"""
        SELECT competitor, date, product, price, prev_price, price_change_pct
        FROM v_price_daily 
        WHERE date = '{today}'
        ORDER BY ABS(price_change_pct) DESC
    """).fetchall()
    
    html = template.render(
        date=today,
        alerts=alerts,
        price_analysis=price_analysis,
        daily_changes=daily_changes
    )
    
    Path(output_path).write_text(html, encoding="utf-8")
    print(f"✅ Report generated: {output_path}")
    return output_path

Complete Run Flow

# Main entry point
if __name__ == "__main__":
    # 1. Initialize monitoring engine
    monitor = CompetitorMonitor()
    
    # 2. Generate/import data
    csv_path = generate_competitor_data()
    monitor.ingest_data(csv_path)
    
    # 3. Create analysis views
    monitor._setup_views()
    
    # 4. Run analysis
    print("\n" + "="*50)
    print("Competitor Price Analysis Report")
    print("="*50)
    
    alerts = monitor.check_alerts()
    if alerts:
        print(f"\n⚠️ Found {len(alerts)} alerts:")
        for alert in alerts:
            print(f"  - {alert['message']}")
    else:
        print("\n✅ No alerts")
    
    # 5. Generate report
    monitor.generate_report("competitor_monitor/report.html")
    
    # 6. Export CSV for further analysis
    monitor.con.execute("""
        COPY (SELECT * FROM v_price_analysis) 
        TO 'competitor_monitor/price_summary.csv' 
        (HEADER, DELIMITER ',')
    """)
    print("\n✅ Data exported: competitor_monitor/price_summary.csv")

From Personal Tool to SaaS Product

7.1 Productization Roadmap

StageFeaturesTech StackPricing
MVPLocal CSV monitoringDuckDB + PythonFree
v1Web UI + Email reportsDuckDB + Flask + SMTP$99/mo
v2API service + Multi-tenantDuckDB + FastAPI + PostgreSQL$299/mo
v3Full SaaSDuckDB + React + Cloud$999/mo

7.2 Key Code: API Service

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="Competitor Monitor API")

class PriceAlert(BaseModel):
    competitor: str
    min_price: float
    max_price: float
    alert_email: str

@app.post("/alerts")
def set_price_alert(alert: PriceAlert):
    """Set price alert"""
    current = monitor.con.execute(f"""
        SELECT price FROM competitor_prices 
        WHERE competitor = '{alert.competitor}'
        AND date = (SELECT MAX(date) FROM competitor_prices)
    """).fetchone()
    
    if current and (current[0] <= alert.min_price or current[0] >= alert.max_price):
        send_alert_email(alert.alert_email, alert.competitor, current[0])
        return {"status": "alert_sent", "price": current[0]}
    
    return {"status": "no_alert", "current_price": current[0] if current else None}

7.3 Automated Scheduling

# Use cron for daily execution
0 9 * * * cd /path/to/competitor_monitor && python3 monitor.py >> logs/cron.log 2>&1

# Or use Python schedule library
import schedule
import time

schedule.every().day.at("09:00").do(run_monitor)
schedule.every().hour.do(check_alerts)

while True:
    schedule.run_pending()
    time.sleep(60)

Comparison with Traditional Solutions

FeatureCommercial Tools (JADU etc.)Python + PandasDuckDB Solution
Price Monitoring
Historical Trends✅ (Limited)✅ (SQL Aggregation)
Anomaly DetectionMust implement✅ (Built-in window functions)
Alert Notifications✅ (Paid)Must implement✅ (Simple code)
Report Generation✅ (Fixed templates)Must implement✅ (Jinja2 templates)
Multi-source Data✅ (ATTACH)
Deployment Cost$500+/yearServer + maintenanceFree/Low-cost
Customization
Learning CurveLowMediumLow (SQL-focused)

Monetization Advice

9.1 Quick Start Steps

  1. Week 1: Build local monitoring system, validate logic with simulated data
  2. Week 2: Connect real data sources (scrapers or APIs), cover 3-5 competitors
  3. Week 3: Generate first automated report, send to 2-3 potential clients for trial
  4. Week 4: Optimize based on feedback, finalize pricing and start promotion

9.2 Pricing Strategy

  • Basic ($199/mo): Daily price monitoring + weekly report
  • Professional ($499/mo): Real-time monitoring + alerts + monthly report + 1 competitor category
  • Enterprise ($999/mo): Full features + multi-category + API access + custom reports

9.3 Customer Acquisition Channels

  • Publish technical articles on Zhihu/Juejin for traffic
  • WeChat Official Account for industry insights
  • Xianyu/Taobao for service listings
  • Community word-of-mouth

Summary

Building a competitor monitoring system with DuckDB offers core value:

  1. SQL-driven: 90% of analysis logic can be expressed in SQL, no complex code needed
  2. Lightweight deployment: Single binary file, no Docker/K8s required
  3. Scalable: From local CSV to Parquet, from single machine to multi-tenant — smooth upgrades
  4. Zero-cost start: Open source and free, marginal cost approaches zero

This system is ready to run. Next steps: connect real data → find your first paying client → iterate the product.

💡 More DuckDB实战技巧 → duckdblab.org

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy

⚠️ This site is an independent community project, not affiliated with, endorsed by, or sponsored by the DuckDB Foundation or official DuckDB project.

"DuckDB" is a registered trademark of the DuckDB Foundation. This site uses the name solely for factual description purposes.

All content is for educational and community promotion purposes only and does not constitute any commercial service.