Project Goal
Build a Slack alert bot in 30 minutes:
- Check 10 core keywords SERP ranking every hour
- Send Slack alert if ranking changes more than 3 positions
- New competitors / disappeared rankings trigger immediate alert
- Daily 9am report (overall changes summary)
Step 1: Slack Setup (5 minutes)
Create Slack App
- Go to https://api.slack.com/apps
- Click "Create New App" → "From scratch"
- Name: "SERP Alert Bot", select workspace
- Click "Incoming Webhooks" → toggle on
- "Add New Webhook to Workspace" → select channel (#seo-alerts)
- Copy webhook URL (looks like
https://hooks.slack.com/services/T.../B.../X...)
Slack Utility
import requests
import os
SLACK_WEBHOOK = os.environ["SLACK_WEBHOOK"]
def send_slack(text, blocks=None):
payload = {"text": text}
if blocks:
payload["blocks"] = blocks
r = requests.post(SLACK_WEBHOOK, json=payload, timeout=10)
r.raise_for_status()
# Test
send_slack("SERP alert bot online")
Step 2: Data Storage (3 minutes)
import sqlite3
from datetime import datetime
def init_db():
conn = sqlite3.connect("serp_history.db")
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TIMESTAMP NOT NULL,
keyword TEXT NOT NULL,
position INTEGER,
url TEXT,
title TEXT,
new_competitor TEXT,
disappeared BOOLEAN DEFAULT 0
)
""")
c.execute("CREATE INDEX IF NOT EXISTS idx_keyword_ts ON history(keyword, ts)")
conn.commit()
conn.close()
Step 3: SERP Query (5 minutes)
import os
import requests
API_KEY = os.environ["SERPBASE_KEY"]
ENDPOINT = "https://api.serpbase.dev/google/search"
DOMAIN = "yourdomain.com"
KEYWORDS = [
"your brand keyword 1",
"your brand keyword 2",
"your product keyword 1",
"your product keyword 2",
"your product keyword 3",
# up to 10
]
def fetch_serp(keyword, gl="us", hl="en", num=10):
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_my_position(data, domain=DOMAIN):
for i, item in enumerate(data.get("organic", []), 1):
if domain in item.get("link", ""):
return i, item.get("link"), item.get("title")
return None, None, None
def get_competitors(data, exclude_domain=DOMAIN, top_n=10):
"""Extract top 10 competitor domains"""
competitors = set()
for item in data.get("organic", [])[:top_n]:
link = item.get("link", "")
if exclude_domain not in link:
domain = "/".join(link.split("/")[:3])
competitors.add(domain)
return list(competitors)
Step 4: Anomaly Detection (10 minutes)
import sqlite3
from datetime import datetime, timedelta
def check_changes(threshold=3):
"""Compare with last data, alert on changes > threshold"""
conn = sqlite3.connect("serp_history.db")
c = conn.cursor()
alerts = []
for kw in KEYWORDS:
# Get last 2 data points
c.execute("""
SELECT ts, position, url, new_competitor, disappeared
FROM history
WHERE keyword = ?
ORDER BY ts DESC LIMIT 2
""", (kw,))
rows = c.fetchall()
if len(rows) < 2:
continue
current_ts, current_pos, current_url, current_competitor, _ = rows[0]
last_ts, last_pos, last_url, last_competitor, last_disappeared = rows[1]
# Ranking change
if current_pos and last_pos:
delta = last_pos - current_pos
if abs(delta) >= threshold:
direction = "up" if delta > 0 else "down"
alerts.append({
"keyword": kw,
"type": "rank_change",
"old": last_pos,
"new": current_pos,
"delta": delta,
"direction": direction,
})
# Disappeared (had rank, now no)
if last_pos and not current_pos:
alerts.append({
"keyword": kw,
"type": "disappeared",
"old": last_pos,
"new": None,
})
# Appeared (no rank before, now has)
if not last_pos and current_pos:
alerts.append({
"keyword": kw,
"type": "appeared",
"old": None,
"new": current_pos,
})
# New competitor
if current_competitor and current_competitor != last_competitor:
alerts.append({
"keyword": kw,
"type": "new_competitor",
"competitor": current_competitor,
})
conn.close()
return alerts
def save_check(keyword, position, url, new_competitor):
conn = sqlite3.connect("serp_history.db")
c = conn.cursor()
c.execute("""
INSERT INTO history (ts, keyword, position, url, new_competitor)
VALUES (?, ?, ?, ?, ?)
""", (datetime.now().isoformat(), keyword, position, url, new_competitor))
conn.commit()
conn.close()
Step 5: Slack Notification (5 minutes)
def send_alerts(alerts):
if not alerts:
return
# Categorize
rank_changes = [a for a in alerts if a["type"] == "rank_change"]
disappeared = [a for a in alerts if a["type"] == "disappeared"]
appeared = [a for a in alerts if a["type"] == "appeared"]
new_competitors = [a for a in alerts if a["type"] == "new_competitor"]
blocks = [
{"type": "header", "text": {"type": "plain_text", "text": f"SERP Alert ({len(alerts)} changes)"}},
]
if rank_changes:
text = ""
for a in rank_changes:
arrow = "UP" if a["delta"] > 0 else "DOWN"
text += f"• {a['keyword']}: #{a['old']} -> #{a['new']} {arrow}{abs(a['delta'])}\n"
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": f"*Rank Changes*\n{text}"}})
if disappeared:
text = "\n".join(f"• {a['keyword']} (was #{a['old']})" for a in disappeared)
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": f"*Disappeared*\n{text}"}})
if appeared:
text = "\n".join(f"• {a['keyword']}: entered #{a['new']}" for a in appeared)
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": f"*New Entries*\n{text}"}})
if new_competitors:
text = "\n".join(f"• {a['keyword']}: {a['competitor']} entered" for a in new_competitors)
blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": f"*New Competitors*\n{text}"}})
send_slack("", blocks=blocks)
def send_daily_report():
"""Send daily 9am summary"""
conn = sqlite3.connect("serp_history.db")
c = conn.cursor()
today = datetime.now().date().isoformat()
c.execute("""
SELECT keyword, position, url FROM history
WHERE ts LIKE ?
ORDER BY keyword
""", (f"{today}%",))
rows = c.fetchall()
conn.close()
if not rows:
send_slack("No SERP data today")
return
text = f"*SERP Daily Report ({today})*\n\n"
for kw, pos, url in rows:
text += f"• {kw}: #{pos or 'unranked'} {url or ''}\n"
send_slack(text)
Step 6: Main Loop (2 minutes)
import schedule
import time
def hourly_check():
"""Check every hour, alert on changes"""
for kw in KEYWORDS:
data = fetch_serp(kw)
pos, url, _ = get_my_position(data)
new_competitors = get_competitors(data)
new_comp = new_competitors[0] if new_competitors else None
save_check(kw, pos, url, new_comp)
alerts = check_changes(threshold=3)
send_alerts(alerts)
if __name__ == "__main__":
init_db()
# Run once at startup
hourly_check()
# Daily 9am report
schedule.every().day.at("09:00").do(send_daily_report)
# Every hour check
schedule.every().hour.do(hourly_check)
while True:
schedule.run_pending()
time.sleep(60)
Deployment Options
| Option | Best for |
|---|---|
| VPS ($5/month) | 24/7 run |
| Cloud Function (scheduled) | Serverless, cheap |
| GitHub Actions | Free, hourly limit |
Slack Message Example
[Header] SERP Alert (3 changes)
[Section] Rank Changes
• your brand keyword 1: #3 -> #6 DOWN3
• your product keyword 1: #5 -> #2 UP3
[Section] New Competitors
• your product keyword 2: competitor-x.com entered
5 Advanced Directions
- Add PAA change alerts: People Also Ask new questions = content opportunity
- Add AI Overview monitoring: brand citation in AI answers
- Add multi-region monitoring: us / uk / cn separately
- Add dashboard: weekly summary chart to Slack channel
- Add anomaly detection: ML detect unusual volatility (Google update mass alerts)
100 free searches: serpbase.dev signup, small-scale 10 keywords / 1 week test.
Top comments (0)