# Bluesky Account Audit: Fake Followers, Engagement & Score (`metallic_pastry/bluesky-account-audit`) Actor

Audit any Bluesky account: fake and ghost follower share from a live follower sample, estimated real followers, engagement rate, posting cadence, content mix, top posts and a transparent account score. Public AT Protocol API, no login. Errors are free.

- **URL**: https://apify.com/metallic_pastry/bluesky-account-audit.md
- **Developed by:** [Simon Liarsgård](https://apify.com/metallic_pastry) (community)
- **Categories:** Social media, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 account auditeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bluesky Account Audit: Fake Followers, Engagement & Score

**Check whether a Bluesky account's audience is real before you pay, partner or follow. One row per account: the share of suspicious and ghost followers from a live follower sample, estimated real followers, engagement rate, posting cadence, content mix, top posts and a transparent account score.**

Paste handles, DIDs or bsky.app links and get structured results you can sort, filter and export. Everything comes from Bluesky's public AT Protocol API. No login, no app password, no browser.

**$0.02 per audited account. Invalid input, unknown or deactivated accounts and API errors are free.**

### Who it is for

- **Influencer and creator vetting:** see how much of a creator's recent follower growth looks like bots or spam before you sponsor them.
- **Agencies and brands:** compare a shortlist of accounts on audience quality, engagement and activity in one table.
- **Partnerships and PR:** check that an account is active, posts original content and reaches real people.
- **Your own account:** watch for spam follower waves and see which of your posts actually landed.

### How to use it

1. Add accounts to **Accounts**: `name.bsky.social`, a custom-domain handle like `nytimes.com`, `@handle`, a `did:plc:…` or a `https://bsky.app/profile/…` link.
2. Optionally change **Follower sample size** (500 by default, up to 5,000) and **Posts to analyze** (100 by default, up to 1,000).
3. Run. Each account becomes one dataset item. The **Overview** view shows the headline numbers; **Detail** has everything.

Schedule it weekly to track how an account's audience changes.

### What you get per account

- **Profile:** handle, display name, DID, bio, account age, followers, following, posts, follower/following ratio, custom-domain handle, Bluesky verification, moderation labels, lists, feeds and starter packs.
- **Follower audit** on the most recent followers:
  - `suspiciousShare`: bot or spam patterns (see Methodology).
  - `ghostShare`: accounts that have never posted.
  - `activeShare`: everyone else.
  - The share of each individual signal: no avatar, no bio, no display name, zero posts, new account, mass-following, spam-type moderation label, broken handle.
  - Median followers and posts of the followers, the share with 1,000+ followers, and the biggest accounts in the sample (`notableFollowers`).
- **Estimated real followers:** followers × (1 − suspicious share).
- **Activity:** originals, replies, quotes and reposts and their shares, media and link share, posts per week, active days, last post, peak posting hours and weekdays (UTC).
- **Engagement** on top-level posts and quotes: median and average interactions, average likes, reposts, replies and quotes, the median and average engagement rate, and how much of the total came from the single best post.
- **Content:** top languages, hashtags and link domains.
- **Top posts** by interactions, with link, text snippet and counts.
- **Scores** (0 to 100) for audience quality, engagement and activity, a combined **account score**, a verdict (`strong`, `solid`, `mixed`, `weak`) and **flags**.

#### Example (shortened, from a real run)

```json
{
  "account": "theonion.com",
  "status": "ok",
  "handle": "theonion.com",
  "displayName": "The Onion",
  "followersCount": 1359784,
  "followsCount": 5,
  "postsCount": 8945,
  "verified": true,
  "suspiciousFollowerShare": 0.012,
  "estimatedRealFollowers": 1343467,
  "audience": { "sampleSize": 500, "suspiciousShare": 0.012, "ghostShare": 0.374, "activeShare": 0.614, "medianFollowerFollowers": 31 },
  "activity": { "originals": 94, "replies": 1, "reposts": 5, "postsPerWeek": 89.76, "mediaShare": 0.8105, "peakHoursUtc": [19, 18, 22] },
  "engagement": { "postsMeasured": 79, "medianEngagement": 1012, "avgLikes": 1238.9, "engagementRate": 0.000744 },
  "content": { "topLinkDomains": [{ "domain": "theonion.com", "count": 66 }] },
  "audienceQualityScore": 83,
  "engagementScore": 32,
  "activityScore": 100,
  "accountScore": 69,
  "verdict": "solid",
  "flags": [],
  "profileUrl": "https://bsky.app/profile/theonion.com"
}
```

### Input

| Field | Default | What it does |
|---|---|---|
| `accounts` | | Handles, DIDs or bsky.app URLs. Duplicates are removed, including a handle and its DID. |
| `maxAccounts` | 100 | Hard cap per run (max 1,000). |
| `followerSampleSize` | 500 | Most recent followers to check (0 to 5,000; 0 skips the follower audit). |
| `postsToAnalyze` | 100 | Recent feed items to read (0 to 1,000; 0 skips the post analysis). |
| `newAccountDays` | 30 | Age below which a follower counts as new (informational). |
| `topPostsInOutput` | 5 | Top posts listed per account (0 to 25). |
| `notableFollowersInOutput` | 5 | Largest followers listed per account (0 to 25). |
| `concurrency` | 3 | Accounts in parallel (1 to 5). |

Example:

```json
{
  "accounts": ["theonion.com", "@jay.bsky.team", "https://bsky.app/profile/nytimes.com"],
  "followerSampleSize": 1000,
  "postsToAnalyze": 200
}
```

### Methodology

**Follower sample.** Bluesky lists followers newest first, so the sample is the account's most recent followers. That is where bought or bot followers usually show up, but it is not a random sample of the whole audience. A long-established audience may look better or worse than recent arrivals. Each follower's full profile is read to get their post, follower and following counts.

**Suspicious.** A follower is suspicious when at least one of these is true:

- It carries a spam-type moderation label from a labeler other than itself: `spam`, `impersonation`, `scam`, `inauthentic`, `misleading`, `engagement-farming`, `!hide`, `!takedown` or `!warn`.
- Its handle no longer resolves (`handle.invalid`).
- It is mass-following: it follows 1,000+ accounts and has fewer than a tenth as many followers.
- It has never posted, follows 500+ accounts and has fewer than 10 followers.

**Ghost.** Not suspicious, but has never posted. These are usually lurkers or abandoned signups. Big accounts collect many of them through onboarding and starter packs, so ghosts weigh much less than suspicious followers. New accounts and empty profiles on their own are reported but never counted against the account.

**Engagement rate.** Interactions are likes + reposts + replies + quotes. They are measured on the account's own top-level posts and quotes, not on replies or reposts. The rate is the median interactions per post divided by followers, so one viral post cannot carry it (`avgEngagementRate` is there too). Posts younger than 24 hours are left out when at least five older ones exist, because they are still collecting interactions. Engagement rates fall naturally as accounts grow, so compare accounts of similar size.

**Cadence.** Posts per week counts own posts, replies and quotes from the oldest analyzed item until now. Times use the post's own timestamp, unless it is far from when Bluesky first saw the post (imports, wrong clocks).

**Scores.**

| Score | Formula |
|---|---|
| audienceQualityScore | `100 − 150 × suspiciousShare − 40 × ghostShare`, clamped to 0 to 100; needs 20+ sampled followers |
| engagementScore | log curve over the median engagement rate: 0.01% = 0, 0.05% = 25, 0.2% = 50, 0.5% = 75, 1.5% = 100; needs 3+ posts |
| activityScore | 60 for recency (last post within 7 days = 60, 30 days = 36, 90 days = 18) + 40 × min(posts per week / 3, 1) |
| accountScore | 45% audience + 35% engagement + 20% activity, reweighted over the parts that exist |

Verdict: 75+ `strong`, 55+ `solid`, 35+ `mixed`, below that `weak`. An account that itself carries a spam-type moderation label is capped at 30. When there isn't enough data (no follower sample and fewer than two parts), the verdict is `insufficient_data`.

**Flags.** `moderation_label`, `many_suspicious_followers` (25%+), `elevated_suspicious_followers` (10%+), `spam_labeled_followers` (3%+ labeled), `mostly_ghost_followers` (50%+), `small_follower_sample` (fewer than 100 followers resolved), `low_engagement` (below 0.05% with 1,000+ followers), `engagement_from_one_post` (one post holds half the interactions), `no_posts`, `inactive_30d`, `mostly_reposts` (60%+ of the feed), `follow_back_pattern` (follows at least 80% as many accounts as follow it, with 1,000+ followers), `new_account` (under 90 days).

These are heuristics. They point you to what to look at, but they don't prove anything about any individual follower or account.

### Pricing

Pay per event:

- **$0.02 per audited account** (`account-audited`), whatever the sample size.
- **Free:** invalid input, handles or DIDs without a profile, deactivated or suspended accounts, and accounts that fail because of an API error. They appear as items with a `status` and an `errorCode`.
- A tiny start fee of $0.00005 per run.

50 accounts cost about $1. If you set a maximum cost per run, the actor stops before going over it and lists the accounts it skipped in the run summary.

### Speed

With the defaults an account takes about 5 seconds (around 27 API requests). A 5,000-follower sample takes roughly 250 requests. Requests are paced and back off automatically if Bluesky asks the actor to slow down.

### Run summary

The `RUN_SUMMARY` record in the key-value store lists audited, charged, free and skipped counts, plus request, retry and rate-limit stats.

### Data source, privacy and limits

- Data comes live from Bluesky's public AppView (`api.bsky.app`) through unauthenticated AT Protocol endpoints. It is read-only: the actor never posts, likes, follows or messages.
- Every run fetches fresh data, so deleted posts and deactivated accounts drop out automatically.
- Follower data is reduced to aggregate shares. The only individual followers listed are the largest public accounts in `notableFollowers`, and you can turn that off.
- Bluesky profiles are personal data in many jurisdictions (for example under the GDPR). You are responsible for having a legitimate reason to process them and for using the results lawfully. Don't use this actor for spam, harassment or automated interaction, and don't use it to build bulk datasets of people.
- Counts come from Bluesky's AppView and can differ slightly from what other apps show.

### Disclaimer

This actor is an independent tool and is not affiliated with, endorsed by or sponsored by Bluesky Social PBC. It only reads publicly available data. A suspicious share, score or flag is a statistical estimate, not a finding that any account is fake or acting in bad faith, so check before you act on it.

# Changelog

This Actor's version history is a separate document: https://apify.com/metallic_pastry/bluesky-account-audit/changelog.md

# Actor input Schema

## `accounts` (type: `array`):

Bluesky handles (name.bsky.social or a custom domain like nytimes.com, with or without @), DIDs (did:plc:…) or bsky.app profile/post URLs. Duplicates are removed, including a handle and its DID. Invalid entries and unknown accounts return a free item.

## `maxAccounts` (type: `integer`):

Safety cap on accounts audited in one run. Extra accounts are skipped and not charged.

## `followerSampleSize` (type: `integer`):

How many of the account's most recent followers to check for fake, spam and ghost patterns. 500 gives a stable share for most accounts; up to 5000 for a deeper check (slower, same price). 0 skips the follower audit.

## `postsToAnalyze` (type: `integer`):

How many recent feed items (posts, replies, quotes and reposts) to read for engagement rate, cadence and content mix. 0 skips the post analysis.

## `newAccountDays` (type: `integer`):

Followers whose account is younger than this count toward the newAccount share. Informational only: new accounts are not treated as fake on their own.

## `topPostsInOutput` (type: `integer`):

Number of best-performing recent posts (by likes + reposts + replies + quotes) included per account.

## `notableFollowersInOutput` (type: `integer`):

Number of the largest accounts (1,000+ followers, no spam labels) from the follower sample listed per account. 0 to leave them out.

## `concurrency` (type: `integer`):

Accounts audited in parallel. The actor keeps a polite overall request rate either way.

## Actor input object example

```json
{
  "accounts": [
    "theonion.com",
    "@jay.bsky.team",
    "https://bsky.app/profile/nytimes.com"
  ],
  "maxAccounts": 50,
  "followerSampleSize": 1000,
  "postsToAnalyze": 200,
  "newAccountDays": 14,
  "topPostsInOutput": 3,
  "notableFollowersInOutput": 10,
  "concurrency": 3
}
```

# Actor output Schema

## `overview` (type: `string`):

Default dataset, Overview view: followers, suspicious and ghost follower share, estimated real followers, engagement rate, cadence, scores and flags per account.

## `detail` (type: `string`):

Default dataset, Detail view: every field including follower signal shares, notable followers, activity, engagement, content mix and top posts.

## `runSummary` (type: `string`):

RUN_SUMMARY record in the default key-value store: audited, charged and free counts plus API request stats.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "accounts": [
        "theonion.com",
        "jay.bsky.team"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("metallic_pastry/bluesky-account-audit").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "accounts": [
        "theonion.com",
        "jay.bsky.team",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("metallic_pastry/bluesky-account-audit").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "accounts": [
    "theonion.com",
    "jay.bsky.team"
  ]
}' |
apify call metallic_pastry/bluesky-account-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,metallic_pastry/bluesky-account-audit"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/XW9b0QWromP035JTD/builds/fwO2JLrC8ZH2AnDZg/openapi.json
