# Bluesky Scraper: Profiles, Posts & Followers (`arman-bd/bluesky-profile-scraper`) Actor

Scrape Bluesky through the public AT Protocol API: profiles, follower and following counts, post history, engagement and bios. Open protocol, no authentication.

- **URL**: https://apify.com/arman-bd/bluesky-profile-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Social media, Marketing, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 profile scrapeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bluesky Scraper: Profiles, Posts & Followers

![Bluesky Scraper: Public Bluesky profiles, handle, bio, follower counts, recent posts and moderation labels](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/bluesky-profile-scraper.jpg)

**Bluesky Scraper** reads public Bluesky profiles through the **AT Protocol AppView**, handle, display name, bio, avatar, banner, follower and following counts, post count, account age, moderation labels, and optionally the account's recent posts with engagement counts and its follower list.

Bluesky is an open protocol, and its public AppView answers public reads. **No proxy setup, no browser, no credentials to manage.**

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/bluesky-profile-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/bluesky-profile-scraper.md
```

### What you get

| Output field | Meaning |
|---|---|
| `did` | The account's permanent decentralised identifier, the only stable key. Handles change; DIDs do not |
| `handle` | Current handle, e.g. `jay.bsky.team` |
| `displayName` | Chosen display name, `null` if unset |
| `description` | Profile bio |
| `avatar`, `banner` | CDN image URLs, `null` if unset |
| `followersCount`, `followsCount` | Followers and following |
| `postsCount` | Lifetime post count as the AppView has indexed it |
| `createdAt` | Account creation time, account age for influence scoring |
| `posts` | Array of recent posts when `includePosts` is on, otherwise `null` |
| `followers` | Array of follower records when `includeFollowers` is on, otherwise `null` |
| `labels` | Moderation label values applied to the account, e.g. `!no-unauthenticated` |
| `indexedAt` | When the AppView last reindexed this profile |
| `scrapedAt` | Run timestamp |

Each entry in `posts` carries `uri`, `cid`, `text`, `createdAt`, `indexedAt`, `langs`, `replyCount`, `repostCount`, `likeCount`, `quoteCount`, plus `isRepost` and `isReply` flags.

Each entry in `followers` carries `did`, `handle`, `displayName`, `avatar` and `createdAt`.

A `RUN_SUMMARY` record in the key-value store holds per-run counts, the options applied and every handle that could not be resolved.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `handles` | array | - | **Required.** Handles, `@handles`, profile URLs or raw DIDs. Mixed input is fine, everything is normalised. |
| `includePosts` | boolean | `false` | Fetch the author's recent posts with engagement counts. |
| `maxPostsPerProfile` | integer | `50` | Cap posts per profile. Only used when `includePosts` is on. |
| `includeFollowers` | boolean | `false` | Fetch the profile's followers, capped at 500 per profile. |

**Which combinations make sense**

- `handles` alone is the cheapest and fastest mode: 25 profiles per request, no follow-up calls. Use it for scheduled follower-count tracking.
- `includePosts` costs one extra request per 100 posts per profile. `maxPostsPerProfile: 50` is a good default for engagement analysis; go higher only if you need history.
- `includeFollowers` is the expensive option, 5 extra requests per profile at the 500 cap. Turn it on for a small, deliberate handle list, not a bulk sweep.

All four input forms resolve to the same profile:

| You pass | Resolves to |
|---|---|
| `bsky.app` | `bsky.app` |
| `@bsky.app` | `bsky.app` |
| `https://bsky.app/profile/bsky.app` | `bsky.app` |
| `did:plc:z72i7hdynmk6r22z27h6tvur` | `bsky.app` |

Passing several forms of the same account is safe, records are de-duplicated by DID before anything is saved, so you are never billed twice for one profile.

**Example input**

```json
{
 "handles": ["bsky.app", "@jay.bsky.team", "https://bsky.app/profile/pfrazee.com"],
 "includePosts": true,
 "maxPostsPerProfile": 5,
 "includeFollowers": false
}
```

### Output

A real record from the run above, with `posts` abridged to one entry:

```json
{
 "did": "did:plc:oky5czdrnfjpqslsw2a5iclo",
 "handle": "jay.bsky.team",
 "displayName": "Jay 🦋",
 "description": "Founder & Chief Innovation Officer @ Bluesky\n\nWorking on @attie.ai\n\n🌱 🪴 🌳",
 "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkrei….jpeg",
 "banner": "https://cdn.bsky.app/img/banner/plain/did:plc:oky5czdrnfjpqslsw2a5iclo/bafkrei….jpeg",
 "followersCount": 595228,
 "followsCount": 3974,
 "postsCount": 4110,
 "createdAt": "2022-11-17T06:31:40.296Z",
 "posts": [
 {
 "uri": "at://did:plc:v7lt2fu7igydcjszoexxrvl2/app.bsky.feed.post/3ms7bligcf22z",
 "cid": "bafyreie46c5unflqnrqi7xw3uzotpxzbobsmqul3iolbgnrth6f352jtym",
 "text": "New corner of the internet, zero idea who to follow? …",
 "createdAt": "2026-08-03T19:53:03.901Z",
 "indexedAt": "2026-08-03T19:53:05.435Z",
 "langs": ["en"],
 "replyCount": 9,
 "repostCount": 9,
 "likeCount": 72,
 "quoteCount": 1,
 "isRepost": true,
 "isReply": false
 }
 ],
 "followers": null,
 "labels": [],
 "indexedAt": "2026-03-29T21:16:33.460Z",
 "scrapedAt": "2026-08-06T11:35:03.807Z"
}
```

`RUN_SUMMARY`, from a run that deliberately included one bad handle:

```json
{
 "handlesRequested": 5,
 "handlesFailed": 1,
 "failures": [
 { "handle": "definitely-not-real-xyz123.bsky.social", "error": "profile not found" }
 ],
 "profilesSaved": 3,
 "filters": {
 "handles": ["bsky.app", "jay.bsky.team", "pfrazee.com", "did:plc:z72i7hdynmk6r22z27h6tvur", "definitely-not-real-xyz123.bsky.social"],
 "includePosts": true,
 "maxPostsPerProfile": 5,
 "includeFollowers": true,
 "maxFollowersPerProfile": 500
 },
 "finishedAt": "2026-08-06T11:35:40.714Z"
}
```

Five handles in, three profiles out: one was a bad handle, and `did:plc:z72i7hdynmk6r22z27h6tvur` is `bsky.app` again, so it collapsed into a single record.

### Use cases

**1. Track influencers on Bluesky.** Run a fixed handle list on a schedule and diff `followersCount` and `postsCount` over `scrapedAt` to get growth curves.

```json
{
 "handles": ["bsky.app", "jay.bsky.team", "pfrazee.com"],
 "includePosts": false,
 "includeFollowers": false
}
```

**2. Social listening on an open network.** Pull recent posts with engagement counts for a watchlist and rank by `likeCount + repostCount`. `isRepost` lets you separate original output from amplification.

```json
{
 "handles": ["bsky.app", "jay.bsky.team"],
 "includePosts": true,
 "maxPostsPerProfile": 100
}
```

**3. Build a creator-discovery tool.** Fetch a seed account's followers, then feed those handles back in as a second run to score them on follower count, post volume and account age.

```json
{
 "handles": ["pfrazee.com"],
 "includeFollowers": true
}
```

### Limits and behaviour

- **Use `public.api.bsky.app`.** The `bsky.social` host returns 403 to unauthenticated reads; this Actor never touches it.
- **Profiles are batched 25 per request.** A 100-handle run costs 4 profile calls, not 100.
- **Unknown handles are dropped, not errored.** `getProfiles` returns 200 with the missing account simply absent, so the Actor diffs requested against returned and records each miss in `RUN_SUMMARY.failures`.
- **De-duplicated by DID.** A handle and its own DID are different inputs but the same account; only one record is saved.
- **Follower collection stops at 500 per profile.** Follower lists are unbounded, `bsky.app` has over 34 million, so an uncapped fetch would run until the Actor timed out. Bluesky returns followers newest-first.
- **The author feed includes reposts and replies.** They are flagged with `isRepost` / `isReply` rather than dropped, so `postsCount` and `posts.length` will not always agree. Filter client-side if you want originals only.
- **A failing profile never aborts the run.** Posts or followers failing for one account are logged, recorded in `RUN_SUMMARY.failures`, and the profile is still saved with that field set to `[]`. The run only errors out if *no* profile could be resolved at all.
- **Transient errors are retried.** 429 and 5xx get three attempts with linear backoff. A 400 `InvalidRequest` fails fast, because retrying an invalid handle never helps.
- **Public data only.** No authentication, no personal data beyond what the account publishes, no access-control bypass. Accounts carrying the `!no-unauthenticated` label expose less through the public AppView by design, and that shows up in `labels`.

