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:
Let's turn these into a scoring system.
Step 1: Fetch Followers
First, grab an account's followers using the XCROP API:
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:
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:
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:
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:
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:
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: