# Bluesky Scraper: Posts, Profiles, Followers & Mentions (`vant47/bluesky-posts-profiles-scraper`) Actor

Search Bluesky (bsky) posts by keyword, hashtag or account, e.g. product launches or #photography. Get likes, reposts, replies, profiles, author follower counts and new-mention alerts. $0.001 per post: 1,000 posts cost $1; $0.002 per profile. No login.

- **URL**: https://apify.com/vant47/bluesky-posts-profiles-scraper.md
- **Developed by:** [fortbrand](https://apify.com/vant47) (community)
- **Categories:** Social media
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 posts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Scraper: Posts, Profiles, Followers & Mentions

For brand monitoring teams, researchers, community managers and developers who need public Bluesky posts with author context. Search topics such as product launches, open source software or #photography, follow known accounts, load specific posts and inspect public profiles without a Bluesky login.

**About $0.001 per post and $0.002 per requested profile.** Twenty posts cost about $0.02; 1,000 delivered posts cost about $1. Author follower counts are included in the post price. A small platform start charge of $0.00005 also applies. Bronze receives 5% off, Silver 10% off and Gold 20% off the row prices. There is no separate charge for the summary, duplicate records, empty results or restricted accounts.

Anonymous search has a source limit: **one page of up to 100 candidate posts per query**. Replies, language filters and visibility exclusions can reduce the number delivered. Author feeds and public custom feeds support normal pagination. This tool does not promise a complete search archive or the entire social graph.

### What you get

| Record | Included information | Useful for |
| --- | --- | --- |
| Post | Text, original timestamp, stable AT URI, CID, author handle and public follower count | Topic monitoring and warehouse joins |
| Engagement | Separate likes, reposts, replies and quotes | Comparing current response to content |
| Rich text | Structured links, hashtags and mentioned account DIDs | Finding shared domains and linked discussions |
| Media | Image URLs and alt text, video playlist when supplied, external card metadata | Content analysis and editorial review |
| Conversation | Reply parent, root URI, quoted-post URI and optional reply sample | Reading public discussion context |
| Profile | Description, follower/following/post counts, account creation time, avatar and banner | Reviewing a known public account |
| Watch | Suppression of records already delivered for a stable watch ID | Repeated collection with fewer duplicate charges |
| Summary | Delivered totals, authors, top hashtags, engagement, exclusions and coverage notes | Checking whether the run covered the requested job |

Missing source values stay `null`. No email, telephone, private messaging or identity enrichment service is used.

### Start in three steps

1. Enter one or more `searchTerms`, or supply `handles` for an author feed. Optional `startUrls` accept public post, profile, search or custom-feed links.
2. Set `maxItems`, the maximum total number of paid rows across the whole run. Start with 20 and leave reply expansion off for a first look.
3. Start the Actor and open its dataset. Export JSON, CSV or Excel, and read the free `SUMMARY` record for exclusions and source limits.

The tested default returned **20 posts in 5.3 seconds** on 29 October 2026. Reply expansion, many sources, watch history and source response times affect duration.

```json
{
  "searchTerms": ["bluesky"],
  "sort": "latest",
  "maxItems": 20,
  "includeReplies": false
}
```

### Choose the right source

`searchTerms` discover posts using Bluesky search syntax. Use keywords, hashtags, quoted phrases, `from:bsky.app` and inline `since:` / `until:` dates. Date bounds intersect with date inputs. For dated account searches, include a keyword or use `handles`. Queries that the public API rejects produce free source notes. Inputs are deduplicated; `matchedTerms` lists queries that actually returned a record in the inspected pages.

`handles` collect an account's own posts. The Actor excludes records present solely because the account reposted somebody else. Set `includeReplies` to include its reply posts as well. Follower enrichment is included, but this input does not create an additional paid profile row.

A profile URL in `startUrls` returns one **profile row only**. A post URL returns that post, with optional reply expansion. A custom-feed URL reads the public feed in its supplied order. A search URL uses the query from its `q` parameter. Plain URL strings and objects containing `url` are both accepted. AT URIs for posts and feed generators are accepted too.

```json
{
  "handles": ["bsky.app", "nature.com"],
  "maxItems": 50,
  "includeReplies": false
}
```

```json
{
  "startUrls": [
    "https://bsky.app/profile/bsky.app",
    {"url": "https://bsky.app/profile/nature.com"}
  ],
  "maxItems": 2
}
```

### Input reference

| Input | Default | Meaning |
| --- | --- | --- |
| `searchTerms` | `bluesky` in the prefill | Array of keywords or native search queries |
| `handles` | Empty | Array of account handles or DIDs whose own posts to collect |
| `startUrls` | Empty | Optional public post, profile, search or feed links |
| `sort` | `latest` | `latest` or `top`, for search results only |
| `since` | Unset | Inclusive date or timestamp boundary for posts |
| `until` | Unset | Exclusive date or timestamp boundary for posts |
| `language` | Unset | Language code such as `en`, `de` or `pt-BR` |
| `maxItems` | 20 | Global paid-row cap, from 1 to 10,000 |
| `includeReplies` | false | Allow reply posts and expand selected roots |
| `maxRepliesPerPost` | 5 | Maximum replies inspected per root, from 0 to 100 |
| `onlyNewSinceLastRun` | false | Enable delivery history for this watch |
| `watchId` | `default` | Stable monitoring name using letters, digits, underscores or hyphens |
| `proxyConfiguration` | No proxy | Optional supported Apify or custom proxy configuration |

Dates without a time mean midnight UTC. For all of 28 October, use `since: "2026-10-28"` and `until: "2026-10-29"`. Post timestamps are checked locally as well as using available search filters. The source search index can use a sorting timestamp different from the original post timestamp, so a filtered search is not an archival completeness guarantee. Date and language filters apply to posts, not profile rows.

### Post fields

Selected fields from a real 29 October 2026 run:

```json
{
  "verdict": "2,979 likes, 325 reposts, 193 replies; 35,430,011 author followers.",
  "type": "post",
  "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3myx7vphq3s2j",
  "text": "getting our Halloween decorations set up",
  "createdAt": "2026-10-28T16:58:02.827Z",
  "authorHandle": "bsky.app",
  "authorFollowers": 35430011,
  "likes": 2979,
  "reposts": 325,
  "replies": 193,
  "quotes": 110,
  "isNewSinceLastRun": null,
  "url": "https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur/post/3myx7vphq3s2j"
}
```

| Field | Description |
| --- | --- |
| `type` | `post` |
| `uri`, `cid`, `url` | Stable record identity, content identifier and public link |
| `text`, `createdAt`, `indexedAt`, `observedAt` | Source text, original time, indexing time and collection time |
| `authorDid`, `authorHandle`, `authorDisplayName` | Public author identity supplied by the source |
| `authorFollowers`, `authorFollowing`, `authorPosts` | Public author counts from profile enrichment |
| `authorCreatedAt`, `authorAvatar`, `authorEnriched` | Extra author context and enrichment status |
| `likes`, `reposts`, `replies`, `quotes` | Separate source counts at collection time |
| `engagement` | Sum of the four counts when all four are available |
| `language` | Source language-code array |
| `links`, `hashtags`, `mentions` | Structured rich-text facets; mentions contain DIDs |
| `images`, `video`, `external` | Available image metadata, video view and external card |
| `isReply`, `replyTo`, `replyRoot` | Conversation relationships |
| `quoteUri`, `quoteUrl` | Referenced quoted record, without copying its content |
| `matchedTerms`, `sourceInputs` | Observed query matches and collection provenance |
| `sourcePostUri` | Root selected for optional reply expansion, otherwise null |
| `isNewSinceLastRun` | true for newly delivered watch rows; null when watches are off |
| `labels` | Public source labels attached to the post |

Profile rows contain `type`, `did`, `handle`, `displayName`, `description`, `followers`, `following`, `posts`, `createdAt` and `url`, plus available avatar, banner, labels, verification and observation time. A profile lookup is not proof of a person's identity or control of another website.

The dataset includes free `note` and `SUMMARY` rows. Filter by `type` before importing. ## Monitor mentions every day

Save a task with a narrow query, enable `onlyNewSinceLastRun` and keep the same `watchId` on each scheduled run. The first run delivers qualifying records and establishes the history. Later runs suppress records that this watch has already delivered. A post found through two inputs is still delivered and charged only once within the run.

```json
{
  "searchTerms": ["from:bsky.app"],
  "sort": "latest",
  "maxItems": 20,
  "onlyNewSinceLastRun": true,
  "watchId": "official-updates"
}
```

Run a watch sequentially. Do not overlap scheduled runs using the same watch ID. An unfinished run blocks reuse of that watch so its pending delivery is not silently repeated. Resume the original run or use a different ID for an independent history. History records delivery, rather than every post that existed between schedules. If a query produces more than a public search page between runs, some posts may be missed. Run narrower queries frequently enough for the topic, or monitor known author feeds. Engagement changes on an old post do not make it a new record; switch watches off to take repeated engagement snapshots.

### Pricing examples

| Delivered records | Standard row price |
| --- | --- |
| 20 posts | $0.02 |
| 100 posts | $0.10 |
| 1,000 posts across eligible sources | $1.00 |
| 20 requested profiles | $0.04 |
| 100 posts and 10 profiles | $0.12 |

Add the small start charge, then apply the relevant row discount. Replies cost the same as posts. `maxItems` includes replies and requested profiles, so reply expansion does not exceed that global row cap. Your maximum total charge is also respected: the Actor stops cleanly when the remaining budget cannot cover another eligible row. A record is saved before its corresponding row charge is made. Empty results, failures and note rows have no post or profile charge.

### Use with AI agents (MCP)

Connect your agent to `https://mcp.apify.com`. Ask it to inspect the input schema and pricing, then request: “Collect 20 public Bluesky posts about open source software with author follower counts, separate repost and quote counts, links and hashtags.” ## API and integrations

Use an Apify token from your environment; no Bluesky token is needed.

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('vant47/bluesky-posts-profiles-scraper').call({
  searchTerms: ['open source'], maxItems: 20, sort: 'latest'
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const posts = items.filter(row => row.type === 'post');
```

The REST endpoint is `POST https://api.apify.com/v2/acts/vant47~bluesky-posts-profiles-scraper/runs`, using an Authorization bearer header and a JSON input body. Poll the returned run ID, then fetch `defaultDatasetId`. The `SUMMARY` record is also available in the default key-value store.

Schedules, Make, Zapier and n8n can trigger tasks and consume webhooks. Upsert by `type` plus `uri` or `did`; preserve `observedAt` for snapshot comparisons.

### Limits and FAQ

**Does it collect follower lists?** No. “Followers” refers to public follower counts in posts and profiles. This Actor does not enumerate follower or following identities.

**Does it require login or solve access challenges?** No. It uses standard signed-out AppView requests. It does not log in, solve challenges or bypass denied search pagination. Accounts and posts carrying `!no-unauthenticated` are excluded, including during author enrichment.

**Can it collect every reply?** No. Reply expansion is a bounded sample of the thread returned by the source, with a maximum depth of six and your per-root cap. Deleted, blocked, restricted and unavailable replies are omitted. The global row limit can stop expansion sooner.

**Why did I receive fewer rows?** Search boundaries, filters, delivery history, source availability and budget can reduce output. Read the summary before increasing the limit.

**Are counts live?** They are source snapshots taken during the run. AppView caching and indexing can lag the website, and counters can change between collection and inspection. The website can display combined repost-and-quote totals, while this output keeps them separate.

**Does a reboot duplicate data or charges?** Saved dataset rows act as the delivery ledger. On restart the Actor reconciles platform charge counts and acknowledged receipts before continuing. Duplicate inputs are free; an ambiguous billing state stops further delivery instead of guessing.

**What happens if a source is unavailable?** Permitted transient retries are paced, rate-limit advice is honoured, and unavailable data produces free notes. Partial results stay available. Check `status`, `notes`, `sources` and `exclusions` in the summary before treating a run as complete.

# Actor input Schema

## `searchTerms` (type: `array`):

Keywords, hashtags or native Bluesky search syntax, including inline since:/until: dates. For dated account searches, include a text keyword or use handles. Anonymous search allows one page of up to 100 candidates per query; filters may reduce delivery.

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

Public handles or DIDs whose own posts to collect. Reposts by these accounts are excluded. For a profile row, use its profile URL in startUrls.

## `startUrls` (type: `array`):

Optional array of bsky.app post, profile, search or custom-feed URLs. Strings and objects with url are accepted. Profile URLs return one profile row, not an author feed. AT URIs are accepted too.

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

latest prioritizes recency; top uses Bluesky relevance. Applies to search only.

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

Optional UTC date or timezone-aware timestamp. Only posts on or after this time are delivered.

## `until` (type: `string`):

Optional UTC date or timezone-aware timestamp. Posts at or after this time are excluded. A date means midnight UTC at the start of that day.

## `language` (type: `string`):

Optional language code such as en, de or pt-BR. Applied to post language tags from Bluesky.

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

Global maximum across all post, reply and profile rows combined, from 1 to 10,000. Anonymous search is still limited to 100 candidates per query. Free notes and summary do not count.

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

Allow reply posts and collect a bounded reply sample for each selected root post. Each delivered reply is charged as a post and counts toward maxItems.

## `maxRepliesPerPost` (type: `integer`):

Maximum returned thread replies to inspect per root, from 0 to 100. Thread depth is limited to 6. Global maxItems still applies.

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

Suppress posts and profiles previously delivered for this watchId. Use sequential runs. First run establishes history; records delivered then count as new.

## `watchId` (type: `string`):

Stable name for a monitoring history, 1 to 80 letters, digits, underscores or hyphens. Different watch IDs have independent histories.

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

Optional custom or Apify proxy settings. Default is ordinary cloud datacenter egress without a proxy; verified to work for all supported routes. No residential proxy is enabled automatically.

## Actor input object example

```json
{
  "searchTerms": [
    "bluesky"
  ],
  "handles": [],
  "startUrls": [],
  "sort": "latest",
  "maxItems": 20,
  "includeReplies": false,
  "maxRepliesPerPost": 5,
  "onlyNewSinceLastRun": false,
  "watchId": "default",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

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

No description

# 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 = {
    "searchTerms": [
        "bluesky"
    ],
    "handles": [],
    "startUrls": [],
    "sort": "latest",
    "maxItems": 20,
    "includeReplies": false,
    "maxRepliesPerPost": 5,
    "onlyNewSinceLastRun": false,
    "watchId": "default",
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("vant47/bluesky-posts-profiles-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 = {
    "searchTerms": ["bluesky"],
    "handles": [],
    "startUrls": [],
    "sort": "latest",
    "maxItems": 20,
    "includeReplies": False,
    "maxRepliesPerPost": 5,
    "onlyNewSinceLastRun": False,
    "watchId": "default",
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("vant47/bluesky-posts-profiles-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 '{
  "searchTerms": [
    "bluesky"
  ],
  "handles": [],
  "startUrls": [],
  "sort": "latest",
  "maxItems": 20,
  "includeReplies": false,
  "maxRepliesPerPost": 5,
  "onlyNewSinceLastRun": false,
  "watchId": "default",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call vant47/bluesky-posts-profiles-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,vant47/bluesky-posts-profiles-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/OCQ0d2AYJx8YjtSAV/builds/hnXqVuuixFpm8h1Q6/openapi.json
