DEV Community

Cover image for Found 897 Fake Followers on DEV.to Here's How I Proved It
GnomeMan4201
GnomeMan4201

Posted on

Found 897 Fake Followers on DEV.to Here's How I Proved It

Follows cost $0.90 via human-run extensions

A full technical audit of a coordinated follower inflation network — methodology, findings, and a detection rule simple enough to run in one query.


On May 19, 2026, I published "Found a Coordinated GitHub Follow Botnet" — a piece documenting a coordinated follower inflation network on GitHub. The next day, my DEV.to follower count started climbing.

Fast.

Date New Followers
May 19 75
May 20 288
May 21 447
May 22 399
May 23 311+

From ~600 followers to 3,045 as of May 24 — and still climbing. Not from a viral post. Not from a mention by a big account. The deployment timing closely followed publication of the article.

So I audited every single one.

TL;DR: 897 of my new followers match a coordinated inauthentic behavior pattern — warehoused accounts deployed from a commercial engagement marketplace called upvote.club ($0.90/follow). The mechanism is a Chrome extension with surveillance-grade permissions that dispatches follow tasks to real human operators. I captured the full task protocol live, reverse-engineered the HMAC signing formula, and confirmed the API is fully replayable without a browser. Everything is reproducible from the repo.

The short version of what I found: every account in the audited follower cohort was following exactly one person — me. Not two. Not ten. One. Across all 1,409 accounts, across four separate account creation waves spanning six months, the Following count was uniformly 1. That's not a heuristic suspicion. That's a graph signature. The rest of this post is the full methodology showing how I got there.


The Setup

# Environment
# Pop!_OS, Python 3.12, venv
pip install requests Pillow imagehash
export DEVTO_API_KEY='your_key'
Enter fullscreen mode Exit fullscreen mode

The full codebase lives at github.com/GnomeMan4201/devto-botnet-hunter. Here's the methodology end-to-end.


Step 1: Pull Every Follower

DEV.to's public API exposes your follower list. Paginate through it and store everything:

import requests, time

API_KEY = 'your_devto_api_key'
BASE    = 'https://dev.to/api'

def get_all_followers():
    followers = []
    page = 1
    while True:
        resp = requests.get(
            f'{BASE}/followers/users',
            headers={'api-key': API_KEY},
            params={'page': page, 'per_page': 1000},
        )
        data = resp.json()
        if not data:
            break
        followers.extend(data)
        page += 1
        time.sleep(0.25)
    return followers
Enter fullscreen mode Exit fullscreen mode

Then for each follower, fetch their full profile:

def get_profile(username):
    resp = requests.get(
        f'{BASE}/users/by_username',
        headers={'api-key': API_KEY},
        params={'url': username},
        timeout=15,
    )
    return resp.json() if resp.status_code == 200 else None
Enter fullscreen mode Exit fullscreen mode

Total audited: 1,409 followers.


Step 2: Score Each Account

Note on username patterns: S3 ID analysis reveals the operator runs three username generators simultaneously — firstname_lastname_[random_hex] (458 accounts), short simple handles like mousefilter, johnmaveric (295 accounts), and creative phrase handles like lawn_meower, just_404_fun, nova_123 (156 accounts). All three styles cluster in the same S3 ID range (3.4M–3.94M), confirming they were created in the same waves. The mixed naming is consistent with deliberate obfuscation — varying username style across three distinct patterns reduces detectability against any single regex-based classifier.

Seven heuristic signals, each worth 1 point. Score ≥ 3 = flagged:

import re  # move to top of your script

def score_account(profile):
    score = 0
    reasons = []

    username = profile.get('username', '')
    followers = profile.get('followers_count', 0)
    following = profile.get('following_count', 0)
    articles  = profile.get('public_articles_count', 0)
    bio       = profile.get('summary', '') or ''
    avatar    = profile.get('profile_image', '')

    if re.search(r'_[a-f0-9]{6,}$', username):
        score += 1; reasons.append('hash_username')
    if not bio.strip():
        score += 1; reasons.append('empty_bio')
    if articles == 0:
        score += 1; reasons.append('zero_articles')
    if avatar.endswith('default_profile_image.png'):
        score += 1; reasons.append('default_avatar')
    if following == 1:
        score += 1; reasons.append('following_one')  # Step 3 shows this is 100% across all 1,409 — treated here as one signal among six
    if followers == 0:
        score += 1; reasons.append('zero_followers')

    return score, reasons
Enter fullscreen mode Exit fullscreen mode

Results across 1,409 accounts:

Tier Count
High-confidence coordinated pattern match (score ≥ 3) 897 (63.7%)
Low-confidence / insufficient evidence (score 1–2) 510 (36.2%)
Clear indicators of sustained organic participation (posting, commenting, multi-account follow graph, or profile customization) 0

Every account in the observed cohort scored suspicious to some degree. None showed strong indicators of sustained organic participation such as posting history, meaningful social graph expansion, or long-term engagement activity. That said, heuristic scoring indicates a pattern, not a proven fact — some dormant real users can superficially resemble low-effort inauthentic accounts. Two accounts (@leob, S3 ID 28,704; @bah123, S3 ID 2,707,292) were removed from the flagged list after S3 ID analysis confirmed their creation predates the operator network by years — legitimate dormant accounts swept in by heuristic scoring. What makes this case different is what came next.


Step 3: The Following=1 Discovery

While reviewing the scored data, I checked the Following field distribution across all 1,409 accounts:

from collections import Counter
import csv

with open('devto_bot_audit_full.csv') as f:
    rows = list(csv.DictReader(f))

dist = Counter(r.get('Following', '0') for r in rows)
for val, cnt in sorted(dist.items(), key=lambda x: -x[1]):
    print(f'Following={val}: {cnt} accounts')
Enter fullscreen mode Exit fullscreen mode

Output:

Following=1: 1409 accounts
Enter fullscreen mode Exit fullscreen mode

Because these accounts appeared in my follower list, a following_count of 1 implies their sole outbound follow edge points to this account. The entire audited spike cohort. All 1,409. Following exactly one person: me.

At that point the investigation stopped being heuristic classification and became graph-pattern detection.

The core invariant: every account in the dataset collapses to a single outgoing follow edge. That is the structural fact from which everything else follows.

The statistical argument does not require a baseline distribution. The anomaly is not that Following=1 is rare on DEV.to — it's that 1,409 accounts independently arrived at the same single target, in synchronized waves, with no other activity. Even without assuming a baseline distribution, the observed convergence of 1,409 independently created accounts onto a single outbound follow edge with no other social activity represents an extreme structural anomaly inconsistent with ordinary organic growth patterns.

A real user who follows only one account is plausible. A thousand accounts — each independently created, each with different usernames, different join dates, different avatars — all following exactly one person, with zero other activity? That's not a coincidence. That's consistent with a follower-inflation deployment pattern rather than organic social behavior — pure follower-count inflation with no engagement attached.

This is a candidate-generation filter — not enforcement logic. Matching accounts should be reviewed, not automatically actioned. With that framing clear:

-- Triage filter for coordinated follower investigation
-- Produces candidates for review, not a ban list
-- joined_at threshold is dataset-specific — adjust to your spike window
-- remove entirely to scan full follower base regardless of join date
SELECT username FROM users
WHERE following_count = 1
  AND public_articles_count = 0
  AND comments_count = 0
  AND joined_at >= '2025-11-01'  -- adjust or remove for your context
Enter fullscreen mode Exit fullscreen mode

On large platforms, this query will surface dormant newcomers, abandoned accounts, and legitimate lurkers alongside coordinated accounts — expected false positives at scale. The value is not precision enforcement but rapid candidate generation: every account in this network would appear in that result set, making it an extremely effective first pass for a coordinated-follower investigation.


Step 4: Batch Creation Waves

Join dates cluster in ways that organic growth doesn't. I parsed the JoinedDate field across the flagged accounts:

from collections import Counter

