Proof of Bluesky's Huge Bot Farms

Because I'm lazy, and don't know Python well enough, I had Kimi write the below blog post and scripts:

Inside Bluesky's Largest Bot Farm: How 8,500 Fake Accounts Are Boosting the Same Political Targets

What started as a hunch turned into a data-driven investigation that uncovered what appears to be the largest coordinated bot operation on Bluesky to date. Using nothing more than the platform's public firehose, some Python scripts we wrote together, and a healthy dose of skepticism, we found a network of approximately 8,500 automated accounts artificially inflating engagement for a specific set of political and media accounts. Here's how it happened, what the data shows, and why it matters for anyone who cares about authentic discourse online.

The Hunch

If you spend enough time on Bluesky, you start to notice patterns. We'd been looking at replies, likes, and reposts on posts from large accounts and something felt off. A surprising number of accounts engaging with these posts had between zero and twenty total posts across years of activity. Their heatmaps—visualizations of posting frequency—looked robotic. But when we checked them one by one, they weren't obviously spammy. They had profile pictures. They spread their engagement across many different accounts rather than concentrating on one target. They didn't reply instantly. In other words, they were designed to look human.

These are the hallmarks of a sophisticated bot farm. Unlike the crude spam bots of old that shout "CHECK MY LINK" in all caps, modern engagement farms are built to evade detection. They limit themselves to 300–400 likes per day. They vary their timing. They sprinkle likes across dozens of accounts so no single target looks suspicious. They even fake circadian rhythms, posting at seemingly random hours to simulate sleep schedules.

But sophisticated doesn't mean invisible. It just means you need better tools than eyeballing individual accounts.

The Data

To test our theory, we collected two hours of raw data from Bluesky's Jetstream firehose—a public feed that broadcasts every like, repost, reply, and post on the network in real time. Two hours may not sound like much, but on a platform as active as Bluesky, that's millions of events. We wrote a series of Python scripts to analyze this data, looking not at what bots say (content analysis is complex and error-prone) but at who they engage with and how their behavior clusters.

The key insight came from network analysis. Instead of asking "Is this account a bot?"—which is hard to answer definitively—we asked a different question: "Which accounts are being liked by the same group of users, over and over, in identical patterns?" If hundreds of different accounts all have the exact same five favorite targets, that's not organic popularity. That's a shared to-do list.

The Findings

The results were staggering.

The Like Farm: Approximately 8,515 automated accounts are engaged in coordinated liking. These accounts all draw from the same master target list, boosting the same set of political commentators, journalists, media outlets, and celebrities.

The Repost Farm: A separate but overlapping operation of roughly 1,700+ accounts performs the same function with reposts. Reposts are particularly valuable for manipulation because they push content into followers' feeds, amplifying reach beyond simple engagement metrics.

The Targets: The top boosted accounts include prominent progressive and anti-Trump voices such as Aaron Rupar (@atrupar.com), Mueller She Wrote (@muellershewrote.com), MeidasTouch (@meidastouch.com), Robert Reich (@rbreich.bsky.social), Mark Hamill (@markhamillofficial.bsky.social), and George Conway (@gtconway.bsky.social), among dozens of others. The list also includes some unexpected entries—dog accounts, personal blogs, and random users—which likely serve as camouflage to break up the political clustering pattern.

The Smoking Gun: Identical Numbers

The most damning evidence isn't the existence of bots. Every social platform has bots. The damning evidence is the mathematical impossibility of the engagement patterns.

When we analyzed how many bot accounts each target received likes from, the numbers clustered in ways that organic behavior simply cannot produce. Five unrelated accounts all received likes from exactly 8,515 distinct bot accounts. Others appeared in pairs and triplets with identical counts: two accounts at exactly 1,585 bot likes, two at 1,114, three at 574, three at 559.

In nature, if you randomly sample 120 accounts' engagement metrics, you might see one or two accidental ties. Seeing thirty-plus exact matches is like flipping a coin and getting heads fifty times in a row. It doesn't happen by chance.

What these identical numbers reveal is a quantized subscription model. The bot farm operator appears to sell engagement in fixed packages: "8,500 likes from unique accounts," "1,500 likes," "550 likes." When a client buys a tier, the farm's controller randomly distributes the work across their pool of automated accounts. This randomization is actually sophisticated—it ensures that @markhamillofficial gets 154 reposts from 154 different bot accounts, making it harder to spot that they're all coming from the same farm.

But while the farm randomizes which bots hit which targets, it didn't randomize which targets share the same bot pool. That's the fatal flaw. When you step back and look at the network, the same 8,500-account pool is boosting the same group of targets. The randomness is only skin-deep.

Why This Matters

For the average Bluesky user, this might seem like inside baseball. Who cares if some political accounts have fake likes?

You should care, for several reasons.

First, algorithmic manipulation. Bluesky's feeds, like any social platform, use engagement signals to determine what content gets shown to whom. When thousands of bots artificially inflate likes and reposts, they're not just padding egos—they're gaming the algorithms that decide what goes viral. Fake engagement pushes real content down and manufactured content up.

Second, social proof. Humans are herd animals. When we see a post with thousands of likes, we assume it's important. Bot farms exploit this psychological shortcut to create the illusion of consensus around specific political narratives. A post that looks popular might just be expensive.

Third, platform integrity. Bluesky is built on AT Protocol, which promises a more open and transparent social web. But openness cuts both ways. If the platform becomes saturated with undetectable engagement farms, the "authentic" alternative to Twitter becomes just as manipulated, only with better operational security.

The Methodology (For the Curious)

For those wondering how this was detected: we used Bluesky's public Jetstream firehose to collect raw JSON data of all network activity. We then wrote scripts that built "engagement profiles" for every account—essentially, a ranked list of each account's favorite targets. By reversing this map (asking "which accounts share the same favorites?"), the scripts identified "network targets"—accounts that appeared in the top-five lists of hundreds or thousands of other accounts simultaneously.

The bots were identified not by their content, their profile pictures, or their bios, but by their behavioral correlation. When 8,500 accounts all independently decide that the same five political commentators are their absolute favorite accounts on the entire platform, that's not taste. That's a script.

What Happens Next

We've shared these findings with Bluesky's moderation team. They have tools we don't—access to IP logs, account registration patterns, and the ability to suspend coordinated networks at scale. Whether they act on it remains to be seen.

For everyday users, the takeaway is simple: engagement metrics are not truth. A post with 10,000 likes isn't necessarily 10,000 times more important than a post with 100. It might just have a better bot budget.

The internet promised to democratize speech. What it actually democratized was the ability to fake popularity. And as this investigation shows, the fakes are getting very, very good at looking real.

END OF AI BLOG POST

Below is the Python script Kimi wrote that analyzes firehose data and creates a CSV that you can analyze, I suggest you sort by total_network_likers.

import json
import csv
import statistics
from datetime import datetime, timezone
from collections import defaultdict, Counter

# ==================== CONFIG ====================
FILE_PATH = r"C:\Users\username\Desktop\bluesky_data_clean.jsonl" # <-- CHANGE THIS
CSV_PATH = FILE_PATH.replace(".jsonl", "_repost_bot_network_analysis.csv")
COLLECTION_WINDOW_MIN = 120
MIN_REPOSTS = 20
TOP_TARGETS_PER_REPOSTER = 5
NETWORK_THRESHOLD = 5 # Target is "network" if in top 5 of >=5 reposters
TOP_N = 1000
# ===============================================

def parse_ts(ts_str):
if not ts_str:
return None
ts_str = ts_str.replace("Z", "+00:00")
try:
return datetime.fromisoformat(ts_str)
except ValueError:
return None

def extract_author_did(uri):
if not uri or not uri.startswith("at://"):
return None
try:
return uri[5:].split("/")[0]
except:
return None

# Data structures
reposter_targets = defaultdict(Counter)
reposter_created_times = defaultdict(list)
reposter_created_granular = defaultdict(int)

print("Pass 1: Building reposter profiles...")

with open(FILE_PATH, "r", encoding="utf-8", errors="replace") as f:
for line in f:
line = line.strip()
if not line or not line.startswith("{"):
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
continue

if event.get("kind") != "commit":
continue
commit = event.get("commit", {})
if commit.get("collection") != "app.bsky.feed.repost" or commit.get("operation") != "create":
continue

record = commit.get("record", {})
created_at_str = record.get("createdAt")
reposter_did = event.get("did")
post_uri = record.get("subject", {}).get("uri")

if not created_at_str or not reposter_did or not post_uri:
continue

created_at = parse_ts(created_at_str)
if not created_at:
continue

reposter_created_times[reposter_did].append(created_at)

target_did = extract_author_did(post_uri)
if target_did:
reposter_targets[reposter_did][target_did] += 1

time_part = created_at_str.split("T")[1] if "T" in created_at_str else ""
time_part = time_part.replace("Z", "")
if "." not in time_part:
reposter_created_granular[reposter_did] += 1

# --- PASS 2: Build network map ---
target_to_reposters = defaultdict(set)
reposter_top5 = {}

print("Pass 2: Mapping repost bot networks...")

for did, targets in reposter_targets.items():
total = sum(targets.values())
if total < MIN_REPOSTS:
continue

top5 = targets.most_common(TOP_TARGETS_PER_REPOSTER)
reposter_top5[did] = top5

for target_did, count in top5:
target_to_reposters[target_did].add(did)

# Identify "network targets"
network_targets = {t: len(reposters) for t, reposters in target_to_reposters.items()
if len(reposters) >= NETWORK_THRESHOLD}

print(f"Found {len(network_targets)} network targets (reposted by >= {NETWORK_THRESHOLD} reposters in their top 5)")

top_network_targets = sorted(network_targets.items(), key=lambda x: -x[1])[:50]

print("\nTop 20 network targets (most repost bots share these in their top 5):")
print("-" * 70)
for target, count in top_network_targets[:20]:
print(f" {count:>3} reposters → {target}")

# --- PASS 3: Score each reposter ---
print("\nPass 3: Scoring reposters...")

bot_scores = []

for did, top5 in reposter_top5.items():
total_reposts = sum(reposter_targets[did].values())
unique_targets = len(reposter_targets[did])

network_hits = sum(1 for target, _ in top5 if target in network_targets)
network_hit_ratio = network_hits / len(top5) if top5 else 0
total_network_reposters = sum(network_targets.get(t, 0) for t, _ in top5 if t in network_targets)

# Interval analysis
created_times = sorted(reposter_created_times[did])
intervals = []
for i in range(1, len(created_times)):
delta = (created_times[i] - created_times[i-1]).total_seconds()
if delta >= 0:
intervals.append(delta)

interval_stats = {
"mean": 0, "median": 0, "stdev": 0, "cv": 0,
"over_5min": 0, "over_10min": 0, "count": len(intervals)
}

if len(intervals) >= 2:
mean_int = statistics.mean(intervals)
try:
stdev_int = statistics.stdev(intervals)
except:
stdev_int = 0
cv = stdev_int / mean_int if mean_int > 0 else 0

interval_stats = {
"mean": mean_int,
"median": statistics.median(intervals),
"stdev": stdev_int,
"cv": cv,
"over_5min": sum(1 for x in intervals if x > 300) / len(intervals),
"over_10min": sum(1 for x in intervals if x > 600) / len(intervals),
"count": len(intervals)
}

exact_sec_ratio = reposter_created_granular[did] / total_reposts
sustained_rate = total_reposts / COLLECTION_WINDOW_MIN

# Scoring
score = 0

if network_hits >= 4:
score += 50
elif network_hits >= 3:
score += 35
elif network_hits >= 2:
score += 20
elif network_hits >= 1:
score += 5

target_diversity = unique_targets / total_reposts
if target_diversity < 0.15:
score += 10

if interval_stats["count"] >= 10:
if interval_stats["over_5min"] < 0.03:
score += 15
if interval_stats["cv"] < 0.6 and interval_stats["mean"] > 15:
score += 10
if (interval_stats["over_5min"] < 0.05 and
interval_stats["over_10min"] == 0 and
interval_stats["cv"] < 1.0):
score += 10

if exact_sec_ratio > 0.95 and total_reposts >= 20:
score += 15

if sustained_rate > 8:
score += 10
elif sustained_rate > 5:
score += 5

bot_scores.append({
"did": did,
"count": total_reposts,
"unique_targets": unique_targets,
"network_hits": network_hits,
"network_hit_ratio": network_hit_ratio,
"total_network_reposters": total_network_reposters,
"target_diversity": target_diversity,
"sustained_rate": sustained_rate,
"exact_sec_pct": exact_sec_ratio * 100,
"interval_mean": interval_stats["mean"],
"interval_median": interval_stats["median"],
"interval_cv": interval_stats["cv"],
"pct_over_5min": interval_stats["over_5min"] * 100,
"score": score
})

bot_scores.sort(key=lambda x: (-x["score"], -x["count"]))

# Print results
print("\n" + "=" * 120)
print("TOP 50 MOST SUSPICIOUS REPOSTERS (network analysis: bots sharing top targets)")
print("=" * 120)
print(f"{'Rank':<5} {'Score':<6} {'Reposts':<8} {'NetHits':<8} {'NetRatio':<9} {'UniqTgt':<8} {'Rate':<6} {'>5min%':<7} {'CV':<6} {'DID'}")
print("-" * 120)