### FAQ

**Do I need a proxy?** No. Proxy configuration is not required to run this Actor.

**Do I need a Bluesky account or app password?** No. You supply no credentials.

**What happens if a handle is wrong?** It is reported in `RUN_SUMMARY.failures` and the run continues. The run only fails outright if not a single profile resolved.

**Can I schedule it?** Yes, it is designed for scheduled runs. Follower-count time series is the most common pattern; use `did` as your join key, since handles change.

**Why does `posts.length` not match `postsCount`?** `postsCount` is the account's lifetime total; `posts` is capped by `maxPostsPerProfile` and includes reposts and replies from the author feed.

**Can I get more than 500 followers per profile?** Not in one run. The cap keeps runtime bounded. For a large follower graph, page through it yourself with the follower `did`s from consecutive runs.

**Can I get the accounts someone follows, rather than their followers?** Not currently, `followsCount` is included, but the follows list is not.

**Are DIDs or handles the right key?** DIDs. Handles are user-changeable domain names; the `did:plc:` identifier is permanent.

**Can I integrate it with something else?** Yes, Apify API, client libraries, webhooks, scheduled runs, dataset exports (JSON/CSV/Excel) or MCP. Output is structured JSON.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~bluesky-profile-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "handles": ["bsky.app", "jay.bsky.team"],
 "includePosts": true,
 "maxPostsPerProfile": 25
 }'
```

### JavaScript example

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

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/bluesky-profile-scraper').call({
 handles: ['bsky.app', '@jay.bsky.team', 'https://bsky.app/profile/pfrazee.com'],
 includePosts: true,
 maxPostsPerProfile: 25,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const p of items) console.log(`${p.handle}, ${p.followersCount} followers, ${p.postsCount} posts`);
```

# Actor input Schema

## `handles` (type: `array`):

Bluesky handles ('bsky.app'), @handles, full profile URLs ('https://bsky.app/profile/bsky.app') or raw DIDs ('did:plc:..'). Everything is normalised automatically. Profiles are resolved 25 per request.

## `includePosts` (type: `boolean`):

Fetch the author's recent posts with engagement counts. Adds one request per 100 posts per profile.

## `maxPostsPerProfile` (type: `integer`):

Cap the posts collected per profile. Only used when 'Include recent posts' is on. The author feed also carries the account's reposts and replies, which are flagged rather than dropped.

## `includeFollowers` (type: `boolean`):

Fetch the profile's followers. Collection stops at 500 per profile. follower lists are unbounded and a popular account would otherwise run until timeout.

## Actor input object example

```json
{
  "handles": [
    "bsky.app",
    "@pfrazee.com",
    "https://bsky.app/profile/jay.bsky.team"
  ],
  "includePosts": false,
  "maxPostsPerProfile": 50,
  "includeFollowers": false
}
```

# Actor output Schema

## `items` (type: `string`):

Every record the run produced.

## `runsummary` (type: `string`):

The RUN\_SUMMARY record from the run's key-value store.

# 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 = {
    "handles": [
        "bsky.app",
        "jay.bsky.team"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/bluesky-profile-scraper").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 = { "handles": [
        "bsky.app",
        "jay.bsky.team",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/bluesky-profile-scraper").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 '{
  "handles": [
    "bsky.app",
    "jay.bsky.team"
  ]
}' |
apify call arman-bd/bluesky-profile-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arman-bd/bluesky-profile-scraper"
        }
    }
}

```

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/lsCoZJhyCXanBiSuV/builds/YDQlEmH0GBPtH6bWd/openapi.json