dates = Counter()
for r in rows:
    d = r.get('JoinedDate', '')
    if d:
        dates[d[:6].strip()] += 1

for date, count in sorted(dates.items(), key=lambda x: -x[1])[:10]:
    print(f'{date}: {count} accounts')
Enter fullscreen mode Exit fullscreen mode

Four distinct creation waves:

Wave Period Accounts Notes
November 2025 Nov 13–19, 2025 218 high-confidence / 339 full cohort Dormant 187 days before activation
January 2026 Jan 2026 17 Small batch
April 2026 Apr 2026 92 Mid-size batch
May 2026 May 13–19, 2026 615 Active deployment wave

194 accounts were created on a single day — May 14, 2026. Organic user creation typically disperses over time rather than concentrating nearly 200 accounts onto a single day within a tightly linked cohort. That single-day concentration represents roughly 13% of the entire audited dataset arriving in one 24-hour window.


Step 5: S3 User ID Sequencing

DEV.to avatar URLs route through a CDN proxy that URL-encodes the original S3 path. Decoding them reveals the underlying user ID — a monotonically increasing integer that reflects account creation order:

import urllib.parse, re

def extract_s3_id(avatar_url):
    """
    Input:  https://media2.dev.to/dynamic/image/.../
            https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2F
            uploads%2Fuser%2Fprofile_image%2F3611242%2F...
    Output: 3611242
    """
    decoded = urllib.parse.unquote(avatar_url)
    m = re.search(r'/profile_image/(\d+)/', decoded)
    return int(m.group(1)) if m else None
Enter fullscreen mode Exit fullscreen mode

The November 2025 wave extracted to:

S3 ID range : 3,610,947 → 3,619,885
Span        : 8,938 IDs across 5 days
Gaps        : 0 significant sequence breaks
Enter fullscreen mode Exit fullscreen mode

218 accounts spread across a span of 8,938 sequential IDs with no significant gaps. The pattern is consistent with accounts created in a single continuous run. The May 2026 wave begins around ID ~3,940,000. The ~320,000 ID gap between the two waves over 6 months tracks with DEV.to's organic signup rate, giving this sequencing value as a rough timestamp proxy for future attribution work.


Step 6: The 187-Day Dormancy

The November 2025 cohort (218 high-confidence flagged accounts, S3 IDs 3,610,947–3,619,885) was created November 13–14.

Two numbers matter here: 218 is the high-confidence flagged subset (score ≥ 3); 339 is all audited accounts with a November join date, including the lower-confidence suspicious tier. I checked Following across all 339 — both tiers:

nov_bots = [r for r in rows if r.get('JoinedDate','').startswith('Nov')]
following = Counter(r.get('Following','0') for r in nov_bots)
print(following)
# Counter({'1': 339})
Enter fullscreen mode Exit fullscreen mode

All 339 November accounts in the observed cohort — high-confidence and suspicious tier alike — had Following=1, pointing at me. The behavioral uniformity holds across both scoring tiers.

The evidence is consistent with a warehoused aged-account inventory: accounts from multiple cohorts manufactured in bulk, held dormant to accumulate age, then deployed on demand. Alternative explanations — accounts created speculatively and abandoned, or purchased in bulk from a separate supplier — cannot be ruled out from observable data alone. The warehousing hypothesis is the most parsimonious fit for the observed batch structure and synchronized activation.

Aged accounts are more valuable to follower inflation services because they appear to have existed before the deployment event. A November 2025 account following you in May 2026 looks 6 months old to a casual observer.

The timing is consistent with a deployment event temporally associated with the publication of the botnet article. I don't have access to purchase records or session logs — only DEV.to's internal telemetry could confirm that directly. But the behavioral evidence is consistent with an on-demand fulfillment event: pre-staged inventory activated in response to a specific trigger.


Step 7: Avatar Fingerprinting

Most accounts that never uploaded a custom avatar (821 of 895) ended up with DEV.to's default letter placeholder — a 96×96 colored square. Not useful for fingerprinting. But the remaining 74 accounts used real custom images, and those tell a different story.

from PIL import Image
import imagehash, hashlib
from pathlib import Path

def fingerprint(path):
    img = Image.open(path)
    return {
        'md5':   hashlib.md5(path.read_bytes()).hexdigest(),
        'phash': str(imagehash.phash(img)),
        'mode':  img.mode,
        'size':  img.size,
    }
Enter fullscreen mode Exit fullscreen mode

Three distinct layers of evidence from the image analysis:

Exact duplicates (same MD5 hash): 55 groups across 131 accounts. Different usernames, different join dates, same bytes. Large-scale exact avatar duplication across otherwise unrelated accounts is difficult to explain organically.

Perceptual similarity clusters (pHash distance ≤ 10): 56 clusters. Images that aren't identical but are visually close — same style, same source material, minor encoding differences from re-uploads.

Stylistic provenance: All 74 real illustrations share a consistent aesthetic — black crosshatch/stipple engravings on transparent backgrounds, natural history subjects (insects, fish, bears, mushrooms). Classic 19th century scientific illustration style. Most accounts used default avatars; the custom-avatar subset exhibited repeated reuse patterns and shared artistic provenance pointing to a single asset source. Independent organic users rarely converge on the same narrow set of obscure public-domain engravings across dozens of otherwise unrelated accounts — the convergence here is consistent with shared asset sourcing rather than independent selection.


Step 8: Tracing the Asset Source

The bear engraving was the most distinctive image — used by @machatter1 and @minakshisrivastava001 among others. I converted it to PNG and ran it through Google Lens.

Two source hits:

DepositPhotos — "American Black bear (Ursus americanus), vintage engraving — Vector", uploaded September 12, 2011.

ClipArt ETC (Florida Center for Instructional Technology) — etc.usf.edu/clipart/2100/2134/grizzly-bear_1.htm — a free public domain archive maintained by the University of South Florida, organized by taxonomy: Animals → Mammals → Bears, with equivalent galleries for fish, insects, birds, reptiles, fungi, and marine invertebrates.

Lens also returned the same bear image appearing as a DEV.to profile avatar going back to December 2023 — across multiple unrelated accounts in what appear to be separate campaigns. The same asset library has been in use for at least 2.5 years across multiple deployments.

Visual survey of the 74 illustration avatars confirms subjects drawn from across the ClipArt ETC natural history collection: bear, grizzly bear, fish, mushroom, chameleon, pelican, horse, death's-head hawk moth, axolotl, deer/stag, jellyfish, stink bug, bat, and fly. The avatar set spans multiple ETC galleries rather than a single narrow source category, suggesting deliberate curation of a varied image pool rather than a bulk download. The selection covers 6+ taxonomic categories — bears, fish, insects, fungi, birds, reptiles, marine invertebrates — a breadth inconsistent with incidental or random asset selection.

No paid pack. No proprietary license to protect. Entirely public domain, EXIF metadata stripped, deployed across accounts and campaigns spanning at least 2.5 years.


Step 9: The Marketplace

With the behavioral signatures mapped, the next question is where this service is sold.

A Google search for "buy DEV.to followers" surfaces an active commercial listing at upvote.club — a points-exchange engagement marketplace that explicitly sells DEV.to followers at $0.90 per follow, with 24-hour delivery. The same platform sells GitHub followers, and also sells downvotes on Hacker News and Indie Hackers — extending the service from follower inflation into active content suppression. That cross-platform coverage directly matches the infrastructure pattern in this investigation: separate account pools per platform, coordinated deployment.

The platform operates on a community points model: users register, earn points by completing follow tasks for others, and spend points to receive follows back. Paid tiers let buyers purchase points directly. New accounts receive 13 free points on registration — an incentive structure that encourages bulk account creation.

Live task queue: An authenticated API call to api.upvote.club/api/tasks1/ during active investigation returned 217 available tasks across 14 platforms — with 3 active DEV.to orders in the queue. The cross-platform breakdown at time of capture:

Platform Active Tasks
Quora 52
GitHub 51
LinkedIn 34
Facebook 12
Instagram 10
Substack 10
DEV.to 3
Others 45

