DEV Community

dodou
dodou

Posted on

SERP API + Python: Build a SEO Rank Monitor in 30 Minutes

Project Goal

Build an SEO monitoring tool in 30 minutes:

  • Check 100 keywords daily for SERP ranking
  • Store data in SQLite
  • Email alerts on ranking changes
  • Simple query interface

Complete Code (5 Steps)

Step 1: Database Design (2 minutes)

import sqlite3
from datetime import datetime

def init_db():
    conn = sqlite3.connect("seo_monitor.db")
    c = conn.cursor()
    c.execute("""
        CREATE TABLE IF NOT EXISTS rankings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            ts DATE NOT NULL,
            keyword TEXT NOT NULL,
            domain TEXT NOT NULL,
            position INTEGER,
            url TEXT,
            snippet TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    c.execute("CREATE INDEX IF NOT EXISTS idx_keyword_ts ON rankings(keyword, ts)")
    c.execute("CREATE INDEX IF NOT EXISTS idx_domain_ts ON rankings(domain, ts)")
    conn.commit()
    conn.close()
Enter fullscreen mode Exit fullscreen mode

Step 2: SERP API Fetch Function (5 minutes)

import requests
import os
from dotenv import load_dotenv
load_dotenv()

API_KEY = os.environ["SERPBASE_KEY"]
ENDPOINT = "https://api.serpbase.dev/google/search"

def fetch_serp(keyword, gl="us", hl="en", num=20):
    r = requests.post(
        ENDPOINT,
        headers={"X-API-Key": API_KEY},
        json={"q": keyword, "gl": gl, "hl": hl, "num": num},
        timeout=10,
    )
    r.raise_for_status()
    return r.json()

def get_position(data, target_domain):
    for i, item in enumerate(data.get("organic", []), 1):
        if target_domain in item.get("link", ""):
            return i, item["link"], item.get("snippet", "")
    return None, None, None
Enter fullscreen mode Exit fullscreen mode

Step 3: Collect + Store Function (5 minutes)

import sqlite3
from datetime import datetime

def collect_and_store(keyword, target_domain, gl="us"):
    data = fetch_serp(keyword, gl=gl)
    pos, url, snippet = get_position(data, target_domain)

    conn = sqlite3.connect("seo_monitor.db")
    c = conn.cursor()
    c.execute("""
        INSERT INTO rankings (ts, keyword, domain, position, url, snippet)
        VALUES (?, ?, ?, ?, ?, ?)
    """, (datetime.now().date().isoformat(), keyword, target_domain, pos, url, snippet))
    conn.commit()
    conn.close()

    return {"keyword": keyword, "position": pos, "url": url}
Enter fullscreen mode Exit fullscreen mode

Step 4: Batch + Cron Scheduling (8 minutes)

KEYWORDS = [
    "best serp api",
    "cheap serp api",
    "serp api for seo",
    # ... up to 100
]
DOMAIN = "yourdomain.com"

def daily_check():
    for kw in KEYWORDS:
        try:
            result = collect_and_store(kw, DOMAIN)
            print(f"OK {kw}: #{result['position']}")
        except Exception as e:
            print(f"FAIL {kw}: {e}")

if __name__ == "__main__":
    daily_check()
Enter fullscreen mode Exit fullscreen mode

Add cron: 0 9 * * * /usr/bin/python3 /path/to/seo_monitor.py

Step 5: Alerts + Reports (10 minutes)

import sqlite3
from datetime import datetime, timedelta

def detect_changes(days=2):
    """Detect ranking changes, send email alerts"""
    conn = sqlite3.connect("seo_monitor.db")
    c = conn.cursor()

    today = datetime.now().date()
    yesterday = today - timedelta(days=1)

    c.execute("""
        SELECT keyword, position, url, ts FROM rankings
        WHERE ts IN (?, ?)
    """, (yesterday.isoformat(), today.isoformat()))

    rows = c.fetchall()

    # Group by keyword, ts
    today_data = {}
    yesterday_data = {}
    for kw, pos, url, ts in rows:
        if pos is None:
            continue
        if ts == today.isoformat():
            today_data[kw] = (pos, url)
        else:
            yesterday_data[kw] = (pos, url)

    # Compare changes
    changes = []
    for kw, (pos_now, url) in today_data.items():
        if kw in yesterday_data:
            pos_yesterday, _ = yesterday_data[kw]
            delta = pos_yesterday - pos_now
            if abs(delta) >= 5:  # 5+ position change
                changes.append({
                    "keyword": kw,
                    "old": pos_yesterday,
                    "new": pos_now,
                    "delta": delta,
                })

    return changes

def send_email(changes):
    import smtplib
    from email.mime.text import MIMEText

    if not changes:
        return

    body = "SERP Monitor Alert:\n\n"
    for c in changes:
        body += f"- {c['keyword']}: #{c['old']} -> #{c['new']} ({'+' if c['delta'] > 0 else ''}{c['delta']})\n"

    msg = MIMEText(body)
    msg["Subject"] = f"SERP Alert: {len(changes)} keyword(s) changed"
    msg["From"] = "alert@yourdomain.com"
    msg["To"] = "you@yourdomain.com"

    with smtplib.SMTP("localhost") as s:
        s.send_message(msg)

if __name__ == "__main__":
    changes = detect_changes()
    send_email(changes)
Enter fullscreen mode Exit fullscreen mode

Complete Flow

[Daily 9am cron] → [daily_check()] → [SERP API] → [SQLite storage]
                                              ↓
                                       [detect_changes()] → [Email alert]
                                              ↓
                                       [Query interface / Report]
Enter fullscreen mode Exit fullscreen mode

5 Advanced Directions

1. Add Dashboard (Streamlit)

import streamlit as st
import pandas as pd
import sqlite3

def dashboard():
    conn = sqlite3.connect("seo_monitor.db")
    df = pd.read_sql("SELECT * FROM rankings ORDER BY ts DESC LIMIT 1000", conn)
    st.dataframe(df)

    # Simple chart
    st.line_chart(df.groupby("ts")["position"].mean())

# streamlit run dashboard.py
Enter fullscreen mode Exit fullscreen mode

2. Add More Regions

REGIONS = [("us", "en"), ("uk", "en"), ("jp", "ja"), ("cn", "zh-CN")]

for gl, hl in REGIONS:
    for kw in KEYWORDS:
        collect_and_store(kw, DOMAIN, gl=gl)
Enter fullscreen mode Exit fullscreen mode

3. Add Competitor Monitoring

def monitor_competitors(keyword, my_domain, competitors):
    data = fetch_serp(keyword)
    for i, item in enumerate(data.get("organic", []), 1):
        for comp in competitors:
            if comp in item.get("link", ""):
                return {"competitor": comp, "position": i}
    return None
Enter fullscreen mode Exit fullscreen mode

4. Add AI Overview Tracking

def check_ai_overview(keyword, my_domain):
    data = fetch_serp(keyword)
    ao = data.get("ai_overview", {})
    if not ao:
        return {"triggered": False}
    cited = any(my_domain in link.get("link", "") for block in ao.get("blocks", []) for link in block.get("links", []))
    return {"triggered": True, "cited": cited, "text": ao.get("text", "")[:200]}
Enter fullscreen mode Exit fullscreen mode

5. Add LLM Analysis

import anthropic

def llm_analyze_changes(changes):
    client = anthropic.Anthropic()
    prompt = f"Analyze these SERP ranking changes:\n{changes}"
    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=500,
        messages=[{"role": "user", "content": prompt}],
    )
    return response.content[0].text
Enter fullscreen mode Exit fullscreen mode

Deployment Options

Option Best for
Local cron Personal / small team
VPS ($5/month) Small-medium SEO tool
Cloud Function Serverless, pay-per-call
GitHub Actions Free, once per day

5 Common Mistakes

  1. Too many keywords: 10k keywords = 10k calls/day = $3/month
  2. No country dimension: only us = incomplete data
  3. No raw data storage: only storing position, lose snippet/url for later analysis
  4. Alert threshold too strict: 1-position change = alert fatigue
  5. No database archival: 1 year later, huge DB; archive old to cold storage

What's Next

Direction Resource
Real-time alerts "SERP API + Slack alert bot"
Cache layer "SERP API cache layer: 5 strategies"
RAG grounding "SERP API + Claude in practice"
Error handling "SERP API error handling: 5 patterns"

100 free searches: serpbase.dev signup, run 100 keywords / 1 week to see your SEO monitoring data.

Top comments (0)