Enterprise rank trackers charge $50-200/month to monitor a handful of keywords. With the Scavio Search API at $0.005/credit, you can track 50 keywords daily for $0.035/week -- under $2/year. This tutorial builds a complete rank tracker with SQLite storage, daily cron scheduling, and position change alerts.
Prerequisites
- Python 3.11+
- A Scavio API key from https://scavio.dev
- SQLite3 (included with Python)
- A cron scheduler or Task Scheduler for daily runs
Walkthrough
Step 1: Set up the SQLite database and keyword list
Create a SQLite database to store daily rank snapshots. Each row records the keyword, the target domain's position, the date, and the URL that ranked.
import sqlite3
from pathlib import Path
DB_PATH = Path("rank_tracker.db")
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS rankings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT NOT NULL,
position INTEGER,
url TEXT,
checked_at TEXT NOT NULL,
domain TEXT NOT NULL
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_keyword_date
ON rankings(keyword, checked_at)
""")
conn.commit()
conn.close()
KEYWORDS = [
"best API for web scraping 2026",
"cheap search API for agents",
"MCP server search tool",
# ... add up to 50 keywords
]
TARGET_DOMAIN = "scavio.dev"
init_db()Step 2: Check rankings via the Scavio Search API
For each keyword, search and find your target domain's position in the results. Store the position (or null if not found in top results) in the database.
import httpx
from datetime import date
SCAVIO_API_KEY = "your-api-key"
async def check_ranking(client: httpx.AsyncClient, keyword: str) -> dict:
resp = await client.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": SCAVIO_API_KEY},
json={"query": keyword, "num_results": 10}
)
resp.raise_for_status()
results = resp.json().get("results", [])
for i, r in enumerate(results):
url = r.get("url", "")
if TARGET_DOMAIN in url:
return {"position": i + 1, "url": url}
return {"position": None, "url": None}
async def check_all_keywords():
conn = sqlite3.connect(DB_PATH)
today = date.today().isoformat()
async with httpx.AsyncClient(timeout=15) as client:
for keyword in KEYWORDS:
result = await check_ranking(client, keyword)
conn.execute(
"INSERT INTO rankings (keyword, position, url, checked_at, domain) VALUES (?, ?, ?, ?, ?)",
(keyword, result["position"], result["url"], today, TARGET_DOMAIN)
)
conn.commit()
conn.close()Step 3: Detect position changes and generate alerts
Compare today's rankings against yesterday's. Flag any keyword that moved 3 or more positions in either direction.
def get_position_changes(threshold: int = 3) -> list[dict]:
conn = sqlite3.connect(DB_PATH)
rows = conn.execute("""
SELECT
r1.keyword,
r2.position AS prev_position,
r1.position AS curr_position,
r1.url,
r1.checked_at
FROM rankings r1
JOIN rankings r2
ON r1.keyword = r2.keyword
AND r1.domain = r2.domain
AND r2.checked_at = date(r1.checked_at, '-1 day')
WHERE r1.checked_at = date('now')
AND r1.position IS NOT NULL
AND r2.position IS NOT NULL
AND ABS(r1.position - r2.position) >= ?
ORDER BY ABS(r1.position - r2.position) DESC
""", (threshold,)).fetchall()
conn.close()
changes = []
for keyword, prev, curr, url, checked in rows:
direction = "up" if curr < prev else "down"
changes.append({
"keyword": keyword,
"previous": prev,
"current": curr,
"change": prev - curr,
"direction": direction,
"url": url
})
return changesStep 4: Generate a daily cost and summary report
Print a summary showing total keywords tracked, credits used, cost, and any significant position changes. Schedule this script with cron for daily automation.
import asyncio
async def daily_report():
await check_all_keywords()
credits_used = len(KEYWORDS) # 1 credit per search
cost = credits_used * 0.005
weekly_cost = cost * 7
changes = get_position_changes(threshold=3)
print(f"Rank Tracker Report - {date.today().isoformat()}")
print(f"Keywords tracked: {len(KEYWORDS)}")
print(f"Credits used: {credits_used}")
print(f"Daily cost: {cost:.3f}")
print(f"Weekly cost: {weekly_cost:.3f}")
print(f"Position changes (3+): {len(changes)}")
for c in changes:
arrow = "improved" if c["direction"] == "up" else "dropped"
print(f" {c['keyword']}: #{c['previous']} -> #{c['current']} ({arrow} by {abs(c['change'])})")
asyncio.run(daily_report())
# Cron: 0 8 * * * cd /path/to/tracker && python rank_tracker.pyPython Example
import asyncio
import sqlite3
import httpx
from datetime import date
from pathlib import Path
SCAVIO_API_KEY = "your-api-key"
TARGET_DOMAIN = "scavio.dev"
DB_PATH = Path("rank_tracker.db")
KEYWORDS = [
"best API for web scraping 2026",
"cheap search API for agents",
"MCP server search tool",
]
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS rankings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT, position INTEGER, url TEXT,
checked_at TEXT, domain TEXT
)
""")
conn.commit()
conn.close()
async def check_ranking(client, keyword):
resp = await client.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": SCAVIO_API_KEY},
json={"query": keyword, "num_results": 10}
)
resp.raise_for_status()
for i, r in enumerate(resp.json().get("results", [])):
if TARGET_DOMAIN in r.get("url", ""):
return i + 1, r["url"]
return None, None
async def main():
init_db()
conn = sqlite3.connect(DB_PATH)
today = date.today().isoformat()
async with httpx.AsyncClient(timeout=15) as client:
for kw in KEYWORDS:
pos, url = await check_ranking(client, kw)
conn.execute(
"INSERT INTO rankings VALUES (NULL,?,?,?,?,?)",
(kw, pos, url, today, TARGET_DOMAIN)
)
conn.commit()
cost = len(KEYWORDS) * 0.005
print(f"Tracked {len(KEYWORDS)} keywords | Cost: {cost:.3f} | Weekly: {cost * 7:.3f}")
conn.close()
asyncio.run(main())JavaScript Example
const SCAVIO_API_KEY = "your-api-key";
const TARGET_DOMAIN = "scavio.dev";
const KEYWORDS = [
"best API for web scraping 2026",
"cheap search API for agents",
"MCP server search tool"
];
async function checkRanking(keyword) {
const resp = await fetch("https://api.scavio.dev/api/v1/search", {
method: "POST",
headers: { "x-api-key": SCAVIO_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ query: keyword, num_results: 10 })
});
const data = await resp.json();
const results = data.results || [];
for (let i = 0; i < results.length; i++) {
if ((results[i].url || "").includes(TARGET_DOMAIN)) {
return { position: i + 1, url: results[i].url };
}
}
return { position: null, url: null };
}
async function main() {
const rankings = [];
for (const kw of KEYWORDS) {
const rank = await checkRanking(kw);
rankings.push({ keyword: kw, ...rank });
}
const cost = KEYWORDS.length * 0.005;
console.log("Rankings:", JSON.stringify(rankings, null, 2));
console.log("Daily cost: $" + cost.toFixed(3));
console.log("Weekly cost: $" + (cost * 7).toFixed(3));
}
main();Expected Output
Tracked 3 keywords | Cost: $0.015 | Weekly: $0.105