DEV.to manipulation was actively in progress at the time of investigation.

On the "real users" question: These accounts are not bots in the traditional sense — they are real human-operated accounts whose owners installed a browser extension and completed one follow task for points. The distinction matters operationally: the behavior is coordinated and inauthentic, but the mechanism is human-mediated rather than fully automated. This is precisely why the behavioral signature (Following=1, zero engagement, zero content) is so uniform — the extension dispatches a single action per task assignment and stops. Real organic users don't behave this way at scale; real users completing paid micro-tasks do.

This model explains every behavioral signature the audit detected:

  • Following=1 — accounts complete one follow task (the target) and stop. Task fulfilled, points spent.
  • 187-day dormancy — the November accounts weren't sitting idle. They were likely earning points by completing follow tasks across the network for six months before being redeemed against this account.
  • Batch creation waves — bulk account registration maximizes free starting points. 218 accounts × 13 free points = 2,834 free points on signup alone.
  • Zero engagement beyond the follow — task completion, not organic interest. The follow is the deliverable.
  • Synchronized activation — a single purchase order pointing all available inventory at one target simultaneously.

At the listed $0.90 per follow rate, the ~920 accounts that followed this account during the spike would cost roughly ~$828 at full price — illustrative math based on public pricing, not a confirmed transaction record.

Platform infrastructure: Authentication for upvote.club is handled by Firebase (upvote-club.firebaseapp.com), confirmed via the Google OAuth consent screen during account creation. This means the platform's user database, authentication tokens, and session management run on Google Cloud infrastructure.

Membership scale discrepancy: upvote.club's marketing claims "50K+ Members." The authenticated API response tells a different story:

"community_rank": {
    "rank": 4127,
    "total_users": 4126,
    "completed_tasks": 0
}
Enter fullscreen mode Exit fullscreen mode

A new account registered in May 2026 received rank 4,127 out of 4,126 total users — an active user base of roughly 4,100, not 50,000. User IDs in the 79,000 range suggest approximately 79,000 accounts have been created historically, but the vast majority are inactive or abandoned. The "50K+ Members" claim overstates the active community by more than 12x.

Important framing note: This analysis identifies upvote.club as a marketplace whose model and pricing are consistent with the deployment pattern observed. I don't have access to purchase records, account registration logs, or payment data — only DEV.to's backend telemetry could confirm which specific service was used. What the behavioral evidence supports is this: the accounts behave exactly as task-completion accounts from a points-exchange follower service would behave, and upvote.club is an active, public-facing service matching that profile for DEV.to and GitHub simultaneously. The marketplace, warehousing behavior, and extension infrastructure are treated here as converging systems consistent with a unified operation — not as a confirmed single-operator pipeline.


Step 10: The Infrastructure Behind the Network

With the marketplace identified, I downloaded and decompiled the upvote.club Chrome extension (ID: fkiaohmeeoiipoknngcppjbkinaamnof, version 1.1.26) directly from the Chrome Web Store to understand how task verification actually works.

The extension is published under the name "Helper App" with the description "Just Helper App." No mention of upvote.club in the listing.

Permissions

The manifest requests the following:

<all_urls>       — content scripts run on every website
webRequest       — intercepts all network requests
tabs             — access to all open tabs and URLs
scripting        — can inject code into any page
storage          — persistent local data
activeTab        — access to current tab
sidePanel        — persistent browser sidebar
webNavigation    — monitors all navigation events
Enter fullscreen mode Exit fullscreen mode

This is a highly privileged permission set spanning all browsing contexts. Additionally, the platform collects browser fingerprint data at registration — user agent string, OS name, device type, landing URL, and referrer timestamp are all stored server-side on the Firebase backend. It is substantially broader than what task verification requires.

What It Actually Does

Note: The following is based on static analysis of the decompiled extension source. Behavior was inferred from code, not observed in a runtime environment. Static analysis alone cannot determine the full runtime behavior, data retention policies, or operational intent of the extension operators.

On DEV.to (social/devto.js): The content script attaches a click listener to every button on every DEV.to page. It detects follow, like, unicorn, save, comment, and reaction actions by inspecting aria-label, className, data-testid, and button text. It also intercepts POST requests to dev.to/follows, dev.to/reactions, and dev.to/comments via the network request layer. Detected actions are reported to the upvote.club backend.

Screenshot capture: The background worker includes captureVisibleTabAsDataUrl() — it takes a PNG screenshot of the active browser window and uploads it to api.upvote.club/api/social-profiles/upload-verification-screenshot/ along with the full extracted text of the page.

Request body interception: The extension intercepts raw POST bodies across 30+ platforms — Twitter/X, Facebook, LinkedIn, Reddit, GitHub, Instagram, TikTok, YouTube, Threads, DEV.to, Quora, Medium, Substack, Mastodon, Hacker News, Bluesky, and Indie Hackers. For each platform it decodes and parses the request body to identify the action type.

Token extraction: When the extension detects an upvote.club tab, it executes localStorage.getItem("accessToken") via chrome.scripting.executeScript to read the user's auth token and sync it to extension storage.

Google redirect interception: The background worker monitors www.google.com/url redirects and extracts task parameters embedded in the destination URLs.

The Shadow Domain

The extension source contains a production config referencing an undisclosed second domain:

production: {
  API_URL: "https://api.upvote.club",
  NS_API_URL: "https://ns.upvote.club",
  SITE_URL: "https://upvote.club",
  NS_SITE_URL: "https://nsboost.xyz"   // not mentioned publicly
}
Enter fullscreen mode Exit fullscreen mode

nsboost.xyz resolves to a separate IP (216.150.1.1) from upvote.club (172.67.182.120), but deeper analysis confirms they are the same operation. Three pieces of evidence establish this definitively:

  1. Same Yandex Metrica ID (98568698) — identical analytics account across both domains, indicating the same operator and business entity.
  2. nsboost.xyz's own sitemap points to upvote.club — both sitemap.xml and server-sitemap.xml on nsboost.xyz list https://upvote.club URLs as canonical, not nsboost.xyz URLs.
  3. robots.txt declares Host: https://upvote.club — the canonical host directive explicitly names upvote.club as the authoritative domain.

nsboost.xyz is a white-label frontend running on separate infrastructure but sharing the same backend, analytics, and operator as upvote.club. The Chrome extension handles both domains transparently — members logged into nsboost.xyz complete tasks that fulfill upvote.club orders and vice versa. The extension currently shows 2,000 active installs.

Hardcoded Secret

The extension ships with a hardcoded API secret visible in plaintext source. This authenticates screenshot uploads to their backend. Anyone who downloads the CRX file — which is public — has this key. The value has been redacted here and disclosed directly to the vendor and to Google.

What This Means for the Network

The accounts completing follow tasks on your DEV.to profile are running a browser extension with a highly privileged permission set. The extension monitors their activity across every major social platform, captures screenshots of their browser, reads their auth tokens, and intercepts their network requests — all while branded as "Helper App."

The behavioral uniformity observed in the audit (Following=1, zero engagement, synchronized activation) is a direct consequence of this architecture: every follow action is mechanically dispatched by the extension in response to a task assignment, with no organic browsing behavior attached.


The Operator's Own Words: GitHub Referrer Spoofing

While mapping the authenticated API, I retrieved the platform's internal blog feed — a members-only section accessible only to logged-in accounts, not publicly indexed. One post, published April 14, 2026 and titled "GitHub is back to platform," contains an explicit technical description of how the platform engineers its GitHub task flow to evade fraud detection:

"when someone completes a GitHub task through our extension, GitHub sees Google as the referrer. Not our platform, not some unknown source. As far as GitHub is concerned, someone found this repo through a Google search and decided to star it."

"Stars coming in from what looks like Google search traffic is exactly the pattern GitHub considers healthy. Repos pick up organic stars from Google all the time. That's the signal we're mimicking."

