How to Build a Node.js Keyword Rank Tracker with a SERP API
Rank tracking is one of the most valuable applications of a SERP API. Knowing where your site ranks for target keywords, and how those rankings change over time, is fundamental to any SEO strategy. Commercial rank trackers can cost $50-300 per month. In this tutorial, you will build your own for a fraction of that cost using Node.js and the Serpent API.
What We Are Building
By the end of this tutorial, you will have a working keyword rank tracker that can:
- Check your website's ranking for a list of keywords
- Store ranking history in a local JSON database
- Detect ranking changes (improvements and drops)
- Send alerts when significant changes occur
- Run automatically on a schedule
The entire application is around 150 lines of JavaScript. It uses no external dependencies beyond Node.js built-in modules and the Serpent API.
Prerequisites
- Node.js 18 or later (for native
fetchsupport) - A Serpent API key (sign up at apiserpent.com to get 10 free web searches)
- Basic JavaScript knowledge
Project Setup
Create a new directory and initialize the project:
mkdir rank-tracker
cd rank-tracker
npm init -y
Create a configuration file called config.json with your tracking settings:
{
"apiKey": "YOUR_SERPENT_API_KEY",
"domain": "yourdomain.com",
"keywords": [
"best project management tools",
"project management software",
"team collaboration app",
"kanban board online",
"agile project tracking"
],
"country": "us",
"language": "en",
"checkInterval": "daily"
}
Replace yourdomain.com with the domain you want to track and update the keywords to match your target terms.
Fetching Rankings from the SERP API
The core function queries the Serpent API for each keyword and finds where your domain ranks in the results. Create the main file tracker.js:
import { readFileSync, writeFileSync, existsSync } from 'fs';
const config = JSON.parse(readFileSync('./config.json', 'utf-8'));
const DB_FILE = './rankings.json';
// Fetch ranking for a single keyword
async function getRanking(keyword) {
const params = new URLSearchParams({
q: keyword,
apikey: config.apiKey,
num: 100, // Check top 100 results
gl: config.country,
hl: config.language
});
const response = await fetch(
`https://apiserpent.com/api/search?${params}`
);
if (!response.ok) {
throw new Error(`API error for "${keyword}": ${response.status}`);
}
const data = await response.json();
const results = data.results?.organic || [];
// Find our domain in the results
const match = results.find(r =>
r.url && r.url.includes(config.domain)
);
return {
keyword,
position: match ? match.position : null,
url: match ? match.url : null,
title: match ? match.title : null,
timestamp: new Date().toISOString()
};
}
// Check all keywords with rate limiting
async function checkAllKeywords() {
console.log(`Checking ${config.keywords.length} keywords...`);
const results = [];
for (const keyword of config.keywords) {
try {
const ranking = await getRanking(keyword);
results.push(ranking);
if (ranking.position) {
console.log(` "${keyword}" => #${ranking.position}`);
} else {
console.log(` "${keyword}" => Not in top 100`);
}
// Small delay between requests to be respectful
await new Promise(r => setTimeout(r, 1000));
} catch (error) {
console.error(` Error checking "${keyword}":`, error.message);
results.push({
keyword,
position: null,
url: null,
title: null,
timestamp: new Date().toISOString(),
error: error.message
});
}
}
return results;
}
This function queries for up to 100 results per keyword, then searches through them to find your domain. If your domain appears, it records the exact position, URL, and page title.
Storing Results Over Time
To track ranking changes, you need to store results historically. We use a simple JSON file as a lightweight database:
// Load existing ranking history
function loadDatabase() {
if (existsSync(DB_FILE)) {
return JSON.parse(readFileSync(DB_FILE, 'utf-8'));
}
return { checks: [] };
}
// Save ranking data
function saveDatabase(db) {
writeFileSync(DB_FILE, JSON.stringify(db, null, 2));
}
// Get the most recent check for a keyword
function getLastRanking(db, keyword) {
for (let i = db.checks.length - 1; i >= 0; i--) {
const check = db.checks[i];
const found = check.results.find(r => r.keyword === keyword);
if (found) return found;
}
return null;
}
Each check creates a timestamped entry with results for all keywords. Over time, this builds a complete ranking history that you can query and analyze.
Detecting Ranking Changes
The most valuable feature of a rank tracker is detecting when rankings change. Here is how to compare current results with previous ones:
// Compare current rankings with previous
function detectChanges(db, currentResults) {
const changes = [];
for (const current of currentResults) {
const previous = getLastRanking(db, current.keyword);
if (!previous) {
// First time tracking this keyword
changes.push({
keyword: current.keyword,
type: 'new',
currentPosition: current.position,
message: current.position
? `First check: ranking #${current.position}`
: `First check: not ranking in top 100`
});
continue;
}
const prevPos = previous.position;
const currPos = current.position;
if (prevPos === currPos) continue; // No change
if (prevPos === null && currPos !== null) {
changes.push({
keyword: current.keyword,
type: 'entered',
currentPosition: currPos,
message: `Entered rankings at #${currPos}`
});
} else if (prevPos !== null && currPos === null) {
changes.push({
keyword: current.keyword,
type: 'dropped',
previousPosition: prevPos,
message: `Dropped out of top 100 (was #${prevPos})`
});
} else if (currPos < prevPos) {
changes.push({
keyword: current.keyword,
type: 'improved',
previousPosition: prevPos,
currentPosition: currPos,
change: prevPos - currPos,
message: `Improved from #${prevPos} to #${currPos} (+${prevPos - currPos})`
});
} else {
changes.push({
keyword: current.keyword,
type: 'declined',
previousPosition: prevPos,
currentPosition: currPos,
change: currPos - prevPos,
message: `Declined from #${prevPos} to #${currPos} (-${currPos - prevPos})`
});
}
}
return changes;
}
Building Alerts
When significant ranking changes occur, you want to know immediately. Here is a simple console alert system that you can extend with email or Slack notifications:
// Display ranking changes
function reportChanges(changes) {
if (changes.length === 0) {
console.log('\nNo ranking changes detected.');
return;
}
console.log(`\n${'='.repeat(50)}`);
console.log('RANKING CHANGES DETECTED');
console.log('='.repeat(50));
for (const change of changes) {
const icon = change.type === 'improved' || change.type === 'entered'
? '[UP]'
: change.type === 'declined' || change.type === 'dropped'
? '[DOWN]'
: '[NEW]';
console.log(`${icon} ${change.keyword}: ${change.message}`);
}
console.log('='.repeat(50));
}
To extend this with email alerts, you could add a function that sends notifications via an SMTP service or a webhook to a Slack channel whenever a keyword drops more than 5 positions.
Automating with Cron Jobs
Put it all together in a main function and run it on a schedule:
// Main execution
async function main() {
console.log(`\nRank Tracker - ${new Date().toLocaleString()}`);
console.log(`Tracking: ${config.domain}`);
console.log('-'.repeat(40));
const db = loadDatabase();
const results = await checkAllKeywords();
const changes = detectChanges(db, results);
// Save the new check
db.checks.push({
timestamp: new Date().toISOString(),
results
});
saveDatabase(db);
// Report changes
reportChanges(changes);
// Summary
const ranked = results.filter(r => r.position !== null);
console.log(`\nSummary: ${ranked.length}/${results.length} keywords ranking`);
if (ranked.length > 0) {
const avg = ranked.reduce((s, r) => s + r.position, 0) / ranked.length;
console.log(`Average position: #${avg.toFixed(1)}`);
}
}
main().catch(console.error);
To run this daily, add a cron job (on Linux/Mac):
# Run every day at 8 AM
0 8 * * * cd /path/to/rank-tracker && node tracker.js >> tracker.log 2>&1
Or on a server, use a process manager like PM2 with its built-in cron support:
pm2 start tracker.js --cron "0 8 * * *" --no-autorestart
Next Steps
You now have a functional keyword rank tracker. Here are some ways to extend it:
- Web dashboard - Build a frontend to visualize ranking trends with charts. See our guide on automating SEO reports for a complete walkthrough.
- Competitor tracking - Track multiple domains to compare your rankings against competitors. Read more in our competitor analysis tool guide.
- SERP feature tracking - Track whether your site appears in featured snippets, People Also Ask, or other SERP features.
- Database upgrade - Replace the JSON file with SQLite or PostgreSQL for better performance with large datasets.
- Email reports - Send weekly summary reports with ranking trends and notable changes.
Cost estimate: Tracking 50 keywords daily uses about 1,500 API calls per month. With Serpent API's pay-as-you-go pricing from $0.03/1K SERP API calls (Scale tier), that costs about $0.05 per month. Compare that to $50-300/month for a commercial rank tracker.
FAQ
How do I build a keyword rank tracker with a SERP API?
Five pieces, in this order: a config file holding your API key, target domain and keyword list; a function that queries the SERP API per keyword and finds your domain in the results; a JSON file or database that stores every check; a comparison function that diffs the latest check against the previous one; and a scheduler that runs the whole thing daily.
That is around 150 lines of JavaScript with no npm dependencies at all — Node's built-in modules and the global fetch cover everything. Build it in that order and each piece is testable on its own before the next one exists.
What does it cost to build a custom rank tracker vs use a commercial one?
Tracking 50 keywords daily is about 1,500 API calls a month. At the default pay-as-you-go rate of $0.60 per 1,000 web pages that is roughly $0.90 a month; on the $0.03 Scale rate it is about five cents. A commercial rank tracker for the same job runs $50 to $300 a month.
The gap widens as you scale, because you pay per call rather than per seat or per keyword tier. Even 1,000 keywords checked daily — 30,000 calls a month — comes to roughly $18 at the default rate and under a dollar at Scale. What you give up is the dashboard, so budget the build time honestly.
How do I detect ranking changes in my custom tracker?
Compare each keyword's current position against its most recent stored position and classify the result into one of five states.
Mark it new when there is no previous record at all. Mark it entered when the keyword was unranked and now ranks, and dropped when it ranked before and no longer does. A lower position number is an improvement and a higher one is a decline — record the delta in both cases.
Logging the classification rather than just the number is what makes the history useful later. Six months in, the pattern you want to see is whether drops cluster around algorithm updates or around your own publishing schedule.
How do I schedule rank tracking to run automatically?
On Linux or macOS, cron is enough: 0 8 * * * cd /path/to/rank-tracker && node tracker.js >> tracker.log 2>&1 runs the tracker every morning at 8 and appends the output to a log you can read when something looks wrong.
On a server that already runs PM2, use its built-in cron support instead: pm2 start tracker.js --cron "0 8 * * *" --no-autorestart keeps scheduling in the same place as your process management. If you would rather not maintain a machine at all, a GitHub Actions scheduled workflow does the same job for free.
What do I need to build a Node.js rank tracker?
Three things: Node.js 18 or later, so you get fetch natively; a SERP API key, which Serpent API gives you with 10 free web searches on signup; and enough JavaScript to read a for-loop.
No npm packages are needed for the core build — the tutorial uses fs and the global fetch and nothing else. Expect about 30 minutes end to end, after which the obvious extensions are email or Slack alerts, competitor domains, and a web dashboard.
Start Building Your Rank Tracker
Get started with Serpent API today. 10 free web searches included, no credit card required.
Get Your Free API KeyExplore: SERP API · News API · Image Search API · Try in Playground
