# Bluesky Scraper —  (No Login) (`pioneering_saxifrage/bluesky-scraper`) Actor

Scrape public Bluesky data via the AT Protocol: profiles, posts, followers, following, search, threads, lists, starter packs and custom feeds. No account or app password required.

- **URL**: https://apify.com/pioneering\_saxifrage/bluesky-scraper.md
- **Developed by:** [DataPulse Lab](https://apify.com/pioneering_saxifrage) (community)
- **Categories:** Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

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?

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 & More (No Login)

Extract public data from **Bluesky** through the official AT Protocol AppView — **without an account, an app password, or a session token**.

Most Bluesky scrapers ask you to hand over your own credentials, which puts your account at risk of rate limits or suspension. This one talks to `public.api.bsky.app`, the read-only endpoint Bluesky serves to anonymous clients. Nothing to log in with, nothing to leak.

***

### 11 scraping modes

| # | Mode | What you get | Needs |
|---|------|--------------|-------|
| 1 | `profiles` | Display name, bio, followers/following/posts counts, verification, avatar, banner, join date, pinned post | `handles` |
| 2 | `posts` | Author timeline with likes, reposts, replies, quotes, images, video, links, hashtags, mentions | `handles` |
| 3 | `followers` | Everyone following an account, with full profile metadata | `handles` |
| 4 | `following` | Everyone an account follows | `handles` |
| 5 | `searchPeople` | Accounts matching a keyword | `searchQuery` |
| 6 | `searchPosts` | Posts matching a keyword, with date/language/sort filters | `searchQuery` |
| 7 | `postUrls` | Specific posts from their `bsky.app` links (handle → DID resolved automatically) | `urls` |
| 8 | `thread` | Full conversation: ancestors above the post and a nested reply tree below | `urls` |
| 9 | `lists` | Public lists and, optionally, every member | `handles` or `urls` |
| 10 | `starterPacks` | Starter packs, their join counts, feeds and members | `handles` or `urls` |
| 11 | `feeds` | Custom feed generators and the posts they currently serve | `handles` or `urls` |

Every run also produces a flattened **CSV export** (`OUTPUT.csv` in the run's key-value store) alongside the JSON dataset.

***

### Quick start

Press **Start** with the default input — it scrapes the `@bsky.app` profile and finishes in seconds.

Then adapt one of these:

```jsonc
// Latest 200 posts from an account
{ "mode": "posts", "handles": ["bsky.app"], "maxItems": 200 }

// Followers of several accounts, capped per account
{ "mode": "followers", "handles": ["bsky.app", "jay.bsky.team"],
  "maxItems": 5000, "maxItemsPerSource": 2500 }

// French posts about a topic, most engaged first
{ "mode": "searchPosts", "searchQuery": "intelligence artificielle",
  "searchSort": "top", "searchLanguage": "fr", "maxItems": 500 }

// An entire conversation under one post
{ "mode": "thread",
  "urls": ["https://bsky.app/profile/bsky.app/post/3lkxyzabc123"],
  "threadDepth": 10 }
```

Handles are flexible: `bsky.app`, `@bsky.app`, `did:plc:z72i7…` and `https://bsky.app/profile/bsky.app` all work.

***

### Input reference

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `mode` | enum | `profiles` | One of the 11 modes above |
| `handles` | array | `["bsky.app"]` | Handles, DIDs or profile URLs |
| `searchQuery` | string | — | Supports Bluesky search syntax: `from:handle`, `"exact phrase"`, `#hashtag` |
| `searchSort` | enum | `latest` | `latest` or `top` (mode 6) |
| `searchSince` / `searchUntil` | string | — | `YYYY-MM-DD` date bounds (mode 6) |
| `searchLanguage` | string | — | Two-letter code, e.g. `fr` (mode 6) |
| `urls` | array | — | Post / list / feed / starter-pack links or `at://` URIs |
| `maxItems` | integer | `50` | Total cap. `0` means unlimited |
| `maxItemsPerSource` | integer | — | Sub-cap so one large account can't eat the budget |
| `includeReplies` | boolean | `true` | Modes 2 and 6 |
| `includeReposts` | boolean | `true` | Mode 2 |
| `includeListMembers` | boolean | `true` | Modes 9 and 10 |
| `threadDepth` | integer | `6` | Reply levels to walk down (mode 8) |
| `threadParentHeight` | integer | `10` | Parent posts to fetch above the target (mode 8) |
| `exportCsv` | boolean | `true` | Also write `OUTPUT.csv` |
| `proxyConfiguration` | object | off | Not needed — the public API has no IP restrictions |

***

### Output samples

**Profile**

```json
{
  "type": "profile",
  "did": "did:plc:z72i7hdynmk6r22z27h6tvur",
  "handle": "bsky.app",
  "displayName": "Bluesky",
  "description": "Official Bluesky account.",
  "followersCount": 4512003,
  "followsCount": 42,
  "postsCount": 1337,
  "createdAt": "2023-04-12T04:53:57.057Z",
  "isVerified": true,
  "avatar": "https://cdn.bsky.app/img/avatar/plain/…@jpeg",
  "banner": "https://cdn.bsky.app/img/banner/plain/…@jpeg",
  "profileUrl": "https://bsky.app/profile/bsky.app"
}
```

**Post**

```json
{
  "type": "post",
  "uri": "at://did:plc:z72i7…/app.bsky.feed.post/3lkxyzabc123",
  "url": "https://bsky.app/profile/bsky.app/post/3lkxyzabc123",
  "text": "Big update shipping today #bluesky #atproto",
  "createdAt": "2026-05-01T12:00:00.000Z",
  "authorHandle": "bsky.app",
  "likeCount": 5231,
  "repostCount": 940,
  "replyCount": 128,
  "quoteCount": 77,
  "engagementTotal": 6376,
  "hashtags": ["bluesky", "atproto"],
  "mentions": [{ "did": "did:plc:…", "handle": "alice.bsky.social" }],
  "links": ["https://bsky.social/about"],
  "images": [{ "url": "…", "alt": "Screenshot", "width": 1200, "height": 675 }],
  "isReply": false,
  "isRepost": false,
  "isQuotePost": false
}
```

In `thread` mode each item carries `depth`, `parentUri` and a nested `replies` array, so you can work with the flat list or rebuild the tree.

***

### Pricing

**Pay per result — $0.003 per item.** You pay only for records actually delivered to your dataset. No monthly platform fee on top, no charge for failed runs.

| Job | Items | Cost |
|-----|-------|------|
| One profile | 1 | $0.003 |
| 1,000 posts from an account | 1,000 | $3.00 |
| 10,000 followers | 10,000 | $30.00 |

Set `maxItems` to keep any run inside a known budget.

***

### Notes & limits

- **Public data only.** Anything requiring a session — DMs, notifications, your own feed, blocked/muted lists — is out of scope by design.
- **Custom feeds (mode 11).** Feed metadata always works anonymously. A few third-party generators refuse unauthenticated reads; when that happens the Actor still returns the generator record and logs a warning instead of failing.
- **Deleted or suspended accounts** are skipped with a warning rather than aborting the run.
- **Rate limits** are handled with `retry-after`-aware backoff. The public API is generous; a proxy is rarely necessary.
- Please respect Bluesky's [Terms of Service](https://bsky.social/about/support/tos) and applicable data-protection law (GDPR included) when scraping personal data.

***

### Local development

```bash
pip install -r requirements.txt

## Offline suite: 56 tests, all 11 modes against a mocked AppView
python3 -m pytest tests/ -v

## Live check against the real API (no credentials required)
python3 tests/live_smoke.py

## Run the Actor locally
apify run --input='{"mode":"profiles","handles":["bsky.app"]}'
```

#### Project structure

```
.actor/
  actor.json           Actor manifest
  input_schema.json    Input form definition
  dataset_schema.json  Dataset views in the Apify UI
  pay_per_event.json   Pay-per-event tariff
src/
  __main__.py          Entry point (python -m src)
  main.py              Orchestration, batching, charging, CSV export
  config.py            Input normalisation and validation
  bsky_client.py       AT Protocol client (retries, pagination, DID cache)
  handlers.py          One handler per scraping mode
  transformers.py      Raw AT Proto views -> flat output records
  parsers.py           URL/AT-URI parsing, richtext facets, embeds
  csv_export.py        Nested JSON -> CSV flattening
tests/
  test_offline.py      Unit + mode tests
  test_actor_e2e.py    Full Actor run through the Apify SDK
  live_smoke.py        Real-API smoke test
Dockerfile
requirements.txt
```

# Actor input Schema

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

Pick a mode, then fill the field it needs: handles, searchQuery or urls.

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

Used by modes 1, 2, 3, 4, 9, 10 and 11. Accepts `bsky.app`, `@bsky.app`, a `did:plc:…` or a full profile URL.

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

Used by modes 5 and 6. Supports the Bluesky search syntax, e.g. `from:bsky.app`, `"exact phrase"`, `#hashtag`.

## `searchSort` (type: `string`):

Only applies to mode 6.

## `searchSince` (type: `string`):

Optional date filter for mode 6, e.g. `2026-01-01`.

## `searchUntil` (type: `string`):

Optional date filter for mode 6, e.g. `2026-06-30`.

## `searchLanguage` (type: `string`):

Optional two-letter code for mode 6, e.g. `fr`, `en`, `es`.

## `urls` (type: `array`):

Used by modes 7, 8, 9, 10 and 11. Paste bsky.app links (post, list, feed, starter pack) or raw `at://` URIs.

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

Hard cap on dataset items for the whole run. Set 0 for unlimited (careful with large accounts).

## `maxItemsPerSource` (type: `integer`):

Optional sub-limit so one big account cannot consume the whole budget.

## `includeReplies` (type: `boolean`):

Applies to modes 2 and 6.

## `includeReposts` (type: `boolean`):

Applies to mode 2.

## `includeListMembers` (type: `boolean`):

Applies to modes 9 and 10. Turn off to only get the list metadata.

## `threadDepth` (type: `integer`):

How many reply levels to walk down in mode 8 (max 1000).

## `threadParentHeight` (type: `integer`):

How many parent posts above the target to fetch in mode 8.

## `exportCsv` (type: `boolean`):

Saves a flattened `OUTPUT.csv` to the run's key-value store, alongside the JSON dataset.

## `proxyConfiguration` (type: `object`):

Optional. The public AT Protocol API needs no proxy — leave this off unless you hit rate limits.

## Actor input object example

```json
{
  "mode": "profiles",
  "handles": [
    "bsky.app"
  ],
  "searchQuery": "bluesky",
  "searchSort": "latest",
  "maxItems": 50,
  "includeReplies": true,
  "includeReposts": true,
  "includeListMembers": true,
  "threadDepth": 6,
  "threadParentHeight": 10,
  "exportCsv": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

One JSON record per scraped item (profile, post, follower, list member, thread post, etc.), stored in the run's default dataset.

## `csv` (type: `string`):

Flattened CSV version of the scraped data, stored as OUTPUT.csv in the run's default 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 = {
    "mode": "profiles",
    "handles": [
        "bsky.app"
    ],
    "searchQuery": "bluesky",
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("pioneering_saxifrage/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 = {
    "mode": "profiles",
    "handles": ["bsky.app"],
    "searchQuery": "bluesky",
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("pioneering_saxifrage/bluesky-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 '{
  "mode": "profiles",
  "handles": [
    "bsky.app"
  ],
  "searchQuery": "bluesky",
  "maxItems": 50
}' |
apify call pioneering_saxifrage/bluesky-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,pioneering_saxifrage/bluesky-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/ULeqtyWIp8pMovRq6/builds/9415PMfu49Fw7Wnm9/openapi.json