This is not behavioral inference or structural analysis. This is the operator describing, in their own words, a deliberate technical mechanism built to make fraudulent GitHub engagement appear as organic Google search traffic.

The post also explains that GitHub was previously removed from the platform entirely — suggesting GitHub's fraud detection had identified and suppressed the original approach. The referrer spoofing was built specifically to circumvent that detection:

"We needed it back. Just not the old way."

What this means technically: The Chrome extension, when completing a GitHub star or follow task, injects or overrides the HTTP Referer header on the outbound request to github.com, replacing the actual origin (upvote.club) with google.com or a Google search URL. From GitHub's server logs, the action appears indistinguishable from a user who arrived via organic search.

This is a materially different threat than fake engagement volume. This is active, engineered evasion of platform fraud detection infrastructure — designed specifically to survive the detection mechanisms platforms use to identify and remove inauthentic behavior.

The same referrer spoofing mechanism almost certainly applies to other platforms in the task queue. The GitHub post describes it as an extension-level capability, not a GitHub-specific implementation.

This finding has been reported to GitHub Security separately.


Step 11: Inside the Platform — Authenticated Investigation

Static analysis and passive traffic capture only go so far. To observe the platform from the inside — the task queue, the economics, the actual API structure — I created a controlled burner account and conducted an authenticated investigation under mitmproxy capture.

Lab Setup

# Isolated Brave profile + mitmproxy on :8181
# Extension loaded as unpacked — no Chrome Web Store install
/opt/brave.com/brave/brave \
    --user-data-dir=~/extension_lab/chrome_profile \
    --proxy-server="http://127.0.0.1:8181" \
    --ignore-certificate-errors \
    --load-extension=$EXT_PATH \
    --no-first-run
Enter fullscreen mode Exit fullscreen mode

All traffic routed through mitmproxy with the CA certificate installed. The lab browser had no saved sessions, no real accounts, and no connection to any personal identity.

Getting there took some friction. The Ubuntu snap package for Chromium ignores --user-data-dir and routes new instances to the existing browser session — meaning the extension would have loaded into my real browser with my real accounts visible. That was unacceptable for a surveillance-capable extension with <all_urls> permissions. The fix was switching to Brave, called directly via its binary path to bypass the snap wrapper entirely. mitmproxy also hit a port collision on first launch, requiring a clean restart on a different port. Neither is a remarkable finding — just the normal friction of building an isolated lab from scratch on short notice.

Registration: OAuth Only, No Email Option

upvote.club offers no email/password registration path. The only options are "Continue with Google" and "Continue with Apple." This is a deliberate architectural choice — every member account is backed by a verified Google or Apple identity.

The OAuth consent screen revealed something significant: the backend Firebase app identifier is upvote-club.firebaseapp.com. upvote.club runs on Google Firebase — their user database, authentication tokens, and session management all run on Google Cloud infrastructure.

A throwaway Google account (marcus.delray.dev@gmail.com) was created for this investigation and used exclusively for this purpose.

Onboarding Flow

After OAuth login, the onboarding flow asked:

  1. Select your country — US selected (not real location)
  2. What platforms do you want to boost? — DEV.to selected
  3. What engagement types? — Likes, Comments, Saves, Followers, Unicorns (all pre-checked)
  4. Describe your goal — free text field, required before continuing
  5. Paywall — $1 for 7 days trial, then $49/month

The paywall has no skip button and no free tier path through the onboarding UI. However, direct navigation to https://upvote.club/dashboard bypasses it entirely — the paywall is a UX gate, not an access restriction. The free account was fully functional after direct navigation.

Each step POSTed to api.upvote.club/api/onboarding-progress/ — the platform stores buyer intent data including selected platforms, engagement types, and free-text goals.

What the Platform Showed in Real Time

The dashboard's live activity feed showed ongoing manipulation across all platforms while I was watching:

+678  Dev.to Likes
+123  Dev.to Comments
+89   Dev.to Unicorns
+156  Dev.to Saves
+28.9k Total Actions Delivered Yesterday
Enter fullscreen mode Exit fullscreen mode

DEV.to engagement inflation was actively in progress during the investigation.

Account Profile — Authenticated API Response

The GET api.upvote.club/api/profile/ endpoint returned:

{
    "id": 79083,
    "user": 79080,
    "balance": 13,
    "status": "FREE",
    "daily_task_limit": 2,
    "available_tasks_for_completion": 331,
    "potential_earnings": 829.5,
    "community_rank": {
        "rank": 4127,
        "total_users": 4126,
        "completed_tasks": 0
    },
    "referrer_user_agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36...",
    "device_type": "desktop",
    "os_name": "Linux",
    "landing_url": "https://upvote.club/login",
    "referrer_timestamp": "2026-05-24T21:33:21.186000Z"
}
Enter fullscreen mode Exit fullscreen mode

Several findings from this response:

The 50K+ Members claim is false. The community_rank field shows rank: 4127, total_users: 4126 — a newly registered account in May 2026 is ranked last out of 4,126 total active users. upvote.club's marketing prominently claims "50K+ Members." The authenticated API puts the real active user base at roughly 4,100 — more than 12x smaller than advertised. User IDs in the 79,000 range suggest ~79,000 accounts created historically, but the vast majority are inactive or abandoned.

Browser fingerprint collected on registration. The platform stores referrer_user_agent, device_type, os_name, landing_url, and referrer_timestamp for every account. Members completing tasks have their full browser identity logged.

Free account economics. Balance: 13 points (signup bonus). Daily task limit: 2. With 331 available tasks showing potential_earnings: $829.50, the implied per-task earnings exceed the base $0.90 — suggesting higher-value tasks exist in the queue.

Live Task Queue

GET api.upvote.club/api/tasks1/ returned the current task distribution across platforms:

Total available: 217 tasks across 14 platforms

Quora:        52    GitHub:    51
LinkedIn:     34    Facebook:  12
Instagram:    10    Substack:  10
Threads:       9    Bluesky:    9
Reddit:        9    TikTok:     9
Twitter:       8    YouTube:    2
IndieHackers:  1    HackerNews: 1

DEV.to:        3 active orders
Enter fullscreen mode Exit fullscreen mode

Three active DEV.to manipulation orders were in the queue at the time of capture. One caveat worth being explicit about: the API returned tasks: [] for the DEV.to-filtered call despite the count showing 3. The task objects were present in the system but not returned to the new account. Free accounts with zero completed tasks appear to be gated out of the actual task delivery — the platform likely requires a completion history before assigning tasks to a member. The count confirms active DEV.to orders exist. The empty task list is a free-tier restriction, not an absence of activity.

Task Initiation Flow — Confirmed at Runtime

Clicking "Complete Task" on an Instagram task triggered:

POST https://api.upvote.club/api/initiate-task/64903/
Enter fullscreen mode Exit fullscreen mode

This confirms the task assignment sequence from static analysis: the upvote.club page sends a message to the extension via externally_connectable, the extension stores the task parameters, and the target URL opens in a new tab. Task ID 64903 was a real Instagram story from @thehawaiianmayan — a real person's content being targeted for fake engagement.

A Test That Failed — and What It Revealed

Before the authenticated session, I tried to trigger the extension's webRequest.onBeforeRequest listener directly by crafting a fake task URL:

https://dev.to/gnomeman4201?taskid=99999&userid=88888&ct=faketoken123&domain=upvote.club
Enter fullscreen mode Exit fullscreen mode

Static analysis showed this listener watches for taskid, userid, and ct parameters in URLs and writes them to chrome.storage.local. The expectation was that navigating to this URL would write currentTaskId: "99999" to storage. The storage stayed empty. The listener never fired.

The reason: the listener's URL filter is *://*.dev.to/* — which requires a subdomain. The bare apex domain dev.to doesn't match *.dev.to. Since DEV.to's production URLs use the apex domain without www, the webRequest listener is functionally dead for real DEV.to pages.