for rank, entry in enumerate(bot_scores[:50], 1):
print(f"{rank:<5} {entry['score']:<6} {entry['count']:<8} "
f"{entry['network_hits']:<8} {entry['network_hit_ratio']*100:<9.1f} "
f"{entry['unique_targets']:<8} {entry['sustained_rate']:<6.1f} "
f"{entry['pct_over_5min']:<7.1f} {entry['interval_cv']:<6.2f} "
f"{entry['did'][:40]}")

flagged = len([b for b in bot_scores if b["score"] > 0])
print(f"\n{'='*120}")
print(f"Analyzed {len(reposter_top5):,} reposters with >= {MIN_REPOSTS} reposts.")
print(f"Found {len(network_targets)} network targets.")
print(f"Flagged {flagged} accounts with score > 0.")

# Write CSV
print(f"\nWriting CSV to: {CSV_PATH}")

with open(CSV_PATH, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.writer(csvfile)
writer.writerow([
"rank", "bot_did", "bot_score", "repost_count", "unique_targets",
"network_hits_in_top5", "network_hit_ratio", "total_network_reposters",
"target_diversity", "sustained_rate_per_min", "exact_sec_pct",
"interval_mean_sec", "interval_median_sec", "interval_cv", "pct_over_5min",
"target_did", "target_repost_count", "target_rank", "is_network_target"
])

for rank, entry in enumerate(bot_scores[:TOP_N], 1):
if entry["score"] == 0:
break

did = entry["did"]
targets = reposter_targets.get(did, Counter())
top_targets = targets.most_common(TOP_TARGETS_PER_REPOSTER)

if not top_targets:
writer.writerow([
rank, did, entry["score"], entry["count"], entry["unique_targets"],
entry["network_hits"], round(entry["network_hit_ratio"], 3),
entry["total_network_reposters"], round(entry["target_diversity"], 3),
round(entry["sustained_rate"], 2), round(entry["exact_sec_pct"], 1),
round(entry["interval_mean"], 1), round(entry["interval_median"], 1),
round(entry["interval_cv"], 3), round(entry["pct_over_5min"], 1),
"", "", "", ""
])
continue

for t_rank, (target_did, t_count) in enumerate(top_targets, 1):
is_net = "yes" if target_did in network_targets else "no"
writer.writerow([
rank, did, entry["score"], entry["count"], entry["unique_targets"],
entry["network_hits"], round(entry["network_hit_ratio"], 3),
entry["total_network_reposters"], round(entry["target_diversity"], 3),
round(entry["sustained_rate"], 2), round(entry["exact_sec_pct"], 1),
round(entry["interval_mean"], 1), round(entry["interval_median"], 1),
round(entry["interval_cv"], 3), round(entry["pct_over_5min"], 1),
target_did, t_count, t_rank, is_net
])

print(f"Done. CSV saved to: {CSV_PATH}")

Below is the script that will run in Spyder which lets you download JSON data from the Jetstream.

import asyncio
import json
import websockets
from datetime import datetime, timezone

# Patch the event loop so asyncio.run() works inside Spyder
import nest_asyncio
nest_asyncio.apply()

# ==================== CONFIG ====================
FILE_PATH = r"C:\Users\username\Desktop\bluesky_data_clean.jsonl" # <-- CHANGE THIS
MINUTES = 120
# ===============================================

async def collect():
cursor = int(datetime.now(timezone.utc).timestamp() * 1_000_000)
url = (
f"wss://jetstream2.us-east.bsky.network/subscribe"
f"?wantedCollections=app.bsky.feed.post"
f"&wantedCollections=app.bsky.feed.like"
f"&wantedCollections=app.bsky.feed.repost"
f"&cursor={cursor}"
)

start = datetime.now(timezone.utc)
count = 0

async with websockets.connect(url) as ws:
with open(FILE_PATH, "a", encoding="utf-8") as f:
async for msg in ws:
f.write(msg + "\n")
count += 1
if count % 500 == 0:
mins = (datetime.now(timezone.utc) - start).total_seconds() / 60
print(f"{count} events, {mins:.1f}m")
if (datetime.now(timezone.utc) - start).total_seconds() > MINUTES * 60:
break

print(f"Done: {count} events")

asyncio.run(collect())