# Bluesky Scraper — Profiles, Posts & Followers (`pixflor/bluesky-scraper`) Actor

Scrape Bluesky via the official AT Protocol API: profiles with follower counts, full post histories with engagement, follower and following lists, flattened reply threads, and custom feeds.

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

## Pricing

from $2.00 / 1,000 row scrapeds

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/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 as clean rows: profiles with follower counts, complete post histories with engagement
numbers, follower and following lists, flattened reply threads, and custom feeds.

No API key. No login. Paste a handle, press start, export CSV or JSON.

***

### What this does that other Bluesky scrapers don't

#### It doesn't stop after the first page

This is the trap, and it's a nasty one because the mistake is the *sensible* heuristic
everywhere else.

Ask Bluesky for 100 followers and it returns **84**. Ask again, **83**. Then **87**. On almost
any other API, receiving fewer rows than you asked for means you've reached the end — so a
scraper stops there and hands you 84 followers out of hundreds of thousands.

Every one of those short pages still carries a cursor, and the data keeps going. **On the AT
Protocol, only a missing cursor means the end.** This Actor pages on the cursor and reports how
many short pages it walked through, so you can see it happening.

#### It extracts the links that aren't in the post text

Bluesky stores URLs, hashtags and mentions in a `facets` structure, not in the post body. Read
`record.text` and you get the words with no URL attached. This Actor pulls `links`, `tags` and
`mentions` out of the facets and gives you them as fields.

#### It tells you the truth about post search

There is **no keyword post search here**, and that's deliberate.

`app.bsky.feed.searchPosts` returns 403 on Bluesky's public host. On the alternate host it
serves exactly one request, then answers *"403 Forbidden — Request forbidden by administrative
rules"* to everything after, with no rate-limit headers and no recovery. That's an
administrative block, not a rate limit.

Getting around it would mean building bot-detection evasion, which is both a losing arms race
and the sort of thing that gets an Actor pulled. So this Actor doesn't offer post search rather
than shipping a feature that breaks. Everything it does offer runs on Bluesky's official public
host and was measured stable under repeated rapid calls.

If you need posts on a topic, **search for accounts** in that niche and pull their feeds — which
this Actor does well, and which is what most people actually want anyway.

***

### What you get

**Posts** — `text`, `url`, `authorHandle`, `authorName`, `authorDid`, `createdAt`,
`likeCount`, `repostCount`, `replyCount`, `quoteCount`, plus a combined **`engagement`**
figure, `isReply`, `isRepost`, `hasEmbed`, `embedType`, `langs`, **`links`**, **`tags`**,
**`mentions`**, `labels`.

**Profiles** — `handle`, `did`, `displayName`, `description`, `followers`, `following`,
`posts`, `createdAt`, `isVerified`, `avatarUrl`, `bannerUrl`, `url`.

**Threads** — every reply flattened into rows with `threadDepth`, so you get a table instead of
nested JSON.

***

### Modes

| Mode | What it returns |
|---|---|
| **Posts by an account** | Full post history, paged on the cursor |
| **Profiles** | Bulk profile lookup, 25 accounts per request |
| **Followers of an account** | Complete follower list |
| **Accounts someone follows** | Their following list |
| **Search for accounts** | Find people by name, keyword or topic |
| **A post and all its replies** | Whole thread, flattened with depth |
| **Who liked a post** | The accounts behind the likes |
| **A custom feed** | Any feed generator, e.g. What's Hot |

***

### Typical uses

- **Audience research.** Who follows a competitor, filtered to accounts above a follower
  threshold — that's a shortlist of people worth talking to.
- **Influencer discovery.** Search accounts by topic, sort by followers, check their real
  posting cadence and engagement rather than trusting a bio.
- **Community mapping.** Follower and following graphs around a niche.
- **Brand and sentiment monitoring.** Pull the feeds of the accounts that matter in your space
  and watch what they post.
- **Content analysis.** Engagement by post type, language, or whether it carried a link.
- **Feeding an LLM or agent.** One row per item, consistent field names, links already extracted.

***

### Reliability

Official AT Protocol public API. No HTML parsing and no bot-detection workarounds anywhere in
this Actor.

A 62-assertion live test suite runs before every release, including assertions that short pages
still carry cursors, that facet extraction still finds links, and that post search is still
blocked — so if Bluesky ever opens it up, the test says so.

***

### Pricing

Pay per event. You are charged for the run start and for each row returned — nothing else, and
platform compute is included free. Profiles are fetched 25 per request rather than one at a
time, so bulk lookups are cheap.

# Actor input Schema

## `mode` (type: `string`):

Pick one. Note there is no keyword post search: Bluesky blocks that endpoint administratively, so offering it would mean shipping something that fails. Account search, feeds and threads all work fine.

## `actors` (type: `array`):

One or more Bluesky accounts. Accepts a handle (jay.bsky.team), a DID (did:plc:...), or a bsky.app profile URL.

## `searchQuery` (type: `string`):

For 'Search for accounts', the name or keyword to find people by. In other modes it filters the rows that come back.

## `postUrl` (type: `string`):

Only used by the thread and likes modes. A bsky.app post URL or an at:// URI. A web URL is resolved to a DID automatically.

## `feedUri` (type: `string`):

Only used by feed mode. The at:// URI of a feed generator, e.g. at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot

## `postFilter` (type: `string`):

Only used by 'Posts by an account'. Narrows what the feed returns.

## `minLikes` (type: `integer`):

Keep only posts with at least this many likes.

## `minReposts` (type: `integer`):

Keep only posts with at least this many reposts.

## `minFollowers` (type: `integer`):

Keep only accounts with at least this many followers. Useful for filtering a follower list down to people worth contacting.

## `excludeReposts` (type: `boolean`):

Drop reposts, keeping only what the account wrote itself.

## `excludeReplies` (type: `boolean`):

Drop replies, keeping only top-level posts.

## `withLinksOnly` (type: `boolean`):

Keep only posts that link somewhere. Links live in the post's facets rather than its text, and are extracted for you.

## `lang` (type: `string`):

Keep only posts declaring this language, e.g. en, es, ja, pt.

## `sortBy` (type: `string`):

How to order the results.

## `maxItems` (type: `integer`):

How many rows to return, and what you pay for. Paging follows the cursor rather than stopping on a short page, so large values really do return complete data.

## Actor input object example

```json
{
  "mode": "authorFeed",
  "actors": [
    "jay.bsky.team",
    "pfrazee.com"
  ],
  "searchQuery": "developer",
  "postUrl": "https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l",
  "feedUri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot",
  "postFilter": "",
  "minLikes": 0,
  "minReposts": 0,
  "minFollowers": 0,
  "excludeReposts": false,
  "excludeReplies": false,
  "withLinksOnly": false,
  "lang": "en",
  "sortBy": "default",
  "maxItems": 100
}
```

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("pixflor/bluesky-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 = {}

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

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

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

```

## CLI example

```bash
echo '{}' |
apify call pixflor/bluesky-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=pixflor/bluesky-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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