Skip to main content
← Back to blog
by Engineering
bot-detectionanalyticstutorial

Build a Twitter Bot Monitor: Detect Fake Followers and Bot Activity

Why Bot Detection Matters

Fake followers are everywhere on X/Twitter. From inflated influencer metrics to coordinated bot campaigns pushing token scams, distinguishing real users from bots is critical for anyone working in crypto intelligence. Whether you're auditing a KOL before partnering, verifying community health for a DAO, or cleaning your own follower list, automated bot detection saves hours of manual review.

In this guide, we'll build a bot detection system using XCROP API that fetches an account's followers and scores each one for bot likelihood using proven heuristics. For the full conceptual background — engagement benchmarks, bot-network patterns, and the manual checks these heuristics are based on — see [How to Detect Fake Followers](/blog/detect-fake-followers-twitter).

The Bot Detection Heuristics

Bot accounts share common patterns that are surprisingly consistent:

1. Default or missing profile picture — Bots often skip customization
2. Low tweet count — Many bots tweet rarely or only retweet
3. Suspicious following ratios — Following thousands but few followers back
4. Creation date clustering — Bots are often created in batches on the same day
5. No bio or generic bio — Real users tend to personalize their profiles
6. Username patterns — Random alphanumeric strings or sequential numbers

Let's turn these into a scoring system.

Step 1: Fetch Followers

First, grab an account's followers using the XCROP API:

JAVASCRIPT
const API_KEY = process.env.XCROP_API_KEY;
const BASE_URL = "https://xcrop.io/api/v2";

async function fetchFollowers(username, pages = 5) {
  const followers = [];
  let cursor = null;

  for (let i = 0; i < pages; i++) {
    const params = new URLSearchParams({ count: "20" });
    if (cursor) params.set("cursor", cursor);

    const res = await fetch(
      BASE_URL + "/users/" + username + "/followers?" + params,
      { headers: { "Authorization": "Bearer " + API_KEY } }
    );

    const json = await res.json();
    if (!json.data) break;

    followers.push(...json.data);
    cursor = json.meta?.next_cursor;
    if (!cursor) break;
  }

  return followers;
}

This fetches up to 100 followers across 5 pages. For larger accounts, increase the page count — but keep credit costs in mind (1 credit per follower result).

Step 2: Build the Bot Scoring Function

Each follower gets a score from 0 (definitely human) to 100 (definitely bot). We combine multiple signals:

JAVASCRIPT
function calculateBotScore(user) {
  let score = 0;
  const reasons = [];

  // 1. Default profile picture
  if (user.profileImage?.includes("default_profile") || !user.profileImage) {
    score += 20;
    reasons.push("default avatar");
  }

  // 2. Very low tweet count
  if (user.tweets !== undefined) {
    if (user.tweets === 0) {
      score += 25;
      reasons.push("zero tweets");
    } else if (user.tweets < 5) {
      score += 15;
      reasons.push("very low tweet count");
    }
  }

  // 3. Suspicious following ratio
  if (user.following && user.followers !== undefined) {
    const ratio = user.following / Math.max(user.followers, 1);
    if (ratio > 50) {
      score += 20;
      reasons.push("extreme following ratio (" + ratio.toFixed(0) + ":1)");
    } else if (ratio > 10) {
      score += 10;
      reasons.push("high following ratio (" + ratio.toFixed(0) + ":1)");
    }
  }

  // 4. No bio
  if (!user.bio || user.bio.trim().length === 0) {
    score += 10;
    reasons.push("no bio");
  }

  // 5. Username looks auto-generated (8+ trailing digits)
  const trailingDigits = user.username?.match(/\d+$/);
  if (trailingDigits && trailingDigits[0].length >= 8) {
    score += 15;
    reasons.push("auto-generated username");
  }

  // 6. Account age vs activity
  if (user.created) {
    const ageInDays = (Date.now() - new Date(user.created).getTime())
      / (1000 * 60 * 60 * 24);
    const tweetsPerDay = (user.tweets || 0) / Math.max(ageInDays, 1);

    if (ageInDays < 30 && user.following > 500) {
      score += 15;
      reasons.push("new account following 500+");
    }

    if (ageInDays > 365 && tweetsPerDay < 0.01) {
      score += 10;
      reasons.push("dormant account");
    }
  }

  return {
    username: user.username,
    score: Math.min(score, 100),
    label: score >= 60 ? "likely_bot" : score >= 30 ? "suspicious" : "likely_human",
    reasons,
  };
}

Step 3: Analyze and Report

Now combine everything into a full audit:

JAVASCRIPT
async function auditFollowers(username) {
  console.log("Auditing followers of @" + username + "...");
  const followers = await fetchFollowers(username, 10);
  console.log("Fetched " + followers.length + " followers");

  const results = followers.map(calculateBotScore);

  // Sort by score descending
  results.sort((a, b) => b.score - a.score);

  // Summary stats
  const bots = results.filter(r => r.label === "likely_bot");
  const suspicious = results.filter(r => r.label === "suspicious");
  const humans = results.filter(r => r.label === "likely_human");

  console.log("\n=== Bot Audit Report ===");
  console.log("Total analyzed: " + results.length);
  console.log("Likely bots:    " + bots.length
    + " (" + ((bots.length / results.length) * 100).toFixed(1) + "%)");
  console.log("Suspicious:     " + suspicious.length
    + " (" + ((suspicious.length / results.length) * 100).toFixed(1) + "%)");
  console.log("Likely human:   " + humans.length
    + " (" + ((humans.length / results.length) * 100).toFixed(1) + "%)");

  // Show top 10 most bot-like
  console.log("\n=== Top 10 Most Bot-Like ===");
  results.slice(0, 10).forEach(r => {
    console.log("@" + r.username + " — score: "
      + r.score + " — " + r.reasons.join(", "));
  });

  return results;
}

// Run it
auditFollowers("target_account");

Step 4: Detect Creation Date Clustering

One of the strongest bot signals is batch creation. If dozens of followers were all created within hours of each other, that's a red flag:

JAVASCRIPT
function detectCreationClusters(followers, windowHours = 24) {
  const sorted = followers
    .filter(f => f.created)
    .sort((a, b) => new Date(a.created) - new Date(b.created));

  const clusters = [];
  let cluster = [sorted[0]];

  for (let i = 1; i < sorted.length; i++) {
    const gap = new Date(sorted[i].created) - new Date(sorted[i - 1].created);
    const gapHours = gap / (1000 * 60 * 60);

    if (gapHours <= windowHours) {
      cluster.push(sorted[i]);
    } else {
      if (cluster.length >= 5) {
        clusters.push({
          date: cluster[0].created,
          count: cluster.length,
          usernames: cluster.map(u => u.username),
        });
      }
      cluster = [sorted[i]];
    }
  }

  if (cluster.length >= 5) {
    clusters.push({
      date: cluster[0].created,
      count: cluster.length,
      usernames: cluster.map(u => u.username),
    });
  }

  return clusters;
}

A cluster of 5+ accounts created within a 24-hour window is a strong indicator of coordinated inauthentic behavior.

Python Alternative

If you prefer Python, here's a condensed version of the scoring logic:

PYTHON
import requests
import os

API_KEY = os.environ["XCROP_API_KEY"]
BASE = "https://xcrop.io/api/v2"

def score_user(user):
    score = 0
    if not user.get("profileImage") or "default" in user.get("profileImage", ""):
        score += 20
    if user.get("tweets", 0) == 0:
        score += 25
    following = user.get("following", 0)
    followers = max(user.get("followers", 0), 1)
    if following / followers > 50:
        score += 20
    if not user.get("bio"):
        score += 10
    return min(score, 100)

def audit(username, pages=5):
    followers = []
    cursor = None
    headers = {"Authorization": "Bearer " + API_KEY}

    for _ in range(pages):
        params = {"count": 20}
        if cursor:
            params["cursor"] = cursor
        r = requests.get(BASE + "/users/" + username + "/followers",
                         headers=headers, params=params)
        data = r.json()
        followers.extend(data.get("data", []))
        cursor = data.get("meta", {}).get("next_cursor")
        if not cursor:
            break

    scored = [(u["username"], score_user(u)) for u in followers]
    scored.sort(key=lambda x: -x[1])

    bots = [s for s in scored if s[1] >= 60]
    print("Likely bots: " + str(len(bots)) + "/" + str(len(scored)))
    for name, s in scored[:10]:
        print("  @" + name + ": " + str(s))

audit("target_account")

Use Cases

Influencer Audit

Before partnering with a crypto KOL, run their followers through the bot detector. An account with 500K followers but 40% bots is far less valuable than one with 50K genuine followers.

Community Health Check

DAOs and token projects can monitor their community accounts. A sudden spike in bot followers might indicate someone is trying to inflate perceived interest — or worse, setting up for a pump-and-dump.

Clean Your Own Following

Run the audit on your own account periodically. Block or report obvious bots to keep your engagement metrics accurate and your feed clean.

Credit Cost Estimation

For a full audit of 200 followers:

200 follower results x 1 credit = 200 credits
Fits comfortably in the Pro plan (2,000,000 monthly credits)

For large-scale audits across multiple accounts, the Pro plan's 2M monthly credits go a long way — and you can add Pay-as-you-go top-up packs (credits never expire) for extra capacity whenever you need it.

Next Steps

This heuristic approach catches the majority of simple bots. To improve accuracy further:

Cross-reference with the batch user lookup endpoint for detailed profile data
Track bot scores over time to detect emerging bot networks
Combine with search to find coordinated tweeting patterns (same text, same links)
Use verified-followers endpoint to quickly identify which followers have paid verification