This means the URL parameter injection path doesn't work for DEV.to. The actual task assignment mechanism routes through externally_connectable message passing from an active upvote.club tab directly to the extension — not from URL traffic interception. The extension needs an open upvote.club tab to receive task parameters. It's not passively watching URLs.

That's a meaningful constraint on the threat model — and I only found it because the test failed.

Extension Balance Sync — Confirmed at Runtime

After login, the extension icon badge updated to show 9 — the account balance minus the 4 points spent navigating the onboarding. This confirms the getUserBalance()updateBadgeWithBalance() flow from static analysis fires on authentication and keeps the badge in sync with the server balance.


Task Completion Protocol — Full Live Capture

Static analysis identified the initiate/complete two-phase task flow. What remained unverified was the actual POST body structure for complete-task — the proof mechanism. That was confirmed through live mitmproxy capture on May 24, 2026.

After initiating an X (Twitter) follow task for @nferhattaleb (task ID 64923), the extension auto-navigated to the target profile and waited. Completing the task required executing one follow action — an unavoidable step in observing the live protocol under realistic conditions. After the follow was completed, the extension initiated and fully completed a second task (ID 64918) autonomously — without any additional user interaction. The full protocol, captured verbatim:

Phase 1 — Initiation:

POST https://api.upvote.club/api/initiate-task/64918/
Body: {}

Response:
{
  "client_sync_pending": false,
  "completion_token": "t6EdrgY1AM-uPRC90bOdLeXmY7Dmbzk8b2Xopxa5RRg",
  "meaningful_comment_text": "",
  "server_ts": 1779667395
}
Enter fullscreen mode Exit fullscreen mode

The server issues a completion_token — a per-task secret that serves as the HMAC signing key for the completion POST.

Phase 2 — Completion:

POST https://api.upvote.club/api/complete-task/64918/
Headers:
  x-uc-client: ext
  x-uc-ext-id: fkiaohmeeoiipoknngcppjbkinaamnof
  x-uc-ext-version: 1.1.26
  origin: chrome-extension://fkiaohmeeoiipoknngcppjbkinaamnof

Body:
{
  "action": "FOLLOW",
  "completion_token": "t6EdrgY1AM-uPRC90bOdLeXmY7Dmbzk8b2Xopxa5RRg",
  "user": "79083",
  "x_sig": "a5853cefa2fb1f8f2bcdbff54acaf8e33a36c4ba030eb84350fd932810bc12fc",
  "x_ts": 1779667400
}

Response:
{
  "message": "Task completed successfully",
  "new_balance": 14.0,
  "reward": 1.0,
  "task_status": "ACTIVE"
}
Enter fullscreen mode Exit fullscreen mode

Phase 3 — Redirect:

GET /dashboard?success-action-redirect&reward=1&balance=14&completed_task_id=64918
Enter fullscreen mode Exit fullscreen mode

Phase 4 — Queue de-duplication:

GET /api/tasks1/?exclude_browser_task_ids=64918
Enter fullscreen mode Exit fullscreen mode

The completed task is filtered from the local task list immediately after completion.


The HMAC Signing Formula — Reverse Engineered

The x_sig field in the completion POST is a verified HMAC-SHA256 signature. Decompiling background.js from the extension source revealed the exact signing function:

