How to Detect X (Twitter) Bots: A Practical, Data-Backed Method
A practitioner method for Twitter bot detection: the real signals (views-to-likes ratio, account age, posting cadence, follower pattern, amplification), runnable API code to pull each one, and a scoring rubric you own.

A views-to-likes ratio above 5,000:1 is the cleanest single tell of purchased amplification on X. Normal organic posts run between 50:1 and 500:1. When a post has hundreds of thousands of views and a few dozen likes, the views were bought and the accounts delivering them scroll without engaging. That one ratio does more practical work than most of the bot-detection literature, and almost nobody writes it down.
This guide is the part of bot detection that the academic papers and the black-box checkers skip: the actual signals, why each one works, the API call to pull it, and a scoring function that combines them into a verdict you own. No machine-learning training, no GPU, no labeled dataset. You pull a handful of fields, score them against thresholds grounded in real engagement data, and count how many converge.
If you have noticed your follower count or your engagement numbers looking off, you are not imagining it. One developer summed up the experience in a thread that has been quietly circulating: "Came back today to hundreds of new bot followers all sending me bizarre messages. Twitter is overwhelmingly spam and bots and false engagement." The tools people reach for next mostly disappoint. So this is the method instead.
What you will get from this guide
Every result on the first page for "twitter bot detection" answers one question: does this single account look like a bot, yes or no? None of them answer the developer's question: what are the signals, how do I pull each one programmatically, and how do I combine them at scale? That gap is what this guide fills. Here is how the practitioner method compares to what currently ranks.
| Dimension | Academic ML tools | Free UI checkers | Academic papers | Signal-based method |
|---|---|---|---|---|
| Signal transparency | No (score only) | Partial | Yes (theory) | Yes, with thresholds |
| Views-to-likes ratio | No | No | No | Yes (first-party data) |
| API / programmatic access | Limited | No | No | Yes (core of it) |
| Code examples | No | No | No | Yes (Python) |
| Bulk analysis | No | One at a time | Offline batch | Yes |
| Free to read and run | Key required | Yes (UI only) | Often paywalled | Yes |
The signal-based approach trades the convenience of a one-click badge for something a developer actually needs: composable signals you wire into your own pipeline, with interpretable output. When an account scores high, you can see exactly which signals fired and decide whether you agree. That transparency is the whole point.
To run any of the code below you need read access to tweet and profile data. This guide uses the GetXAPI endpoints because they return account age, follower counts, and post-level view and like counts as cleaned JSON from a single Bearer-authenticated call, which keeps the examples short. The same logic works against the official X API v2 with field expansions, or against any source that returns the same fields. The detection method is independent of where the data comes from. The best Twitter API for scraping breakdown compares the read-access options if you have not picked one yet.
Why bot detection is harder than it looks
Bot detection has a reputation for being easy. Look at the profile, see if it has a default avatar, done. That stopped working years ago. The accounts worth detecting now use real stolen photos, aged or purchased handles, and AI-generated text that reads like a person wrote it. The visual tells are gone. What is left is quantitative.
A commenter on r/Twitter put the difficulty plainly in a thread that is still the top community discussion on the topic: "Detecting bots is not as easy as some think. The bot traffic on a server does not announce itself as a bot. Most can pass a captcha and those that can't, well, humans fail captchas too." That is the correct mental model. Captchas, default-avatar checks, and bio keyword scans are noise floors that sophisticated automation walks straight through.
There is a second-order problem. X runs its own spam purges, and they work on the crude end. A developer on r/digital_marketing described the effect on the supply side: "since the last X update bots have been declining by A LOT. Most SMM panels you can get 1000 followers for, but 999 of them will vanish the next day. These are useless now for X following." Crude bots get cleared, which pushes the market toward sophisticated engagement manipulation that survives the native filter. The bots that remain are the ones your detection has to catch, and they are specifically the ones that pass naive checks.
https://www.reddit.com/r/Twitter/comments/17qazr9/is_there_any_way_to_detect_bots_these_days/
X's own filter also overcorrects. A thread on r/twitterhelp captured the complaint: "A lot of people on X/Twitter are starting to feel that the anti-bot systems have gone too far. blunt and overly aggressive. it also prevents X [legitimate users]." So the native system simultaneously catches real people and misses real bots. That is the case for owning your own detection logic: you set the thresholds, you see why an account fired, and you can tune for your tolerance rather than inheriting someone else's.
The takeaway that shapes the whole method: no single signal is a verdict. Modern bots beat any individual check. They lose to convergence. When account age, follower ratio, posting velocity, and engagement ratio all point the same direction at once, the probability that you are looking at a real human collapses. The rest of this guide is about computing those signals and stacking them.
A 2023 MIT Sloan Management Review writeup on bot-detector accuracy found that general-purpose machine-learning detectors can perform no better than chance when evaluated outside their training data, which is exactly why the signal-based approach exists: the signals are observable fields, not model weights that decay when tactics change. Research from Stanford Internet Observatory on coordinated inauthentic behavior reaches the same conclusion: behavioral signals (timing, network structure, post velocity) outlast model-based classifiers because tactics evolve faster than training datasets do. X's own platform manipulation policy distinguishes declared automation from inauthentic behavior on exactly these grounds: the bad category is the one that mimics human-like signals, which is what the detection method here targets.
The five signals that identify a Twitter bot
There are five signal families that, taken together, separate automated accounts from humans. Each one is a field or a short computation you can pull from an API. None of them requires a model. The trick is that you compute all five and count how many cross their threshold, because any one of them on its own produces false positives.
Here is the full signal table with the threshold for each and why it works, before we pull them one at a time.
| Signal | Bot-class threshold | Why it works |
|---|---|---|
| Account age vs post count | Under 90 days old AND over 5,000 posts | Automation posts at machine speed; a 60-day-old account with 8,000 posts averages 130+ posts/day |
| Follower-to-following ratio | Following greater than 3x followers | Mass-following to trigger reciprocal follows, no organic audience built |
| Reply velocity | Median reply gap under 30 seconds | Faster than human reading plus typing; sustained across many replies |
| Views-to-likes ratio | Above 5,000:1 on posts | Purchased view amplification: impressions inflated, organic engagement flat |
| Content fingerprint | Repeated identical phrases, AI-vocabulary clustering, high em-dash density | Templated or model-generated output at scale |
A developer on X walked through the manual version of these same tells, the repetitions and the amplification patterns that give bot-farm accounts away when you read enough of them:
https://x.com/Mr_Andrew_Fox/status/1827052764133826568
The point of the rest of this section is to turn that manual read into a computation. We go through each signal, what to pull, and the threshold, then assemble them into the scoring function.
Signal 1: account age and posting cadence
The cheapest signal to compute, and one of the strongest. Pull the account creation date and the total post count. Divide posts by account age in days to get an average daily posting rate. A human power-user might sustain 20 to 50 posts a day. An account that is 60 days old with 8,000 posts is averaging over 130 posts a day, every day, with no breaks. That is automation.
The threshold that flags reliably: account age under 90 days combined with a post count over 5,000. Either alone is fine. A new account with few posts is just new. An old account with many posts is just prolific. The combination, young and hyperactive, is the bot signature, because real accounts do not accumulate thousands of posts in their first weeks.
import requests, os
from datetime import datetime, timezone
GETXAPI_KEY = os.environ["GETXAPI_KEY"]
BASE = "https://api.getxapi.com"
HEADERS = {"Authorization": f"Bearer {GETXAPI_KEY}"}
def get_profile(username):
r = requests.get(
f"{BASE}/twitter/user/info",
params={"userName": username},
headers=HEADERS,
)
r.raise_for_status()
return r.json()
def age_and_cadence(profile):
created = datetime.fromisoformat(
profile["createdAt"].replace("Z", "+00:00")
)
age_days = max((datetime.now(timezone.utc) - created).days, 1)
posts = profile["statusesCount"]
posts_per_day = posts / age_days
young_and_loud = age_days < 90 and posts > 5000
return {
"age_days": age_days,
"posts": posts,
"posts_per_day": round(posts_per_day, 1),
"signal_age_cadence": young_and_loud,
}
profile = get_profile("someaccount")
print(age_and_cadence(profile))
The createdAt and statusesCount fields come back on the profile object with no extra calls, which makes this the first filter to run when you are scoring a large list. It is free in the sense that it costs one profile lookup and rejects a meaningful share of bots before you spend anything on per-post data. The GetXAPI best practices guide covers caching profile responses to avoid redundant calls on large lists.
Signal 2: follower-to-following pattern
Real accounts accumulate followers over time. They rarely follow vastly more accounts than follow them back, because the follow-back rate is never 100 percent and humans lose interest in following thousands of strangers. Bot accounts do the opposite: they mass-follow to trigger reciprocal follows, harvesting an audience without earning one.
The signal is the ratio of following to followers. When following exceeds followers by more than three times, the account is in bot territory. This is exactly the pattern behind a recurring community ask for a follower-filtering tool: an account that follows thousands, is rarely followed back, and barely tweets.
def follower_pattern(profile):
followers = profile["followersCount"]
following = profile["followingCount"]
# guard against divide-by-zero on brand-new accounts
ratio = following / max(followers, 1)
skewed = following > followers * 3 and following > 200
return {
"followers": followers,
"following": following,
"following_to_followers": round(ratio, 2),
"signal_follower_pattern": skewed,
}
The following > 200 guard matters. A new human account that follows 40 people and has 5 followers has a high ratio but is obviously not running a follow-farm. The bot pattern is high ratio at volume. Keep this signal as one input, not a verdict: legitimate new accounts and accounts recovering from a follower purge can both look skewed for a while.
Signal 3: reply velocity
Amplification bots reply fast, because the whole point is to flood a target post with engagement the moment it lands. Humans cannot read a tweet, compose a reply, and post it in under 30 seconds, not consistently and not across many posts. So the median gap between a parent post and the replies to it, computed per replying account, is a behavioral fingerprint.
Pull the replies to a post, take each reply's timestamp, subtract the parent post timestamp, and look at the distribution. A median delta under 30 seconds across a cluster of different accounts is a coordinated amplification signal. One fast reply is nothing. A dozen accounts all replying within seconds of the same post is a network.
from statistics import median
def get_replies(tweet_id, count=100):
r = requests.get(
f"{BASE}/twitter/tweet/replies",
params={"tweetId": tweet_id, "count": count},
headers=HEADERS,
)
r.raise_for_status()
return r.json()["replies"]
def reply_velocity(parent_created_iso, replies):
parent = datetime.fromisoformat(
parent_created_iso.replace("Z", "+00:00")
)
deltas = []
for rep in replies:
ts = datetime.fromisoformat(
rep["createdAt"].replace("Z", "+00:00")
)
deltas.append((ts - parent).total_seconds())
fast = [d for d in deltas if 0 <= d < 30]
median_delta = median(deltas) if deltas else None
return {
"reply_count": len(deltas),
"median_delta_seconds": median_delta,
"fast_reply_share": round(len(fast) / max(len(deltas), 1), 2),
"signal_reply_velocity": median_delta is not None and median_delta < 30,
}
Reply velocity is the signal to reach for when you are auditing amplification on a specific post rather than scoring a single account, because it surfaces the network around the post, not just the post's author. It is more expensive than the metadata signals (it costs a replies call) so run it after the cheap filters have narrowed the field. The twitter API rate limit guide covers backoff strategies for sustained reply-fetch jobs.
Signal 4: the views-to-likes ratio
This is the signal that the academic tools and the UI checkers do not have, because the public views count only arrived on the X API in late 2022, after most of those models were built. It is also the single most useful tell for purchased amplification, which is the category of fraud that survives X's native filter.
The mechanism: when someone buys views, automated accounts deliver impressions by scrolling past the post, but they do not like it, because liking at scale is what gets bot accounts caught. So views climb while likes stay flat, and the ratio between them blows out. Organic posts do not do this. A real post that gets 100,000 views has earned them through engagement, and the likes scale with the views.
The numbers, grounded in aggregated engagement data across a large volume of processed tweets: normal organic posts run a views-to-likes ratio between 50:1 and 500:1. The low end is small accounts with tight, engaged audiences. The high end is large accounts and broadcast-style content. Above 5,000:1, you are looking at inflated impressions with suppressed organic engagement, the statistical signature of view farming. Posts that cross 5,000:1 also show a markedly higher co-occurrence with the other bot indicators in this guide, which is what makes the ratio worth treating as a high-weight signal rather than a curiosity.
def get_user_tweets(username, count=40):
r = requests.get(
f"{BASE}/twitter/user/tweets",
params={"userName": username, "count": count},
headers=HEADERS,
)
r.raise_for_status()
return r.json()["tweets"]
def views_to_likes(tweets):
flagged = []
for t in tweets:
views = t.get("viewCount") or 0
likes = t.get("likeCount") or 0
if views < 1000:
continue # low-view posts are noisy, skip them
ratio = views / max(likes, 1)
if ratio > 5000:
flagged.append({"id": t["id"], "views": views,
"likes": likes, "ratio": round(ratio)})
share = len(flagged) / max(len([t for t in tweets
if (t.get("viewCount") or 0) >= 1000]), 1)
return {
"flagged_posts": flagged,
"flagged_share": round(share, 2),
"signal_views_to_likes": share > 0.3,
}
Two guards keep this honest. Skip posts under 1,000 views, because low-view posts produce wild ratios from rounding and are not informative. And flag the account only when a meaningful share of its posts cross the line, not on a single outlier, since any account can catch one weird viral-but-unliked post. A pattern of high-ratio posts is the signal. To pull view and like counts at scale you need post-level engagement fields, which the tweet-scraping workflow covers in more depth.
Signal 5: the content fingerprint
The last family is linguistic. Templated and model-generated output leaves marks. Three are worth computing because they hold up against sophisticated bots.
First, repeated identical phrases across an account's own posts, or across a cluster of accounts. Bot farms run from templates, so the same exact sentence shows up verbatim more often than a human would ever repeat themselves. Second, AI-vocabulary clustering: a density of the words that large language models overproduce, terms like delve, tapestry, multifaceted, holistic, showing up far above the base rate of normal speech. Third, em-dash density. AI-generated text uses the em-dash at a rate human casual writing does not, so an em-dash count above roughly 2 percent of total characters is a content signal worth scoring.
import re
from collections import Counter
AI_WORDS = {"delve", "tapestry", "multifaceted", "holistic",
"seamless", "robust", "leverage", "elevate"}
def content_fingerprint(tweets):
texts = [t.get("text", "") for t in tweets]
joined = " ".join(texts).lower()
total_chars = max(len(joined), 1)
# em-dash density (U+2014)
em_dashes = joined.count("\u2014")
em_density = em_dashes / total_chars
# AI-vocabulary share
words = re.findall(r"[a-z']+", joined)
ai_hits = sum(1 for w in words if w in AI_WORDS)
ai_density = ai_hits / max(len(words), 1)
# exact-phrase repetition across the account's own posts
norm = [re.sub(r"\s+", " ", t.strip().lower()) for t in texts if t.strip()]
repeats = sum(c - 1 for c in Counter(norm).values() if c > 1)
repeat_share = repeats / max(len(norm), 1)
return {
"em_dash_density": round(em_density, 4),
"ai_word_density": round(ai_density, 4),
"repeat_phrase_share": round(repeat_share, 2),
"signal_content": em_density > 0.02 or ai_density > 0.01
or repeat_share > 0.15,
}
The content fingerprint is the noisiest of the five, because human writing varies and some real accounts genuinely overuse em-dashes or stock phrases. Keep its weight modest in the final score. It is a tiebreaker that pushes a borderline account over the line, not a signal you convict on alone.
Start building with GetXAPI
$0.05 per 1,000 tweets. $0.10 free credits. No credit card required.
Combining the signals into a score you own
Five signals, each returning a boolean and the raw numbers behind it. The detection rule is not "any signal fires equals bot." It is a weighted sum, because the signals are not equally trustworthy. The views-to-likes ratio and the age-cadence combination are high-confidence. The follower pattern and reply velocity are medium. The content fingerprint is low. Weight accordingly, sum, and compare to a threshold.
def bot_score(profile, tweets, replies=None, parent_created=None):
s_age = age_and_cadence(profile)
s_follow = follower_pattern(profile)
s_views = views_to_likes(tweets)
s_content = content_fingerprint(tweets)
score = 0
score += 3 if s_age["signal_age_cadence"] else 0
score += 3 if s_views["signal_views_to_likes"] else 0
score += 2 if s_follow["signal_follower_pattern"] else 0
score += 1 if s_content["signal_content"] else 0
if replies is not None and parent_created is not None:
s_reply = reply_velocity(parent_created, replies)
score += 2 if s_reply["signal_reply_velocity"] else 0
if score >= 6:
verdict = "likely automated"
elif score >= 3:
verdict = "suspicious, review manually"
else:
verdict = "no strong bot signals"
return {"score": score, "verdict": verdict,
"signals": {"age_cadence": s_age, "follower": s_follow,
"views_likes": s_views, "content": s_content}}
The weights are a starting point, not gospel. The right move is to run this against a sample of accounts you can hand-label, then tune the weights and the threshold until the false-positive rate matches your tolerance. A brand-safety team auditing sponsorship deals wants a high bar before flagging. A spam-filtering pipeline can run hotter. The reason to own the logic instead of trusting a black-box badge is exactly this: you get to set the tolerance, and you can always see which signals fired.
Notice what the function returns: not just a score, but every signal's raw numbers. When an account scores 7, you can read off that it is 40 days old with 9,000 posts, follows 12,000 accounts with 300 followers, and has six posts over 5,000:1 views-to-likes. That is an explanation, not a verdict handed down from a model. The transparency is the feature.
Auditing a follower list in bulk
Scoring one account is a demo. The real job is usually auditing a whole follower list, your own or a prospect's, to find what share is automated. That is the use case the community keeps asking for: pull the followers, score each one, report the bot percentage.
The cost-control trick is ordering. The metadata signals (age, cadence, follower ratio) compute from the profile object with no extra calls. The engagement signals (views-to-likes, reply velocity) cost per-post data. So run the cheap signals across the entire list first, then spend per-post calls only on the accounts that already look suspicious. That cuts the cost of auditing a large list by an order of magnitude.
import time
def get_followers_page(username, cursor=None, count=200):
params = {"userName": username, "count": count}
if cursor:
params["cursor"] = cursor
r = requests.get(f"{BASE}/twitter/user/followers",
params=params, headers=HEADERS)
r.raise_for_status()
return r.json()
def audit_followers(username, max_accounts=2000):
results, cursor, seen = [], None, 0
while seen < max_accounts:
page = get_followers_page(username, cursor)
for f in page["followers"]:
# cheap signals only, from the profile object we already have
cheap = 0
cheap += 3 if (f.get("statusesCount", 0) > 5000) else 0
following = f.get("followingCount", 0)
followers = f.get("followersCount", 0)
if following > followers * 3 and following > 200:
cheap += 2
results.append({"userName": f["userName"],
"cheap_score": cheap})
seen += 1
cursor = page.get("next_cursor")
if not cursor or not page.get("has_more"):
break
time.sleep(0.5) # be polite, respect rate limits
suspicious = [r for r in results if r["cheap_score"] >= 2]
bot_share = len(suspicious) / max(len(results), 1)
return {"checked": len(results),
"suspicious_on_cheap_signals": len(suspicious),
"bot_share_estimate": round(bot_share, 2),
"to_deep_check": suspicious}
Run the cheap pass to get a fast bot-share estimate, then feed the to_deep_check list into the full bot_score function with per-post data to confirm. Store results keyed by the account's stable ID so a re-run does not double-count, and so you can diff today's audit against last month's to watch the trend. The follower-export guide has the pagination and async-batching patterns for pulling large lists efficiently, and the rate-limit guide covers backoff for long-running audits.
This is also the answer to the fake-follower problem that real users keep running into. Their own follower list fills with bots, the native tools do not surface which ones, and the question goes unanswered:
Are there any 100% free websites that can identify fake followers? from r/Twitter
The bulk audit above is the programmatic version of that answer. You own the list, you own the scoring, and you can re-run it whenever the follower count does something suspicious. For broader context on the API options that power this kind of audit, the twitter API cost comparison shows what large-list pulls actually cost across providers, and the twitter API tutorial covers the auth setup from scratch.
Detecting amplification networks, not just single accounts
Single-account detection misses the most damaging pattern, which is coordinated amplification: a network of accounts that individually pass casual inspection but together push a narrative or inflate a post. This is the astroturfing case, and it shows up as temporal clustering rather than per-account weirdness.
The detection shifts from "is this account a bot" to "do these accounts behave as a coordinated group." The signals are different. You look at a target post and ask whether the accounts engaging with it share suspicious structure: a cluster of replies within seconds of each other (reply velocity, applied across the responder set), accounts created in the same narrow window, near-identical bios or display-name patterns, and the same posts boosted by the same set of low-follower accounts.
An OSINT investigation walked through exactly this kind of unmasking, tracing hundreds of fake accounts back to the operators behind them by following the coordination patterns rather than judging any single account:
https://www.youtube.com/watch?v=o3YsTIfXGBk
The practical version: pull the engagers on a suspicious post (likers, repliers, retweeters), then score the set for shared structure. Account-creation clustering is the strongest network signal, because a legitimate post's engagers were created across years, while a bot network's engagers were often created in the same weeks.
from collections import Counter
def creation_clustering(engager_profiles):
# bucket account creation by year-month
buckets = Counter()
for p in engager_profiles:
created = datetime.fromisoformat(
p["createdAt"].replace("Z", "+00:00")
)
buckets[(created.year, created.month)] += 1
total = sum(buckets.values())
top_bucket_share = (max(buckets.values()) / total) if total else 0
return {
"engagers": total,
"top_creation_bucket_share": round(top_bucket_share, 2),
"signal_coordinated": top_bucket_share > 0.4,
}
When more than 40 percent of a post's engagers were created in a single month, that is not how organic audiences form. It is how a batch of accounts gets registered. Combine creation clustering with reply velocity across the same set and you have a coordinated-amplification detector, the network-level complement to the per-account score. The X developer community has been asking how to do exactly this filtering inside the API for years, with no built-in answer, which is why you build it yourself. For patterns like this that require reading many accounts' tweet histories at once, the python twitter API tutorial and the tweet history scraping guide cover the data-pull patterns in detail.
Where Botometer and the other tools fit
Botometer is the name that comes up first, and it deserves a fair description. It is an academic project from Indiana University's Observatory on Social Media, which also publishes the BotometerLite dataset and research. You submit a handle, it returns a score from 0 to 5 using its BotometerLite model, where higher means more bot-like. For a quick manual check on one account, it is genuinely useful and worth keeping in your toolkit.
The limits are structural, not a knock on the project. It requires X developer credentials to run, which is a barrier before any analysis starts. It predates the public views API, so it does not use engagement-fraud signals like the views-to-likes ratio at all, which means it is blind to the exact category of manipulation that survives X's filter. And a 2023 MIT Sloan Management Review writeup reported that general-purpose machine-learning bot detectors, trained on one dataset, can perform no better than chance when applied to another. That is the well-documented weakness of any model-based detector: it generalizes poorly to bot tactics that were not in its training data, and bot tactics change constantly.
The free UI checkers like BotBlock and the now-defunct Bot Sentinel occupy the other end. They are fast, they require no setup, and they surface a useful set of metadata and linguistic signals. The limit is the same for all of them: they are one-account-at-a-time UIs with no API, so a developer cannot wire them into a pipeline or audit ten thousand followers. They give you a badge, not a composable signal.
This is the honest framing: academic tools score accounts, and this guide shows the signals behind the score. The signal-based method is not better at producing a single verdict on a single account, where a quick Botometer check is fine. It is better when you need programmatic access, bulk analysis, engagement-fraud signals, and an output you can interpret and tune. Those are developer needs, and no current tool serves them, which is the entire reason this method exists. Practitioners have noticed the gap for years; one summed up the state of the tools bluntly: "They successfully killed off botometer and Bot Sentinel almost never works."
The X developer team announced the public views count field in early 2023, which is what unlocked the engagement-fraud signal class entirely. Before that, third-party detection tools had no access to post-level impression data at all. But the views-to-likes ratio is only one input. Detection is ultimately about behavior over time: how fast an account posts, how it replies, and how its engagement looks against its reach, which is the same point practitioners keep making.
The cheapest Twitter API. Try it free.
$0.05 per 1,000 tweets. $0.10 free credits. No credit card required.
False positives: the caveats that keep this honest
A detection method that flags real humans as bots is worse than no method, because it erodes trust in every flag it raises. The single most important rule in this entire guide is the one stated at the top: no single signal is a verdict. Every threshold here produces false positives in isolation, and the scoring function exists specifically to require convergence before it calls anything automated.
The common false positives, by signal:
- Account age and cadence. A new account run by a prolific human, a journalist live-tweeting an event, or a brand account managed by a social team can all post fast while young. The age-cadence signal catches them. The other signals clear them, because a real prolific account has a normal follower ratio and a normal engagement ratio.
- Follower pattern. New accounts legitimately follow more than they are followed. Accounts recovering from a spam purge show a skewed ratio for weeks. The
following > 200guard helps, but this signal needs backup before it counts. - Reply velocity. Power-users with notifications on genuinely reply within seconds sometimes. One fast reply means nothing; the signal only fires on a sustained sub-30-second median across many replies.
- Views-to-likes ratio. A post can go viral on a platform surface (the For You algorithm, a trending topic) and rack up views faster than likes, briefly pushing the ratio up. The guards (skip sub-1,000-view posts, require a pattern across multiple posts) handle most of this, but a single high-ratio post is never enough.
- Content fingerprint. Plenty of real people overuse stock phrases or em-dashes, and some humans genuinely write like a corporate blog. This is why the content signal carries the lowest weight and serves as a tiebreaker, never a primary.
The discipline that makes this trustworthy: tune against labeled data, report the score and the signals rather than a bare verdict, and set the threshold where your tolerance for false positives sits. A flag is the start of a review, not the end of one. When you hand someone "this account scored 7 because it is 40 days old with 9,000 posts and six posts over 5,000:1 views-to-likes," they can check your work. When a black box hands them "bot, confidence 0.86," they cannot. Interpretability is what keeps the method honest, and it is the thing the academic tools cannot offer.
Putting it together: a complete detection workflow
The full method, start to finish, is short. Pull the profile, pull a sample of recent posts, score the five signals, and read the verdict with the signals attached. For a follower audit, run the cheap signals across the list first, then deep-check the survivors. For an amplification investigation, pull the engagers on a target post and score the set for creation clustering and reply velocity.
def detect(username):
profile = get_profile(username)
tweets = get_user_tweets(username, count=40)
result = bot_score(profile, tweets)
print(f"@{username}: {result['verdict']} (score {result['score']}/10)")
for name, sig in result["signals"].items():
fired = [k for k, v in sig.items() if k.startswith("signal_") and v]
if fired:
print(f" {name}: {sig}")
return result
detect("someaccount")
That is a working Twitter bot detector in well under 100 lines of code, no machine learning, no GPU, no labeled training set. It reads a handful of fields, scores them against thresholds grounded in real engagement data, and returns a verdict you can interpret and tune. Every signal is a field you pull from an API, which is why the method is composable: drop it into a spam filter, a follower-quality dashboard, a brand-safety check, or an astroturf investigation, and it works the same way.
The thing that makes it data-backed rather than heuristic is the views-to-likes ratio, anchored on aggregated engagement data across a large volume of processed tweets: organic posts run 50:1 to 500:1, and the 5,000:1 line is where purchased amplification shows up. No academic paper offers that threshold, because the signal post-dates their datasets, and no UI checker exposes it, because it requires post-level engagement data and a pipeline. That is the practitioner's edge.
Pulling the signals: getting set up
Every code sample above needs read access to profile and post data, including the view and like counts that power the engagement signal. GetXAPI returns all of it as cleaned JSON from a single Bearer-authenticated endpoint, which is what keeps the examples to a few lines each. If you are comparing data providers before picking one, the GetXAPI vs twitter API v2 breakdown covers the feature and cost differences, and the best Twitter API for scraping guide places them in the wider field including Apify's scraper and GetXAPI alternatives.
Step 1: Get an API key. Sign up at /signup. Email and password, no developer-portal application, no credit card. You get $0.10 in free credits, enough to score a few hundred accounts while you validate the method.
Step 2: Pull a profile.
curl "https://api.getxapi.com/twitter/user/info?userName=someaccount" \
-H "Authorization: Bearer YOUR_KEY"
Step 3: Pull recent posts with engagement fields.
curl "https://api.getxapi.com/twitter/user/tweets?userName=someaccount&count=40" \
-H "Authorization: Bearer YOUR_KEY"
From there, the scoring functions in this guide run on the JSON directly. At $0.001 per call, scoring a single account (one profile call plus one posts call) costs a fraction of a cent, and a 2,000-follower cheap-pass audit costs a few cents before any deep-check. The twitter API cost guide breaks down the per-call economics in detail across a range of monthly volumes. The same logic runs against the official X API v2 if you prefer direct platform access; you pay more per call and configure field expansions, but the detection method does not change. For the official API setup path, the how to get a twitter API key guide covers the developer portal process end to end.
For the broader data-access picture, the best Twitter API for scraping comparison covers the read-access options, the tweet-scraping guide goes deeper on pulling post data without getting blocked, and the sentiment-analysis walkthrough shows the content-analysis patterns that feed the linguistic signal. The twitter advanced search operators guide is useful for narrowing the post set you score when you are investigating a specific topic or hashtag rather than a single account.
Get Started
GetXAPI provides $0.10 in free credits at signup with no credit card required, enough to run the profile lookups, timeline fetches, and follower pulls needed to score a sample of accounts before committing. The links below cover the tools and guides used at each stage of the detection workflow.
- GetXAPI: Sign up free ($0.10 credits, no card required)
- Pull profile and post data: GetXAPI Pricing at $0.001 per call
- Read-access options compared: Best Twitter API for scraping
- Pulling post data without blocks: How to scrape tweets in 2026
- Large follower-list pulls: Export Twitter followers via API
- Content-signal analysis: Twitter sentiment analysis in Python
- Backoff for long audits: Twitter API rate-limit guide
- Tweet history at scale: Scrape tweet history via API
- API cost breakdown: Twitter API cost per 1,000 calls
- Send DMs programmatically: Send Twitter DMs via API
- Monitor trending topics: Twitter trends API guide
This guide describes detection signals and thresholds for educational and analytical use. Engagement-ratio figures are grounded in aggregated, anonymized data across a large volume of processed tweets and are presented as signals requiring convergence, not single-trigger verdicts. No specific account is named or characterized as automated. The MIT Sloan reference is a 2023 writeup on machine-learning bot-detector accuracy. Thresholds should be tuned against labeled data for your own use case. Data and method verified June 2026.
Frequently Asked Questions
Check for several converging signals rather than one. The strongest are: an account younger than 90 days with a very high post count, a follower-to-following ratio skewed heavily toward following (following greater than 3x followers), reply timestamps clustering under 30 seconds apart, no likes anywhere in the tweet history, a bio with Telegram or WhatsApp links, and a views-to-likes ratio above 5,000:1 on posts. One signal alone is not a verdict. Two or more present at the same time is a strong indicator. The method in this guide scores each signal and sums them, so a single false positive never decides the call.
Botometer X is still live and useful for a quick manual score on a single account, but it has two limits for a developer pipeline. It requires X developer credentials to run, and it operates on a model that predates the public views API, so it does not use engagement-fraud signals like the views-to-likes ratio. A 2023 MIT Sloan writeup also reported that general-purpose machine-learning bot detectors can perform no better than chance when applied outside their training data. Botometer is a fine spot check. It is not a composable signal source you can wire into your own scoring.
A bot detection score is a single number, often on a 0 to 10 or 0 to 5 scale, that represents how likely an account is automated. It is calculated by combining weighted signals: metadata signals such as account age and follower ratio, behavioral signals such as posting speed and reply timing, and engagement signals such as the views-to-likes ratio. Each signal contributes points, and the total reflects how many signals converge. No single signal produces the verdict, which is what keeps the false-positive rate low.
Automated accounts often reply with a median gap under 30 seconds from the target post. Human reading and typing speed makes consistent sub-30-second replies statistically implausible at scale, especially across many posts. A cluster of replies from different accounts all timestamped within seconds of one tweet is a strong amplification signal. You compute this by pulling the reply timestamps and the parent post timestamp and taking the median delta.
X policy distinguishes declared automated accounts, which follow the rules, label themselves, and post disclosed automated content, from inauthentic bots, which impersonate humans, run coordinated inauthentic behavior, or amplify content deceptively. Detection targets the second group: accounts that behave like bots but present as people. A self-labeled weather bot or sports-score bot is not what these signals are hunting. The signals are tuned to catch accounts pretending to be human.
Pull the follower list with a paginated API call, then score each account against the cheapest signals first: account age and follower-to-following ratio compute from profile fields alone, so you can filter the whole list before spending calls on per-post engagement data. Score the survivors against the views-to-likes ratio and reply velocity. Rate-limit the loop and store results keyed by account ID so the job is idempotent. The follower-audit section of this guide has the loop.
Normal organic posts typically show a views-to-likes ratio between 50:1 and 500:1, depending on account size and content type. Smaller accounts with engaged audiences sit nearer 50:1, large accounts and news-style posts run higher. A ratio above 5,000:1 is a strong indicator of purchased view amplification or view farming: the impressions are inflated by automated accounts that scroll but do not engage, so likes stay flat while views balloon. The ratio is a post-level signal, so you can flag specific tweets, not just whole accounts.
Yes. The data you need is exposed as plain fields: account creation date, follower and following counts, tweet frequency, and on tweet objects the view and like counts. You pull these fields and score them against threshold rules. No machine learning is required. The X API v2 exposes most of this, and third-party data APIs like GetXAPI return the same fields in cleaned JSON from a single endpoint, which is what the code in this guide uses.
A ratio where following count significantly exceeds followers is a known bot signal, with following greater than three times followers as a common threshold. Real accounts accumulate followers over time and rarely follow far more accounts than follow them back. Bot accounts often follow aggressively to trigger reciprocal follows without building an organic audience. Treat this as one signal among several. Plenty of legitimate new accounts also follow more than they are followed, which is why the ratio only counts when it stacks with other signals.
Yes, in two ways. First, fake followers suppress your engagement rate, because bots do not like, reply, or retweet, so a large fake base drags down the ratio that advertisers and partners use to judge an account. Second, when X runs spam purges, fake followers get removed in bulk, which produces visible follower-count drops that signal purchased followers to anyone auditing the account later. Auditing your own follower list with the method here lets you find the problem before someone else does.
Yes. Signal-based detection with threshold rules does not require training a model. You pull account age, follower ratio, posting velocity, and engagement ratio via an API, score each field against empirically grounded thresholds, and count how many signals converge. A Python script reading from a Twitter data API can implement the whole thing in well under 100 lines. This guide includes the scoring function in full.
Check out similar blogs
More guides on the Twitter/X API, scraping, and pricing.







