I needed to track where my domain ranks for ~100 keywords on Google. Every tool I tried was either too expensive ($50–$130/month), too fragile (the captcha treadmill), or too complex (full SEO suites with 200 features I didn't need).
So I built my own. 30 lines of Python, scheduled with cron, runs every morning. Total cost: $0.90 per month.
This post walks through the architecture, the code, and the math.
The cost ceiling
I gave myself a $1/month budget. The Starter Boost tier I picked is $3 for 10,000 searches, expires one month after purchase. That works out to $0.30 per 1,000 searches — and 3,000 searches is exactly what I needed for 100 keywords × 30 days.
If I needed more, the next tier up is $10 for 20,000 searches with no expiry. But I didn't need more, and the 1-month expiry on the Boost tier was actually useful — it kept me disciplined.
The endpoint
POST https://api.serpbase.dev/google/search
Headers: X-API-Key: <your-key>
Body (JSON):
{
"q": "serp api",
"gl": "us",
"hl": "en",
"num": 10
}
Each call costs 1 credit. If the dispatch fails (upstream timeout, QPS cap, whatever), the credit is automatically refunded. The response includes a request_id you can store for log correlation.
P50 latency is around 1.4 seconds. Not the fastest in the category — Serper is closer to 1.2s — but for a daily batch job, latency doesn't matter.
The code
import requests
import csv
import time
from datetime import datetime
API_KEY = "your-serpbase-key"
ENDPOINT = "https://api.serpbase.dev/google/search"
KEYWORDS = [
"serp api",
"cheap serp api",
"serp api for llm",
# ... your full list of ~100 keywords
]
BRAND_DOMAIN = "yourdomain.com"
def fetch_serp(keyword, gl="us", hl="en"):
r = requests.post(
ENDPOINT,
headers={"X-API-Key": API_KEY},
json={"q": keyword, "gl": gl, "hl": hl, "num": 10},
timeout=10,
)
r.raise_for_status()
return r.json()
def rank_for_keyword(keyword):
data = fetch_serp(keyword)
for i, item in enumerate(data.get("organic", []), 1):
if BRAND_DOMAIN in item.get("link", ""):
return i, item.get("title"), item.get("link")
return None, None, None
def main():
results = []
for kw in KEYWORDS:
rank, title, link = rank_for_keyword(kw)
results.append({
"ts": datetime.utcnow().isoformat(),
"keyword": kw,
"rank": rank,
"title": title,
"link": link,
})
time.sleep(0.2) # don't blow through QPS
with open("rank_log.csv", "a", newline="") as f:
writer = csv.DictWriter(f, fieldnames=results[0].keys())
writer.writerows(results)
if __name__ == "__main__":
main()
That's the whole thing. 30 lines. time.sleep(0.2) between calls gives you ~5 QPS, well under any public QPS cap.
Scheduling it
crontab -e, add:
0 8 * * * /usr/bin/python3 /path/to/serp_rank.py
8 AM UTC, every day. The script appends a row to rank_log.csv per keyword.
Reading the data
import pandas as pd
df = pd.read_csv("rank_log.csv")
df["ts"] = pd.to_datetime(df["ts"])
df.groupby("keyword")["rank"].plot(legend=True)
Or push it to a Google Sheet if you want a dashboard. I just look at the CSV in a Jupyter notebook when I want to investigate a keyword that moved.
The math
| Item | Value |
|---|---|
| Keywords tracked | 100 |
| Days run | 30 |
| Searches / month | 3,000 |
| Cost per 1,000 | $0.30 (Starter Boost) |
| Total / month | $0.90 |
| Cost per keyword per day | $0.0003 |
If I had to use a $50/month subscription for 5,000 searches, I'd be paying ~55x more for the same data, and the unused searches would expire at month end.
What I learned
Three things I didn't expect:
1. The "rank not found" case is more useful than I thought. When my domain drops out of the top 10, that's the signal. Tracking presence is more actionable than tracking position.
2. request_id is the best debugging tool. I log every request_id, and when something looks off (e.g., a sudden rank change), I can look it up in the provider's dashboard and see the upstream Google response.
3. P50 latency doesn't matter for batch. I cared about latency when I was scraping with headless Chrome (8–15 seconds per request). With a JSON API at 1.4s, the difference between 1.2s and 1.4s is invisible when you're running 100 requests sequentially.
When NOT to use this approach
- You need 10,000+ keywords. Use the larger prepaid tier (125,000 searches for $50, $0.40 per 1k) or a real rank tracker.
- You need real-time updates. This runs once a day. For minute-level rank tracking, you need a different architecture.
- You need Google Maps, News, Images, etc. Different endpoints, different credit costs. Same pattern, more code.
- You don't have a server. The script needs to run somewhere. A $5/month VPS works fine.
The single thing I'd add
If I were doing this for a team, I'd add Slack alerts: when rank drops by more than 5 positions day-over-day, post to a channel. The data is there; the alert is what makes it useful.
Cost ceiling vs. the alternatives
Quick sanity check against the alternatives I considered:
| Tool | Cost / month | Setup time | Cost multiplier |
|---|---|---|---|
| Ahrefs | $99 | 0 | ~110x |
| SEMrush | $130 | 0 | ~144x |
| SerpApi (subscription) | $50 (5k searches) | 1h | ~55x, expires |
| My script | $0.90 | 2h | 1x |
The savings are real, but they only matter if you can spend 2 hours setting it up. If your time is worth $200/hour, just buy Ahrefs.
Conclusion
If you're an indie dev or small team with a small keyword list, a $0.90/month rank tracker is a viable alternative to paying for an SEO suite. The code is 30 lines, the math works, and the data is the same as what the expensive tools give you.
I built this on top of a low-cost SERP API I've been using for several projects — SerpBase. There's 100 free searches on signup, no card required: serpbase.dev.
Source: SERP API comparison, updated Apr 23, 2026.
Top comments (0)