Tutorial

How to Build a Rank Tracker on Cloudflare Workers

Build a serverless rank tracker using Cloudflare Workers and Scavio API. Daily keyword tracking with zero infrastructure.

Rank trackers need to run daily but do not justify a full server. This tutorial builds a rank tracker on Cloudflare Workers that runs on a cron schedule, checks your keyword positions via the Scavio API, stores results in KV, and exposes a dashboard endpoint. Zero infrastructure, free Workers tier, $0.005/keyword.

Prerequisites

  • Node.js 18+
  • Wrangler CLI (npm install -g wrangler)
  • A Scavio API key from scavio.dev
  • Cloudflare account (free tier works)

Walkthrough

Step 1: Set up the Worker with scheduled trigger

Create a Cloudflare Worker that runs daily via cron trigger.

JavaScript
// wrangler.toml config:
// name = "rank-tracker"
// [triggers]
// crons = ["0 6 * * *"]  # Daily at 6 AM UTC
// [[kv_namespaces]]
// binding = "RANKINGS"
// id = "your-kv-namespace-id"

export default {
  async scheduled(event, env, ctx) {
    const keywords = ['search api python', 'mcp search tool', 'serp api alternative', 'web search api pricing'];
    const domain = 'scavio.dev';
    const results = [];
    for (const kw of keywords) {
      const resp = await fetch('https://api.scavio.dev/api/v1/search', {
        method: 'POST',
        headers: { 'x-api-key': env.SCAVIO_API_KEY, 'Content-Type': 'application/json' },
        body: JSON.stringify({ query: kw, country_code: 'us' }),
      });
      const data = await resp.json();
      const organic = data.organic_results || [];
      const pos = organic.findIndex(r => r.link?.includes(domain));
      results.push({ keyword: kw, position: pos >= 0 ? pos + 1 : null, date: new Date().toISOString().split('T')[0] });
    }
    // Store in KV
    const today = new Date().toISOString().split('T')[0];
    await env.RANKINGS.put(`rankings:${today}`, JSON.stringify(results));
    console.log(`Tracked ${results.length} keywords. Cost: $${(keywords.length * 0.005).toFixed(3)}`);
  },

  async fetch(request, env) {
    // Dashboard endpoint
    const url = new URL(request.url);
    if (url.pathname === '/api/rankings') {
      const today = new Date().toISOString().split('T')[0];
      const data = await env.RANKINGS.get(`rankings:${today}`);
      return new Response(data || '[]', { headers: { 'Content-Type': 'application/json' } });
    }
    return new Response('Rank Tracker. GET /api/rankings for today.');
  }
};

Step 2: Add historical trend storage

Store 30 days of ranking history and calculate trends.

JavaScript
async function getHistory(env, days = 30) {
  const history = [];
  for (let i = 0; i < days; i++) {
    const date = new Date(Date.now() - i * 86400000).toISOString().split('T')[0];
    const data = await env.RANKINGS.get(`rankings:${date}`);
    if (data) {
      history.push({ date, rankings: JSON.parse(data) });
    }
  }
  return history.reverse();
}

async function getTrends(env, keyword) {
  const history = await getHistory(env);
  const positions = [];
  for (const day of history) {
    const entry = day.rankings.find(r => r.keyword === keyword);
    if (entry) {
      positions.push({ date: day.date, position: entry.position });
    }
  }
  if (positions.length < 2) return { trend: 'insufficient_data', positions };
  const latest = positions[positions.length - 1].position;
  const previous = positions[positions.length - 2].position;
  let trend = 'stable';
  if (latest && previous) {
    if (latest < previous) trend = 'improving';
    else if (latest > previous) trend = 'declining';
  } else if (latest && !previous) {
    trend = 'new_ranking';
  } else if (!latest && previous) {
    trend = 'lost_ranking';
  }
  return { trend, positions, latest, previous };
}

// Usage in fetch handler:
// const trends = await getTrends(env, 'search api python');
// return new Response(JSON.stringify(trends));
console.log('Trend tracking configured with 30-day KV history');

Step 3: Deploy and verify

Deploy the Worker and test the ranking endpoint.

JavaScript
// Deploy steps:
// 1. wrangler secret put SCAVIO_API_KEY
// 2. wrangler deploy
// 3. wrangler kv:namespace create RANKINGS

// Test locally:
// wrangler dev
// curl http://localhost:8787/api/rankings

// Test the scheduled trigger:
// wrangler dev --test-scheduled

// Verify in production:
async function verifyDeployment() {
  const resp = await fetch('https://rank-tracker.your-subdomain.workers.dev/api/rankings');
  const data = await resp.json();
  console.log(`Rankings for today: ${data.length} keywords`);
  for (const r of data) {
    const pos = r.position ? `#${r.position}` : 'not found';
    console.log(`  ${r.keyword.padEnd(30)} | ${pos}`);
  }
}

// Cost breakdown:
// Cloudflare Workers: Free tier (100K req/day)
// Scavio API: 4 keywords x $0.005 = $0.020/day
// Monthly: $0.60/month for daily tracking
// vs. Ahrefs: $99/mo, SEMrush: $129/mo
console.log('Deployed. $0.020/day for 4 keywords.');
console.log('Monthly: $0.60 vs Ahrefs $99/mo');

Python Example

Python
import os, requests
SH = {'x-api-key': os.environ['SCAVIO_API_KEY'], 'Content-Type': 'application/json'}

def track(keyword, domain):
    data = requests.post('https://api.scavio.dev/api/v1/search',
        headers=SH, json={'query': keyword, 'country_code': 'us'}, timeout=10).json()
    pos = next((i+1 for i, r in enumerate(data.get('organic_results', [])) if domain in r.get('link', '')), None)
    print(f'{keyword[:30]:30} | {f"#{pos}" if pos else "absent"}')

for kw in ['search api python', 'mcp search tool']:
    track(kw, 'scavio.dev')

JavaScript Example

JavaScript
const SH = { 'x-api-key': process.env.SCAVIO_API_KEY, 'Content-Type': 'application/json' };
for (const kw of ['search api python', 'mcp search tool']) {
  const data = await fetch('https://api.scavio.dev/api/v1/search', {
    method: 'POST', headers: SH,
    body: JSON.stringify({ query: kw, country_code: 'us' })
  }).then(r => r.json());
  const pos = (data.organic_results || []).findIndex(r => r.link?.includes('scavio.dev'));
  console.log(`${kw}: ${pos >= 0 ? '#' + (pos+1) : 'absent'}`);
}

Expected Output

JSON
Tracked 4 keywords. Cost: $0.020

Rankings for today: 4 keywords
  search api python              | #3
  mcp search tool                | #2
  serp api alternative           | #5
  web search api pricing         | #4

Deployed. $0.020/day for 4 keywords.
Monthly: $0.60 vs Ahrefs $99/mo

Related Tutorials

Frequently Asked Questions

Most developers complete this tutorial in 15 to 30 minutes. You will need a Scavio API key (free tier works) and a working Python or JavaScript environment.

Node.js 18+. Wrangler CLI (npm install -g wrangler). A Scavio API key from scavio.dev. Cloudflare account (free tier works). A Scavio API key gives you 250 free credits per month.

Yes. The free tier includes 250 credits per month, which is more than enough to complete this tutorial and prototype a working solution.

Scavio has a native LangChain package (langchain-scavio), an MCP server, and a plain REST API that works with any HTTP client. This tutorial uses the raw REST API, but you can adapt to your framework of choice.

Start Building

Build a serverless rank tracker using Cloudflare Workers and Scavio API. Daily keyword tracking with zero infrastructure.