async function buildCompleteTaskSignature(taskId, action, completionToken, submittedComment, clockOffsetSec) {
  const ts = Math.floor(Date.now() / 1e3) + offset;
  const commentHashHex = bufferToHex(await crypto.subtle.digest("SHA-256", enc.encode(submittedComment || "")));
  const message = `${taskId}|${action}|${ts}|${commentHashHex}`;
  const key = await crypto.subtle.importKey("raw", enc.encode(completionToken), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const sigBuf = await crypto.subtle.sign("HMAC", key, enc.encode(message));
  return { x_sig: bufferToHex(sigBuf), x_ts: ts };
}
Enter fullscreen mode Exit fullscreen mode

The signing key is the completion_token issued by initiate-task. There is no separate hardcoded secret — the server can verify every signature because it knows the token it issued.

The formula was verified against the live capture using only captured values:

import hmac, hashlib

token = "t6EdrgY1AM-uPRC90bOdLeXmY7Dmbzk8b2Xopxa5RRg"
ts = 1779667400
comment_hash = hashlib.sha256(b"").hexdigest()
message = f"64918|FOLLOW|{ts}|{comment_hash}"
sig = hmac.new(token.encode(), message.encode(), hashlib.sha256).hexdigest()

# sig == "a5853cefa2fb1f8f2bcdbff54acaf8e33a36c4ba030eb84350fd932810bc12fc"
# MATCH: True
Enter fullscreen mode Exit fullscreen mode

The signature matches exactly. The complete task API is replayable from a valid JWT and a task ID — no browser, no extension required. This verification script is in the repo at evidence/verify_sig.py.

What this means: Anyone with a valid upvote.club JWT can programmatically initiate tasks, construct valid HMAC signatures, and submit completions at scale. The extension is not a technical enforcement boundary — it is a convenience wrapper around a fully scriptable API. The architecture does not prevent automated abuse; it outsources the action to an installed browser extension and trusts the token-based signature as proof.


Autonomous Task Execution — Extension Acts Without User Interaction

The most significant behavioral finding from the live session: the extension initiated and completed task 64918 autonomously, without any additional user action after the initial follow on task 64923.

The sequence captured in the mitmproxy log:

[user clicks Follow on @nferhattaleb — task 64923]
[extension scrapes ~300 Twitter profile images from pbs.twimg.com — follower list verification]
[extension initiates task 64918 — new task, no user prompt]
[extension navigates to YouTube target — No Text To Speech channel]
[extension completes task 64918 — full HMAC-signed POST]
[dashboard reloads: success-action-redirect&reward=1&balance=14]
Enter fullscreen mode Exit fullscreen mode

The extension does not wait for user input between tasks. After one task completes, it automatically queues and executes the next. From the user's perspective, they installed a browser extension, completed one follow, and received points. Behind that interaction, the extension performed additional tasks on their behalf.

This has a direct consequence for the platform's "real humans" framing: users completing tasks may not be aware of the full scope of actions the extension takes in their browser session.


Follower-Scrape Verification Mechanism — Captured Live

Between task initiation and the complete-task POST, the mitmproxy capture showed a burst of over 300 sequential GET requests to pbs.twimg.com/profile_images/ — Twitter's CDN for profile photos.

The extension was crawling the follower list of the target account (@nferhattaleb) to verify that the burner account's follow had landed. Rather than relying on Twitter's API response to confirm the follow action, the extension scrapes the visual follower list and cross-references profile images to confirm the new follower appears.

This is the mechanism behind the captureVisibleTab permission identified in static analysis. Screenshot-based verification — not API confirmation — is how the extension proves task completion to the server.


Google Referrer Spoofing — Captured Live

The internal blog post describing GitHub referrer spoofing (quoted in Step 10) was confirmed in the live traffic capture. During the session, the following request was observed:

GET https://www.google.com/url?q=https%3A%2F%2Fx.com%2FBAxCoinbase HTTP/2.0
Enter fullscreen mode Exit fullscreen mode

The extension routed an X (Twitter) navigation through a Google redirect URL — google.com/url?q=<target> — before opening the target profile. From the destination platform's server logs, this request arrives with google.com as the referring domain. The platform sees what appears to be a user who found the account through Google, not through upvote.club task dispatch.

The Google session cookies present in the browser were transmitted to Google's redirect endpoint as a side effect, leaking authenticated Google session state to the redirect intermediary. This is a collateral privacy consequence of the spoofing architecture.

This confirms the referrer spoofing described in the operator's own blog post is not limited to GitHub — it is a general mechanism applied across platform tasks.


Why This Account, Why Now

The deployment timing raises a question worth addressing directly: why did a follower flood targeting a security researcher begin one day after that researcher published botnet exposure work?

Two interpretations are consistent with the evidence:

Targeted retaliation. Someone connected to the fake engagement ecosystem purchased a follower inflation order specifically against this account in response to the GitHub botnet article. The 24-hour lag is consistent with a human purchasing decision rather than automated monitoring.

Reputation poisoning as an attack vector. A DEV.to account that gains 900 followers in four days from accounts with no organic activity could trigger automated platform integrity systems — potentially flagging the target as the bad actor. For security researchers specifically, having your account suspended for artificial follower inflation immediately after publishing botnet research would be a highly effective way to discredit the work. Whether or not this was the intent, it is the structural effect.

This is temporal correlation, not proven attribution — but the convergence of timing, target profile, and deployment scale makes organic coincidence the least parsimonious explanation.

On the single-use account question: the November 2025 accounts show no sign of having followed and unfollowed previous targets. The observed behavior is consistent with a single-use deployment model — accounts created, aged, deployed once against one target, then warehoused indefinitely. That makes the aged-account inventory more valuable but also more wasteful: 897 accounts burned for one order.


End-to-End Timeline

Nov 13-14, 2025  ──  218 accounts created (S3 IDs 3,610,947–3,619,885)
                      Zero activity. Warehoused.

Jan 2026         ──  17 more accounts created. Warehoused.

Apr 2026         ──  92 more accounts created. Warehoused.

May 13-14, 2026  ──  615 accounts created across 2 days.

May 19, 2026     ──  "Found a Coordinated GitHub Follow Botnet" published.

May 20, 2026     ──  Deployment begins. 288 new followers in one day.
                      All four waves activated. Following=1, target=GnomeMan4201.

May 19–23, 2026  ──  2,300+ accounts added. Count: ~600 → 3,045+. Still active.
Enter fullscreen mode Exit fullscreen mode

The timing is consistent with a targeted follower inflation deployment temporally associated with the article publication. Four account batches created across six months, warehoused, then activated in close temporal proximity to a specific publication event.


How to Audit Your Own Followers

You don't need my full toolchain. The Following=1 signal is enough to get started:

import requests, time

API_KEY = 'your_key'

def audit_your_followers():
    page, flagged = 1, []
    while True:
        resp = requests.get(
            'https://dev.to/api/followers/users',
            headers={'api-key': API_KEY},
            params={'page': page, 'per_page': 1000},
        )
        batch = resp.json()
        if not batch:
            break

        for user in batch:
            u = requests.get(
                'https://dev.to/api/users/by_username',
                headers={'api-key': API_KEY},
                params={'url': user['username']},
            ).json()
            time.sleep(0.25)

            if (u.get('following_count') == 1
                    and u.get('public_articles_count') == 0
                    and u.get('followers_count') == 0):
                flagged.append(user['username'])
                print(f'[FLAGGED] @{user["username"]}')

        page += 1

    print(f'\n{len(flagged)} accounts matching coordinated inauthentic pattern')
    return flagged

audit_your_followers()
Enter fullscreen mode Exit fullscreen mode

If you see a sudden follower spike after publishing — especially security or platform research — run this. Accounts matching this behavioral profile will surface immediately. For deeper analysis (batch wave detection, image fingerprinting, S3 sequencing), the full toolchain is in the repo.


What I Reported

I disclosed everything across four channels:

  1. DEV.to security (four emails) — 897 flagged usernames, scored CSV, audit scripts, S3 sequencing, dormancy timeline, image fingerprinting, asset attribution, extension analysis. DEV.to previously suspended a related fraud marketplace (3 accounts, 34 articles) the same day I reported it and has been responsive throughout. Notably, the follower flood remained active through the entire investigation and disclosure period — from first detection May 19 through at least May 24, reaching 3,045+ total followers while this research was being compiled and reported.

  2. Google Chrome Web Store — policy violation report filed against the "Helper App" extension for misleading name/description and undisclosed data collection. Ticket: 1-1457000040647.

  3. GitHub Security — filed May 24, 2026. The internal blog post explicitly admitting GitHub referrer spoofing constitutes documented, intentional fraud detection evasion targeting GitHub’s platform integrity systems. The HMAC signing formula and replayable API were included as supplementary technical findings.

  4. upvote.club directly — the hardcoded API secret identified in static analysis was disclosed to the vendor prior to publication. The value has been redacted throughout this article.

I'm publishing this because developers deserve to know what these campaigns look like and how to detect them. Follower counts carry real social weight — they affect credibility signals, algorithm visibility, and how new readers decide whether to trust your work. Artificially inflating those numbers is platform manipulation, and it's more technically sophisticated than most people expect.

The more developers understand these mechanics, the harder these networks are to run quietly. Transparency makes the community harder to exploit.


Limitations

This audit used publicly observable metadata and heuristic scoring — not internal platform telemetry. I don't have access to:

  • IP address or device fingerprints
  • Session linkage data
  • Payment or purchase records
  • Internal moderation signals

As a result, this analysis identifies coordinated inauthentic behavior patterns rather than attributing activity to a specific individual or organization. The findings are strong enough to act on at the platform level, but the full picture requires data that only DEV.to's backend can provide.


Full Findings Summary

Metric Value
Total followers audited 1,409 (snapshot taken May 23)
Flagged as likely coordinated inauthentic 897 (63.7%)
False positives identified and removed 2 (via S3 ID analysis)
Accounts with Following=1 1,409 (100%)
Accounts with zero posts 1,393+
Exact duplicate image groups 55
Perceptual similarity clusters 56
Custom illustration avatars 74
Username generators identified 3 (hex-suffix, simple, creative phrase)
Creation waves identified 4
November wave dormancy 187 days
GitHub follower crossover 0 of 897 — complete platform separation confirmed
Asset source Public domain (ClipArt ETC / DepositPhotos)
Asset library in use since At least Dec 2023
Marketplace identified upvote.club ($0.90/follow, 24hr delivery)
Shadow domain nsboost.xyz (confirmed same operator)
Extension active installs 2,000
Extension published as "Helper App" / "Just Helper App"
Estimated order value ~$828 at public pricing
Follower count at publication 3,045+ and climbing
Platform backend Firebase (Google Cloud)
Claimed membership 50K+
Actual active users (API confirmed) ~4,126
Active task orders at time of investigation 217 across 14 platforms
Active DEV.to orders in queue 3
Task completion protocol Two-phase: initiate (server issues token) → complete (HMAC-SHA256 signed POST)
Signing key source Server-issued completion_token doubles as HMAC key — no hardcoded secret
API replayability Fully scriptable from JWT + task ID — no browser or extension required
Autonomous task execution Extension completes tasks without user interaction between assignments
Verification mechanism Follower-list scrape via pbs.twimg.com (~300 profile image GETs per task)
Referrer spoofing — confirmed live google.com/url?q=<target> routing captured in mitmproxy session
Disclosures filed DEV.to (4 emails), Chrome Web Store (ticket 1-1457000040647), GitHub Security (May 24), vendor direct

A note on the cross-platform null result: Finding zero GitHub overlap across all 897 flagged accounts was initially a disappointing outcome — the same accounts appearing on multiple platforms would have been a stronger finding. But the null result is itself the finding. The operator runs completely siloed account pools. Detection on DEV.to gives zero signal about their GitHub, Reddit, or Bluesky accounts. The compartmentalization is consistent with the extension architecture: task assignments are platform-specific, and account identities are not recycled. This also means platform-level enforcement is necessarily blind to the broader network.


Full toolchain at github.com/GnomeMan4201/devto-botnet-hunter. Methodology critiques and PRs welcome.


Coordinated follower inflation looks organic at the individual-account level. At graph scale, it becomes a structurally degenerate pattern — detectable not by individual account properties, but by the topology of the graph itself.

Top comments (16)

Collapse
 
ben profile image
Ben Halpern

Thanks for this and your email, this is constantly something we're trying to improve and any help in squashing these rings is appreciated

Collapse
 
gnomeman4201 profile image
GnomeMan4201

I appreciate you taking the time to review it.

Hopefully the data helps

Collapse
 
toxy4ny profile image
KL3FT3Z

Fantastic work - especially the graph analysis and the HMAC reverse. I want to suggest one more dimension for your next investigation that flows directly from your findings.

You showed that upvote.club sells not just follows, but also comments, likes, and unicorns. This means a second layer of infrastructure may already be active beneath "good" articles on DEV.to - AI agents writing templated comments to fulfill orders for "meaningful engagement."

Here's what I'd add to your toolchain:

1. Comment text pattern analysis

  • Your fake followers are following=1, zero articles. But what if some of them (or a parallel cohort) have already graduated to the "earn points by commenting" phase?
  • upvote.club explicitly asks for "meaningful comment text" - and that's the key phrase. In their context, "meaningful" means "not spam that immediately triggers moderation." So the comment must be grammatically correct, topically relevant- and yet completely meaningless in terms of real dialogue.
  • The perfect job for an LLM agent: read the article, generate 2-3 paragraphs of generic phrases, drop in a "thanks for sharing" and a "looking forward to more."

2. A graph signal for comments - the Following=1 equivalent

  • You had Following=1 as an invariant for followers. For commenters, the analogous invariant could be: an account that left a comment on an article, but has articles=0, comments_count=1 (or very low), and its only comment is on an article that's active in the upvote.club queue.
  • If the extension intercepts POSTs to /comments (you confirmed this in the static analysis of social/devto.js), then comments are a full attack vector, no less automatable than follows.

3. What to look for in the text

  • Perfect grammar + zero specificity. Real developers write with typos, abbreviations, and references to their own experience. LLM agents generate "smooth" text without a single concrete detail from the article.
  • Recurring n-grams or structural templates. For example: "This is a great deep dive into [topic]. I particularly appreciated how you explained [generic concept]. Have you considered [vague suggestion]? Looking forward to your next post!" - the skeleton may vary, but graph-wise it will cluster.
  • Temporal clustering. If 15 comments appear under an article within 24 hours, and 8 of them are from accounts with joined_at in the same wave (your November/May cohorts) - that's not hype, that's fulfillment.

4. The scariest part - recursive hallucinations

  • You mentioned that DEV.to is a community. But if an AI comment appears under an article, and then another AI agent (possibly from the same upvote.club or just an automated bot) replies to it — you get a hallucinatory dialogue. Two agents exchange gratitude and generic phrases, creating the illusion of a "lively discussion" under the article.
  • This is worse than fake followers. Followers are a metric. Comments are semantic noise that pollutes the platform's information space. A reader sees "20 comments," opens the thread, and finds 15 automated "great post" messages and 5 recursive replies between bots.

5. Technical implementation

  • Your score_account() could be extended to score_commenter():
  def score_commenter(profile, comment_text):
      score = 0
      if profile['articles_count'] == 0: score += 1
      if profile['comments_count'] <= 2: score += 1
      if profile['joined_at'] in known_bot_waves: score += 1
      if generic_praise_ratio(comment_text) > 0.8: score += 1
      if no_specific_references(comment_text, article_content): score += 1
      return score
Enter fullscreen mode Exit fullscreen mode
  • For template detection - simple TF-IDF + cosine similarity between comments from the same author or the same wave. Or even perceptual hashing, but for text: signature sentence hashes.

6. Why this matters right now

  • You discovered that upvote.club sells comments. You confirmed that the extension intercepts POSTs to /comments. You showed that the API is fully replayable.
  • The logical next step: if a customer buys "DEV.to Comments" and the executor is a human with the extension installed, which can autonomously execute tasks (you captured this live - task 64918 completed without interaction), then comments could be generated not by a human, but by a script that calls an LLM and posts the result via the replayable API.
  • upvote.club doesn't verify what is written in the comment - they only verify that a POST request happened. This means the comment quality can be absolutely zero, and the platform will still count it as completed.

TL;DR for your next project:

  • Scrape comments under articles that were simultaneously in the upvote.club queue (you have the API for this).
  • Check if any accounts with following=1 (your fake followers) suddenly started commenting.
  • Look for text clusters — recurring templates, "meaningful" generic phrases, absence of specificity.
  • If you find recursive dialogues between suspicious accounts - that's another graph invariant, no weaker than Following=1.

Your methodology with score_account and graph invariants maps perfectly onto this vector. Followers are volume. Comments are informational environment toxicity. And if the follower botnet is already proven, the comment botnet is just the same extension hitting a different endpoint.

Thank you for open-sourcing the toolchain and for publishing the methodology, not just the results. That makes the ecosystem more resilient.

Collapse
 
gnomeman4201 profile image
GnomeMan4201 • Edited

I really appreciate you commenting on this and adding to the conversation. It's rare to have any dialogue about this niche area.

The comment vector framing is dead on and the infrastructure confirmation goes deeper than what I published.

On the extension intercepting /comments POSTs confirmed, but the scope is wider than DEV.to. The Helper App (fkiaohmeeoiipoknngcppjbkinaamnof) intercepts LIKE, UNICORN, SAVE, BOOKMARK, FOLLOW, and COMMENT across 20+ platforms through the same C2 at api.upvote.club. Instagram and Threads get GraphQL hooks running in MAIN world not DOM manipulation, actual page-level API interception. The task loop is fully documented: C2 delivers taskId + completionToken via URL params, content script executes the action, HMAC-SHA256 signature built from ct+taskId+action+timestamp proves completion, POST to /api/complete-task/{taskId}/. The whole thing is replayable without a human in the loop.

On the two-vector separation I published that framing may be wrong. 58% of MD5 avatar cluster groups contain BOTH pure bot accounts (hash-generated usernames, zero activity) AND confirmed upvote.club engagement farm participants. Same image source feeding both populations. The parsimonious explanation is one account factory with two deployment modes: some accounts get handed to the engagement farm as "real users," some get deployed as pure follow bots. The line between the two is less clear than the platform claims.

On the self-boosting angle there's a specific instance I haven't published yet. They're running their own Helper App extension through their own CHROMESOCIAL network (product ID 46) to inflate its own install count and reviews on the Chrome Web Store. The operation is farming itself. That's a direct CWS policy violation on top of everything else, and it's verifiable from the product catalog.

On the full circle architecture here's the part that reframes the whole thing. The Helper App includes a captureVisibleTab handler that screenshots whatever tab is active during verification and uploads it to /api/social-profiles/upload-verification-screenshot/ authenticated with a hardcoded EXTENSION_SCREENSHOT_UPLOAD_SECRET in background.js. They built covert surveillance into their own engagement farm to police their own members. The fraud detection mechanism is itself a privacy violation. Anyone who installed this extension to earn points handed the operator a silent screen capture capability. I logged the endpoint, caught it live, and confirmed it: HTTP 401 without auth, HTTP 200 with valid JWT plus the hardcoded secret. The secret is in every CRX that's been distributed. That's the trust hierarchy operator surveils members, members surveil platforms, platforms surveil users. Three layers of covert observation stacked on top of each other.

On comment text patterns and the LLM pipeline their own blog posts document it explicitly. Post 170 and 199 describe an AI drafting pipeline for Quora: AI writes natural non-promotional replies designed to pass moderation, aged accounts post them, community upvotes them, deleted answers get reposted with modified text to evade re-detection. That's not speculation that's their published product description. The meaningful_comment handler in devto.js is the same pipeline hitting a different endpoint.

On active platform security evasion this is where it crosses a line blog post 169 is an operator admission I haven't published yet. Direct quote from their own content: when someone completes a GitHub task through the extension, GitHub sees Google as the referrer. Not their platform. Not an unknown source. Google. That's deliberate header spoofing to circumvent GitHub's fraud detection systems. That's not engagement farming. That's active evasion of platform security infrastructure, documented in their own words. Combined with the Quora answer restoration pipeline deleted content reposted with modified text to evade re-detection and the 17-platform coordinated comment injection system, these are operator admissions of systematic detection evasion across multiple platforms simultaneously.

On the recursive hallucination model this is the thread I want to pull hardest. You're right that it's worse than fake followers. A follow is a vanity metric. A comment thread between two agent-generated accounts under a real article is synthetic discourse injected into a community's knowledge base. The detection invariant you proposed....accounts with articles=0, comments_count=1, only comment on an article active in the upvote.club queue is exactly the right shape. I have the API access to run that query. The temporal clustering angle closes the gap: if 8 of 15 comments under an article within 24 hours come from accounts in the same joined_at wave, that's not hype, that's fulfillment. That's a graph invariant I haven't formalized yet but should.

On operator infrastructure and attribution I went down every thread I could find and link together. Domain registration chains, git commit metadata, Firebase UIDs, Discord IDs, blog post authorship, product catalog ownership, MX record overlap, Yandex Metrica tag sharing, S3 bucket fingerprints, Wayback Machine snapshots, employer records recovered from commit history across repos spanning nearly a decade. I mapped the entire organization from the inside out legal entity, infrastructure stack, named individuals, development environment, side operation vs day job. The person at the head of this is genuinely technical. This isn't a marketer who hired a developer. The architecture of the extension, the HMAC signing scheme, the C2 design, the multi-platform GraphQL hooks whoever built this knows exactly what they're doing. The hardcoded ngrok tunnel in the production build pointing to a Vultr VPS dev environment was the opsec failure that confirmed it. The version bump from 1.1.26 to 1.9.43 post-disclosure had zero code changes identical background.js, identical hardcoded secret, identical surveillance mechanisms. They saw the disclosure and changed the version number. Nothing else. That tells you everything about how they assessed their exposure.

The score_commenter() sketch you wrote fits the existing codebase almost exactly as written. Part 3 is already outlined. The recursive hallucination section is its own threat model, not a footnote and im curious what you think.

Collapse
 
toxy4ny profile image
KL3FT3Z

Good data on the 58% overlap! unified pipeline with JIT routing makes more sense than two silos. Confirms S3 sequencing and avatar provenance are cross-branch invariants, which simplifies detection logic.
The CWS self-boost loop via CHROMESOCIAL (product ID 46) is a separate report to Google. Shadow product farming its own install count through the same extension that passed review that's a CWS trust model failure, not just a policy violation.
On the surveillance hierarchy: you named three layers, there's a fourth. means the operator collects on every site the member visits, not just task targets. 2,000 browsers as passive SIGINT platform, funded by microtask economics. The hardcoded screenshot secret in every distributed CRX means anyone who installed prior to disclosure has a known-static credential potentially exposing their full browsing session. That's not C2 architecture, that's bulk collection.
Posts 170/199 confirm the adversarial loop: generate → post → detect → evade → regenerate with semantic distance checks. The Quora → DEV.to pipeline transfer means meaningful_comment is a standardized LLM interface, not a one-off. Confirms the handler in devto.js is production code for cross-platform deployment.
Post 169 establishes intent. Operator's own words on referrer spoofing = documented evasion of platform security infrastructure. Published in members-only blog = either arrogance or misjudgment of member loyalty. Neither changes the legal character.
On recursive hallucinations: agree on the invariant shape. I'd add referential thinness as a signal -LLM comments mention "the approach" or "your analysis" without anchoring to specific paragraphs, methods, or findings. Real technical comments cite specifics. Generic praise + zero referential anchors = high-confidence synthetic.
Version bump 1.1.26 to 1.9.43 with zero code changes and identical secret: they assessed exposure as manageable. Implies overconfidence or backup infrastructure. Both are bad for defenders.
Re: Part 3 your API access validates the query. Single instance of joined_at-clustered comments under an active queue article = proof of concept for recursive hallucination model. The score_commenter() sketch maps directly to your existing codebase.
One question: with your attribution depth (legal entity, named individuals, infrastructure stack), have you evaluated the evidence preservation boundary? Platform reports and CWS disclosure cover the technical side. Operator blog posts contain admissions that may support law enforcement interest. At what point doesn’t evidence?

Thread Thread
 
gnomeman4201 profile image
GnomeMan4201

Evidence preservation boundary ..yes, I've evaluated it. Everything is in a public versioned repo: 36 timestamped commits, raw authenticated API captures, full decompiled extension source, 688KB product catalog JSON, and operator blog post admissions all committed before the storefront went offline May 26. The commit timeline predates the operator response, so the preservation is independently verifiable, not just my word. AKI GDPR complaint is filed. My honest hesitation on LE has been whether a solo independent researcher gets taken seriously by a foreign DPA. But looking at what's actually in the repo, this isn't a tip it's a case file. The FTC is also in scope given ~4,000 US installs and undisclosed surveillance collection under a misleading listing. I've sat on the LE referral question. Your question is making me reconsider that

Thread Thread
 
gnomeman4201 profile image
GnomeMan4201

One addition to the evidence preservation point .the investigation is still active. The backend is live as of today, API still serving ~4,000 extension users, CRX unchanged at v1.1.26 since May 24. Longitudinal monitoring has been running continuously since disclosure. The case file isn't frozen , it's still writing

Thread Thread
 
toxy4ny profile image
KL3FT3Z

Good chain of custody. 36 timestamped commits predating operator response = independent verifiability, not just researcher credibility.
On LE hesitation: solo researcher concern is valid, but the repo structure changes the calculus. This isn't a tip requiring DPA resources to investigate it's a pre-built case file. DPA or FTC can intake this directly and issue enforcement without preliminary phase. The 688KB product catalog + decompiled source + live captures = evidence package, not leads.
FTC is the sharper angle here. ~4K US installs, undisclosed collection under misleading CWS listing = Section 5 violation with precedent (Stylish, Hover Zoom). FTC doesn't need to prove harm to individual users - deceptive practices in data collection are sufficient. GDPR complaint covers EU installs. Two jurisdictions, one evidence package.
On storefront going offline May 26: operator response confirms they assessed exposure. But offline storefront ≠ destroyed evidence. Your commit timeline captured everything pre-response. Their takedown is actually corroborating evidence - flight response after disclosure.
On the hesitation itself - I get it. Solo researcher, impostor syndrome, self-checking instead of acting fast. Same here. The difference between a tip and a case file is what lets you push past that. Your repo is the case file. You don't need to be right about everything you need to be right about enough, and the evidence speaks for itself. Send it.

Thread Thread
 
gnomeman4201 profile image
GnomeMan4201

You were right. FTC Report 202214588, IC3 Submission e2fe6128622143a4a37763e2d0c40d75. Filed today. The case file sent itself.....we will see if i fucked it up in any way and if anything happens

Collapse
 
yune120 profile image
Yunetzi

Fake followers pollute the signal. Time for verifiable identities and public audits so real engagement actually counts.

Collapse
 
gnomeman4201 profile image
GnomeMan4201 • Edited

Agree about the signal problem, but I don’t want my identity and data sitting with every platform’s security team just to participate online more than I already have to. Forced verification isn’t a complete solution. We should also be teaching people what manipulation looks like and how to see through coordinated noise and fake engagement

Collapse
 
taqui profile image
Taqui

i have more than 20k 😭

Collapse
 
gnomeman4201 profile image
GnomeMan4201

Somewhere there’s a server farm that’s a big fan of your work.

Collapse
 
sirinivask profile image
Srinivas Kondepudi

Glad Im reached end of the article :)

but the findings are real, great work really, thanks for this :)

Collapse
 
gnomeman4201 profile image
GnomeMan4201

Thanks, I appreciate you taking the time to read through it. I know it is a long read.
I wanted to make sure the claims were backed by reproducible evidence instead of assumptions.

Collapse
 
tracygjg profile image
Tracy Gilmore • Edited

I have long stopped paying any attention to the number of followers when I realised many of them were linked to gambling sites in the Far East and had no bio recorded.

Goodhart's law is an adage that has been stated as,

"When a measure becomes a target, it ceases to be a good measure".

There is also the fact that any measure is bound to be gamed eventually, but I don't know of a quote for that.