# Bluesky Posts, Profiles & Followers Scraper (`owlabee/bluesky-monitor`) Actor

Search Bluesky posts by keyword or hashtag, scrape profiles with follower and post counts, author feeds, follower and following lists from the public AT Protocol API. Flat JSON/CSV, no login or proxies. Pay per post ($0.0005) or profile ($0.002).

- **URL**: https://apify.com/owlabee/bluesky-monitor.md
- **Developed by:** [Owlabee](https://apify.com/owlabee) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 post 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/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 Posts, Profiles & Followers Scraper

### What it does

This Actor pulls public data out of **Bluesky** through the official, unauthenticated **AT Protocol API** (the `public.api.bsky.app` AppView, with its origin `api.bsky.app` as fallback for post search) and writes it as flat, analysis-ready records: posts matching keywords or hashtags, full profiles with follower / following / post counts, the posts of specific accounts, and complete follower and following lists. Every record is normalized the same way regardless of mode, so you can drop the dataset straight into a spreadsheet, a BI tool, a CRM or an LLM pipeline. Apify gives you the results as JSON, CSV, Excel, an API endpoint, webhooks, or through the Apify MCP server for AI agents, at no extra cost.

It is built for brand and reputation monitoring, social listening, competitor and influencer research, audience analysis, and journalists or researchers who need Bluesky data without writing AT Protocol code. Turn on **Only new since last run** and schedule the Actor, and it becomes a monitor that emits only posts, followers or profile changes that appeared since the previous run and can POST them to your webhook.

No login, no API key, no proxies, no browser: it talks to the same public endpoints the Bluesky app uses, politely rate-limited.

### How to use

1. Pick a **Mode** and enter your **Queries**, one per line: keywords or hashtags for `searchPosts` / `searchActors`, handles (`bsky.app`), DIDs or profile URLs for `profiles`, `authorFeed`, `followers` and `following`.
2. Optionally set **Max results**, **Sort** and **Since** (for post modes), and a **Webhook URL**. For scheduled monitoring enable **Only new since last run**.
3. Click **Start**. Records appear in the dataset as they are found; download them or connect via API, integrations or MCP.

### Input

| Field | Type | Required | Description |
|---|---|---|---|
| `mode` | string | yes | `authorFeed` (default), `profiles`, `followers`, `following`, `searchActors` or `searchPosts` |
| `queries` | string\[] | yes | Keywords / hashtags, or handles / DIDs / profile URLs depending on `mode` |
| `maxResults` | integer | no | Upper bound on dataset items across all queries (default 200); `searchPosts` additionally stops at 100 posts per query, see Limits |
| `sort` | string | no | `latest` (default) or `top`; `searchPosts` only |
| `since` | string | no | ISO date or datetime; only posts created on or after it (`searchPosts`, `authorFeed`) |
| `onlyNewSinceLastRun` | boolean | no | Emit only records not seen by previous runs (default `false`) |
| `stateStoreName` | string | no | Named key-value store for the monitor memory (default `bluesky-monitor-state`) |
| `webhookUrl` | string | no | POST target for the run summary and up to 100 records |

### Output example

Post record (`searchPosts`, `authorFeed`):

```json
{
  "type": "post",
  "query": "apify",
  "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l",
  "cid": "bafyreihqbw4xqgzvrjj7mcjf4wq6zq4b5j6m3i3f6zkr3vfyhdgq4i2z3q",
  "rkey": "3l6oveex3ii2l",
  "authorHandle": "bsky.app",
  "authorDid": "did:plc:z72i7hdynmk6r22z27h6tvur",
  "authorDisplayName": "Bluesky",
  "authorAvatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihagr2c...@jpeg",
  "text": "We shipped a new Apify integration today! Try the open API: https://docs.bsky.app",
  "createdAt": "2026-10-15T18:02:11.482Z",
  "indexedAt": "2026-10-15T18:02:12.000Z",
  "likeCount": 310,
  "repostCount": 40,
  "replyCount": 12,
  "quoteCount": 5,
  "langs": ["en"],
  "hasImages": true,
  "hasVideo": false,
  "embedUrls": ["https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreia@jpeg"],
  "externalUrl": null,
  "quotedPostUri": null,
  "quotedPostUrl": null,
  "isReply": false,
  "replyParentUri": null,
  "replyRootUri": null,
  "isRepost": false,
  "repostedByHandle": null,
  "repostedByDid": null,
  "labels": [],
  "postUrl": "https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l",
  "fetchedAt": "2026-10-16T12:40:32.064Z"
}
```

Profile record (`profiles`, `searchActors`):

```json
{
  "type": "profile",
  "query": "bsky.app",
  "did": "did:plc:z72i7hdynmk6r22z27h6tvur",
  "handle": "bsky.app",
  "displayName": "Bluesky",
  "description": "official Bluesky account (check username👆)\n\nBugs, feature requests, feedback: support@bsky.app",
  "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihagr2c...@jpeg",
  "banner": "https://cdn.bsky.app/img/banner/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreichzyov...@jpeg",
  "followersCount": 15123456,
  "followsCount": 20,
  "postsCount": 1234,
  "createdAt": "2023-04-12T04:53:57.057Z",
  "indexedAt": "2026-10-01T00:00:00.000Z",
  "labels": [],
  "profileUrl": "https://bsky.app/profile/bsky.app",
  "fetchedAt": "2026-10-16T12:41:04.001Z"
}
```

Follower records (`followers`, `following`) have `type` `follower` or `following`, `subjectHandle` / `subjectDid` (the account you asked about), and the follower's `did`, `handle`, `displayName`, `description`, `avatar`, `createdAt`, `profileUrl`. Bluesky's follower lists do not include per-follower counts; run the `profiles` mode on the handles you care about to enrich them.

Error items look like `{ "type": "error", "query": "...", "mode": "profiles", "error": "PROFILE_NOT_FOUND", "errorMessage": "..." }` (`INVALID_HANDLE`, `PROFILE_NOT_FOUND`, `SEARCH_FORBIDDEN`, `RATE_LIMITED`, `FETCH_FAILED`) and are charged at the same rate as a successful record, because the work to discover the problem was done.

### Pricing

Pay per event. You only pay for the records the Actor produces; set a **maximum cost per run** in the run options to cap spend.

| Event | Price | Charged in |
|---|---|---|
| Post scraped | $0.0005 | `searchPosts`, `authorFeed` |
| Profile scraped | $0.002 | `profiles`, `searchActors` |
| Follower scraped | $0.0005 | `followers`, `following` |
| Actor start | $0.00005 | Apify platform event, every run |

**Examples:** 1,000 posts ≈ **$0.50**; 1,000 full profiles ≈ **$2.00**; 10,000 followers ≈ **$5.00**. Error items cost the same as the record they stand in for. With **Only new since last run**, the first (baseline) run charges for every record it emits; later runs charge only for new records, so a quiet keyword costs you nothing but the Actor start fee.

### Limits and fair use

- Requests are rate-limited to about 2 per second against the public Bluesky AppView, with exponential backoff on `429` / `5xx`. One request returns up to 100 posts or followers (25 profiles), so 10,000 followers take roughly one minute.
- `searchPosts` is backed by Bluesky's search index; very broad terms are capped by the index itself and `top` ordering is Bluesky's relevance ranking. Use `since` and `latest` for exhaustive recent coverage.
- **Post search is capped at 100 posts per query from datacenter networks.** The CDN in front of `public.api.bsky.app` answers `app.bsky.feed.searchPosts` with an HTML 403 page from datacenter networks (Apify's included) while every other method is served normally. The Actor recognizes that page and retries the call on the AppView origin `api.bsky.app`, the same unauthenticated service that Bluesky documents alongside the public host. The origin serves the first page of results (up to 100 posts per query, newest first with `latest`, best first with `top`) but refuses cursor pagination without a login, so each query stops there; with `since`, narrower keywords, several queries, or **Only new since last run** on a schedule you keep up with the newest matches. Only if both hosts refuse does the run continue with one `SEARCH_FORBIDDEN` error item per query (charged as one post) instead of failing. The default and daily-test mode is `authorFeed`.
- Only public data is accessed: posts and profiles whose authors opted out of logged-out visibility (`!no-unauthenticated` label) may be partially or entirely absent, exactly as on the public web.
- Memory: 256 MB is plenty; the default 1024 MB cap is never needed.
- Monitor memory keeps the most recent 10,000 record keys per query in your state store; delete that store (or change `stateStoreName`) to reset the baseline.

### Related Actors

Other pay-per-event Actors from the same account, all limited-permission and agent-friendly:

- [Podcast RSS Feed Scraper — Episodes & Transcripts](https://apify.com/owlabee/podcast-rss-scraper) — track podcast episodes and transcripts for the same brand, sponsor or topic mentions.
- [Website Change Monitor — Sitemap & Page Content Diff](https://apify.com/owlabee/website-change-monitor) — get a change feed for the websites of the accounts you follow.
- [Domain WHOIS, DNS, Email Security & TLS Checker](https://apify.com/owlabee/domain-intelligence) — enrich the domains linked from Bluesky profiles with WHOIS, DNS, email security and tech stack.

### FAQ

**Does this need proxies, a login or an API key?** No. It uses Bluesky's public, unauthenticated XRPC endpoints over plain HTTPS from Apify's standard datacenter infrastructure.

**How is cost controlled?** Set `maxResults` and the run's maximum total charge. Each record is charged the moment it is saved, and the Actor stops cleanly when either limit is reached; nothing is charged in bulk at the end.

**How does "Only new since last run" work?** The Actor keeps, per mode and query, the keys of records it already emitted in the named key-value store `stateStoreName`. On the next run it pages newest-first, emits only unseen posts / followers / changed profiles, and stops as soon as it reaches already-known records. Profiles are re-emitted only when the handle, name, bio, avatar or any count changed (`changedFields` tells you what).

**Can I search hashtags or exact phrases?** Yes: `#opensource`, `"exact phrase"`, and Bluesky's search operators such as `from:handle`, `lang:en` or `domain:example.com` are passed through to the search index. Each query returns at most its first 100 matches per run (see Limits), so split broad topics into several queries or narrow them with `since` and operators.

**Can I run it on a schedule or call it from an AI agent?** Yes. Use Apify Schedules with a webhook, the REST API, or the Apify MCP server. Example prompts: "Find the 50 latest Bluesky posts mentioning our brand since yesterday", "Get follower counts for these 20 Bluesky handles", "List everyone who started following @ourcompany since the last check."

**What happens on errors?** Bad handles, unknown profiles, a blocked search endpoint (`SEARCH_FORBIDDEN`) or temporary API failures produce error items with a clear code instead of failing the whole run; genuinely unusable input (empty queries, invalid `since`) fails fast before any paid work.

### Legal

The Actor reads only data that Bluesky publishes through its public API; respect Bluesky's terms, the AT Protocol community guidelines and applicable privacy law (GDPR, CCPA) when storing or processing personal data such as handles, names and bios. This Actor is not a consumer reporting agency and its output must not be used for employment, credit, insurance, housing or tenant screening decisions.

# Actor input Schema

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

What to scrape. `authorFeed`, `profiles`, `followers` and `following` treat Queries as Bluesky handles (e.g. `bsky.app`), DIDs or profile URLs; `searchPosts` and `searchActors` treat them as keywords or hashtags. `searchPosts` returns at most 100 posts per query per run (Bluesky's public search serves one page without a login); use `since`, narrower keywords or several queries for more.

## `queries` (type: `array`):

One entry per line. Handles, DIDs or profile URLs for `authorFeed`, `profiles`, `followers` and `following` (e.g. `bsky.app`, `did:plc:z72i7hdynmk6r22z27h6tvur`, `https://bsky.app/profile/bsky.app`); keywords or hashtags for `searchPosts` / `searchActors` (e.g. `apify`, `#opensource`, `"exact phrase"`). The prefilled value is the cheap, always-available example used by Apify's daily automated test.

## `maxResults` (type: `integer`):

Upper bound on dataset items across all queries (posts, profiles or followers depending on Mode). Use it together with the run's maximum cost to control spend.

## `sort` (type: `string`):

`latest` returns the newest matching posts first (best for monitoring); `top` returns the most engaged posts first.

## `since` (type: `string`):

Only return posts created on or after this date. ISO date (`2026-10-01`) or datetime (`2026-10-01T12:00:00Z`). Applies to `searchPosts` and `authorFeed`; ignored by the other modes.

## `onlyNewSinceLastRun` (type: `boolean`):

Monitor mode. Remembers what previous runs already returned (per Mode and query) in the state store and emits only records not seen before: new posts, new followers / follows, new or changed profiles. The first run is the baseline and emits everything. Ideal for scheduled runs with a webhook.

## `stateStoreName` (type: `string`):

Named key-value store that keeps the per-query memory for `onlyNewSinceLastRun`. Use a different name per schedule to keep monitors independent; delete the store to reset the baseline.

## `webhookUrl` (type: `string`):

If set, the Actor POSTs a JSON summary of the run plus up to 100 of the records it produced to this URL when it finishes. Failures are logged, never fatal.

## Actor input object example

```json
{
  "mode": "authorFeed",
  "queries": [
    "bsky.app"
  ],
  "maxResults": 20,
  "sort": "latest",
  "onlyNewSinceLastRun": false,
  "stateStoreName": "bluesky-monitor-state"
}
```

# Actor output Schema

## `results` (type: `string`):

One item per post, profile or follower record in the default dataset (type, query, authorHandle, text, createdAt, likeCount, repostCount, postUrl, handle, followersCount, profileUrl; error items carry error and errorMessage). Fields are described in dataset_schema.json.

## `summary` (type: `string`):

SUMMARY record in the run's default key-value store: mode, queries, items produced and charges, written when the run finishes.

# 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": "authorFeed",
    "queries": [
        "bsky.app"
    ],
    "maxResults": 20,
    "sort": "latest"
};

// Run the Actor and wait for it to finish
const run = await client.actor("owlabee/bluesky-monitor").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": "authorFeed",
    "queries": ["bsky.app"],
    "maxResults": 20,
    "sort": "latest",
}

# Run the Actor and wait for it to finish
run = client.actor("owlabee/bluesky-monitor").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": "authorFeed",
  "queries": [
    "bsky.app"
  ],
  "maxResults": 20,
  "sort": "latest"
}' |
apify call owlabee/bluesky-monitor --silent --output-dataset

```

## MCP server setup

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

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/84nrv3cMP1aiNLo97/builds/gCb40sM8SNXVrlykw/openapi.json
