Overview
Runs weekly SERP checks for a target keyword list, records which domains appear in top-10 results for each keyword, and identifies gaps where competitor domains rank but the monitored domain does not.
Trigger
Weekly cron (Sundays at 9 PM)
Schedule
Weekly, Sundays at 9 PM (cron: 0 21 * * 0)
Workflow Steps
Load keywords and competitor domains
Read keyword list and list of competitor domains from config. These are the queries to check weekly.
Fetch SERP results for each keyword
POST to Scavio search API for each keyword. Extract top-10 result URLs and their positions.
Record domain presence
For each keyword result, check which tracked domains (your domain + competitors) appear in positions 1-10. Store presence data with position.
Identify gap keywords
A gap keyword is one where at least one competitor domain appears in positions 1-10 AND your domain does not appear in top-10 at all.
Score and prioritize gaps
Score each gap by competitor ranking position (competitor at #1 = higher priority gap than competitor at #9) and by number of competitors ranking for the keyword.
Output gap report
Generate CSV report: keyword, your_position (or 'not ranking'), competitors_ranking (list with positions), gap_score. Sort by gap_score descending.
Python Implementation
import sqlite3
import requests
import csv
from datetime import date
import time
DB_PATH = "keyword_gaps.db"
SCRAVIO_KEY = "YOUR_API_KEY"
MY_DOMAIN = "scavio.com"
COMPETITOR_DOMAINS = ["serpapi.com", "serper.dev", "brightdata.com"]
KEYWORDS = [
"search api for ai agents",
"tiktok data api",
"amazon product search api",
"serp api python",
]
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.executescript("""
CREATE TABLE IF NOT EXISTS serp_snapshots (
keyword TEXT, domain TEXT, position INTEGER, date TEXT
);
""")
conn.commit()
return conn
def fetch_serp(keyword: str) -> list:
resp = requests.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": SCRAVIO_KEY},
json={"query": keyword, "platform": "google", "num": 10}
)
resp.raise_for_status()
return resp.json().get("results", [])
def run():
conn = init_db()
today = date.today().isoformat()
gaps = []
for kw in KEYWORDS:
results = fetch_serp(kw)
domain_positions = {}
for i, r in enumerate(results[:10], start=1):
url = r.get("url", "")
for domain in [MY_DOMAIN] + COMPETITOR_DOMAINS:
if domain in url:
if domain not in domain_positions:
domain_positions[domain] = i
conn.execute("INSERT INTO serp_snapshots VALUES (?,?,?,?)",
(kw, r.get("url"), i, today))
my_pos = domain_positions.get(MY_DOMAIN)
competing = {d: p for d, p in domain_positions.items() if d != MY_DOMAIN}
if competing and not my_pos:
gap_score = sum(11 - p for p in competing.values())
gaps.append({
"keyword": kw,
"your_position": "not ranking",
"competitors": "; ".join(f"{d}@{p}" for d, p in competing.items()),
"gap_score": gap_score
})
conn.commit()
time.sleep(0.3)
gaps.sort(key=lambda x: x["gap_score"], reverse=True)
with open(f"keyword_gaps_{today}.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["keyword", "your_position", "competitors", "gap_score"])
writer.writeheader()
writer.writerows(gaps)
print(f"{len(gaps)} gap keywords found. Report: keyword_gaps_{today}.csv")
if __name__ == "__main__":
run()
JavaScript Implementation
const Database = require('better-sqlite3');
const fetch = require('node-fetch');
const fs = require('fs');
const { stringify } = require('csv-stringify/sync');
const DB_PATH = 'keyword_gaps.db';
const SCRAVIO_KEY = 'YOUR_API_KEY';
const MY_DOMAIN = 'scavio.com';
const COMPETITOR_DOMAINS = ['serpapi.com', 'serper.dev', 'brightdata.com'];
const KEYWORDS = ['search api for ai agents', 'tiktok data api', 'amazon product search api'];
const db = new Database(DB_PATH);
db.exec('CREATE TABLE IF NOT EXISTS serp_snapshots (keyword TEXT, domain TEXT, position INTEGER, date TEXT);');
async function fetchSerp(keyword) {
const res = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST',
headers: { 'x-api-key': SCRAVIO_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ query: keyword, platform: 'google', num: 10 })
});
return (await res.json()).results || [];
}
async function run() {
const today = new Date().toISOString().slice(0, 10);
const gaps = [];
for (const kw of KEYWORDS) {
const results = await fetchSerp(kw);
const domainPos = {};
results.slice(0, 10).forEach((r, i) => {
const url = r.url || '';
for (const d of [MY_DOMAIN, ...COMPETITOR_DOMAINS]) {
if (url.includes(d) && !domainPos[d]) domainPos[d] = i + 1;
}
});
const myPos = domainPos[MY_DOMAIN];
const competing = Object.fromEntries(Object.entries(domainPos).filter(([d]) => d !== MY_DOMAIN));
if (Object.keys(competing).length && !myPos) {
const gapScore = Object.values(competing).reduce((s, p) => s + (11 - p), 0);
gaps.push({ keyword: kw, your_position: 'not ranking', competitors: Object.entries(competing).map(([d,p]) => `${d}@${p}`).join('; '), gap_score: gapScore });
}
await new Promise(r => setTimeout(r, 300));
}
gaps.sort((a, b) => b.gap_score - a.gap_score);
fs.writeFileSync(`keyword_gaps_${today}.csv`, stringify(gaps, { header: true }));
console.log(`${gaps.length} gaps found.`);
}
run().catch(console.error);
Platforms Used
Web search with knowledge graph, PAA, and AI overviews