A rank tracker is the simplest high-value SEO tool you can build: query Google for a keyword, find your domain, record the position, repeat daily. Here’s a complete one in Python using serp.cheap.
0. Sanity-check your key
Export your key and hit the API once with curl before writing any Python:
export SERP_CHEAP_API_KEY="your-key"
curl -s https://api.serp.cheap/v1/search \
-H "X-API-Key: $SERP_CHEAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"q":"project management software","gl":"us"}'
You get back an organic[] array with position, title, link, and snippet per result. That’s everything a rank tracker needs.
1. Find your position
import os
import requests
API_KEY = os.environ["SERP_CHEAP_API_KEY"]
def get_rank(keyword, domain, gl="us"):
r = requests.post(
"https://api.serp.cheap/v1/search",
headers={"X-API-Key": API_KEY},
json={"q": keyword, "gl": gl},
timeout=30,
)
r.raise_for_status()
for result in r.json()["organic"]:
if domain.lower() in result["link"].lower():
return result["position"]
return None # genuinely not in the results
print(get_rank("project management software", "asana.com"))
2. Store history in SQLite
One subtlety: a failed request and “not ranked” are different things. If the API call times out and you store None, your history shows a false ranking drop. So catch request errors, log them, and skip the row — only store None when the search actually ran and your domain wasn’t there.
import sqlite3
import datetime
import logging
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s")
db = sqlite3.connect("ranks.db")
db.execute("""CREATE TABLE IF NOT EXISTS ranks
(date TEXT, keyword TEXT, domain TEXT, position INTEGER)""")
def record(keyword, domain, gl="us"):
try:
pos = get_rank(keyword, domain, gl)
except requests.RequestException as e:
logging.warning("skipped %r: %s", keyword, e)
return "skipped" # no row written — retry on the next run
db.execute("INSERT INTO ranks VALUES (?,?,?,?)",
(datetime.date.today().isoformat(), keyword, domain, pos))
db.commit()
return pos
3. Track a list daily
KEYWORDS = ["project management software", "best crm", "time tracking app"]
DOMAIN = "yoursite.com"
for kw in KEYWORDS:
pos = record(kw, DOMAIN)
if pos == "skipped":
print(f"{kw}: skipped (request failed, no row written)")
else:
print(f"{kw}: {f'#{pos}' if pos else 'not in top results'}")
Example output:
project management software: #4
best crm: not in top results
time tracking app: #12
And when a request fails, the log shows the skip instead of polluting the database:
2026-06-10 07:00:18 WARNING skipped 'best crm': HTTPSConnectionPool(host='api.serp.cheap', port=443): Read timed out. (read timeout=30)
Schedule it with cron (0 7 * * *) or GitHub Actions for a daily snapshot.
4. The cost
Each keyword check is one request. 100 keywords/day ≈ $0.06/day fresh — and repeated checks hit the cache at half price. Compare that to hosted rank trackers charging $20–$100+/month.
Next
Try serp.cheap
Track 100 keywords daily for about $0.06/day. 100 free searches on signup — no card required; pay-as-you-go from $0.75/1k after that, and credits stay valid for 12 months.
- Get your free API key →
- Prefer paying by card? serp.cheap is also on RapidAPI (
$0.90/1k) and Apify ($1.50/1k).