# Bluesky Scraper | $0.50/1K | Posts & Engagement (`apivault_labs/bluesky-public-posts-scraper`) Actor

Collect public Bluesky author feeds and keyword searches with text, dates, engagement and media metadata. Latest search uses overlapping time windows, URI deduplication and bound continuation. No complete archive guarantee.

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

## Pricing

from $0.40 / 1,000 useful bluesky posts

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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 — Public Search, Author Posts & Engagement

![Bluesky public post collection illustration](https://raw.githubusercontent.com/apivault-labs/apivault-labs/a632461e35ff77d3b75659154c792a22cfaf8312/assets/bluesky-public-posts/cover-v1.png)

*Concept illustration, not actual post content or live product UI.*

Turn public Bluesky posts into structured JSON or a CSV table: original text, author identity, source dates, observed engagement and available media links. Use it for a small brand-mention sample, an author's public posting history, or research datasets that retain the original post URI.

**Private development candidate.** Initial pricing has owner approval; publication requires exact-build review. Private QA is not a public Store release. Public source access can change. The Actor does not log in, solve challenges, rotate proxies or retrieve private content.

### Start with an author feed

```json
{
  "mode": "author",
  "actor": "bsky.app",
  "maxPosts": 5,
  "maxPages": 2,
  "pageSize": 10
}
```

Supply a Bluesky handle or DID. This mode reads the public author feed, rather than searching for mentions of that person. Replies are included by default; repost entries are excluded. A two-page local source probe returned ten distinct posts, but that is not proof of unlimited archive access.

### Search public posts

```json
{
  "mode": "search",
  "query": "python",
  "sort": "latest",
  "maxPosts": 5,
  "pageSize": 10
}
```

Search uses Bluesky's public AppView search endpoint. Latest search now defaults to overlapping time windows using the supported `until` filter. Each following window overlaps the oldest source creation timestamp by one second; URI deduplication prevents a repeated result event. A local proof covered three windows each for two keywords. This is a bounded sample of a live search index, not a complete historical export. Native cursor requests returned403; they remain an explicit optional mode, without automatic retries or a silent fallback after an error. A source error retains useful rows and diagnostics. `top` uses native cursor mode and may therefore return403 on continuation. Time windows require `latest`. The Actor does not calculate a popularity score.

### Useful recipes

| Goal | Settings |
|---|---|
| Public author sample | mode=author, actor=bsky.app |
| Original standalone posts | includeReplies=false, includeReposts=false |
| Include repost entries | includeReposts=true; still deduplicates original URI |
| Brand/keyword mention sample | mode=search with your query |
| Only posts with an observed reaction threshold | minLikes=10; unknown counts fail a positive threshold |
| Resume a bounded collection | Copy CONTINUATION.resumeInput unchanged |

These are input recipes, not claims of five published Tasks. Saved Tasks, if created during QA, remain private until separately published and read back. The source index is live: cursor pagination cannot guarantee a stable historical snapshot or completeness.

### What each row means

| Fields | Meaning |
|---|---|
| uri, cid, postId, postUrl | Original at:// post identity, content ID when present and public link |
| text | Source post text; an image/video-only post may have empty text |
| author | Original author's DID, handle and display name; not the person who reposted |
| createdAt | Author-supplied creation timestamp, if present |
| indexedAt | AppView indexing timestamp, separately named |
| observedAt | Time our collector observed the row |
| likeCount, repostCount, replyCount, quoteCount | Exposed nonnegative counters; unavailable values remain null |
| languages, hashtags, links | Declared record languages and rich-text facet metadata |
| images, video, external | Available embed metadata and source links; no media files downloaded |
| parentUri, rootUri, quotedUri | Exposed references; no thread tree or quoted-post enrichment |
| isRepost, repostedBy | Feed repost context; original URI remains the deduplication key |

Counts and links are observations, not guarantees of current engagement, availability or ownership. The Actor does not infer sentiment, author location, contact information, follower lists or demographic attributes. It does not fetch the external links contained in posts. The same original post returned more than once becomes one Dataset row per run.

### Inputs and bounds

| Input | Default | Behavior |
|---|---:|---|
| mode | search | search or author |
| query | Required in search | Up to500characters; source search syntax |
| actor | Required in author | Handle or DID, not an arbitrary URL |
| sort | latest | latest or top; search route only |
| searchPagination | time_windows for latest search | Overlapping until windows; cursor for top/author. No complete archive guarantee |
| since / until | Unset | ISO timestamps with timezone; search only, not a full archive guarantee |
| lang | Unset | Language code forwarded to search; author mode rejects this filter |
| maxPosts | 20 | 1–10,000 delivered posts; synthetic bound, not a bulk benchmark |
| maxPages | 5 | 1–1,000 requests; errors stop rather than retry |
| pageSize | 25 | 1–100 source entries per request |
| maxRunSeconds | 120 | 30–1,800seconds; allowance for an in-flight request |
| requestDelayMs | 500 | 250–10,000ms between sequential pages |
| includeReplies | true | Filter reply posts; does not collect replies to a post |
| includeReposts | false | Filter author-feed repost entries |
| minLikes | 0 | Minimum known like count; unknown passes only0 |
| startCursor | Unset | Source cursor; use full continuation input |
| excludeUris | Empty | Up to20,000 known post URIs; affects delivery, not source page fetching |

Unsupported fields and invalid combinations fail before source requests. This protects against silently ignoring a date/language filter in author mode.

### Continuation, errors and coverage

After a result, time or page limit, open the **CONTINUATION** record and copy `resumeInput` into the next run. It contains the mode, query/author, source cursor, filters and already delivered URIs. If the run stops midway through a page, the next run intentionally replays that page and excludes previously delivered posts, so an unprocessed tail is not silently skipped. Keep the same query and ordering. Time-window continuation also binds the original date filters, language, page size and pagination mode; changing these fields is rejected before source requests. You can change result/page/time limits. A saturated timestamp boundary stops with `SEARCH_WINDOW_STALLED` rather than jumping over a tied group. If the source has more posts at that boundary than a page can expose, coverage remains incomplete. Source creation timestamps are author-supplied and can differ from source indexing/sort timestamps, another reason that time windows cannot establish complete coverage.

A source-exhausted response has `resumeInput: null`. This only means no next cursor was returned; it does not prove a complete public or historical archive. A429/403 response, invalid API shape, repeated cursor or stalled time-window boundary is a diagnostic rather than a successful empty collection. Errors after useful posts retain those rows and report partial coverage. Errors with no useful posts fail visibly. A legitimately empty search or filter result can succeed with zero rows.

`SUMMARY` distinguishes fetched entries, useful delivered posts, filtered entries, duplicates, invalid entries, pages and stop reason. `ERRORS` contains sanitized failure codes. No API credentials are accepted or required by this MVP. After a crash, reconcile the actual Dataset before continuation because a recent successful write may precede the latest saved checkpoint. Do not interpret missing posts as deletions or an incomplete query as a complete monitoring baseline.

### Cost and integrations

The owner-approved initial result tariff is **$0.50 per1,000 useful posts on the Free tier and $0.40 per1,000 on paid plans**, with **no Actor startup fee**. Apify compute, storage and transfer are paid separately by the user. Failed source requests can consume platform resources even when no post result is charged. A maximum Actor charge caps event fees, not all separate platform usage. Use page and time limits too. The code distinguishes an accepted free Dataset write from a rejected positive-price PPE event; live billing and budget checks must pass on the exact candidate before release.

Export the Dataset as JSON, CSV or Excel through Apify. JSON preserves nested author/embed fields; the overview table offers common columns. An authorized API client can start the Actor and read its `defaultDatasetId` using [Apify's Python client](https://docs.apify.com/api/client/python) or [JavaScript client](https://docs.apify.com/api/client/js). A generic [n8n HTTP/Apify workflow](https://docs.apify.com/platform/integrations/n8n) can use the same input recipes. A dedicated Bluesky npm node, PyPI package or custom MCP server is not verified or advertised.

### FAQ

**Does it include private posts or require a session cookie?** No. The MVP only requests public AppView endpoints and accepts no session secrets.

**Will it collect every matching post or all of an author's history?** No completeness guarantee is made. Source indexing, cursor behavior, filters and errors limit coverage. The supported numeric cap does not establish live access to that many posts.

**Are replies a full discussion tree?** No. `includeReplies` controls whether reply posts already in the search/feed are retained. Parent/root fields are references, not fetched thread data.

**Will a repost create duplicate charges for the same original post?** The original URI deduplicates output within a run and across a continuation exclusion list. Separate independent runs are separate observations. Duplicates and filtered entries do not become result events.

**Can I monitor deletions or engagement growth?** You can collect timestamped observations, but the Actor does not implement lifecycle classification, deletion detection or notifications. A missing post in a limited response cannot establish deletion.

# Changelog

This Actor's version history is a separate document: https://apify.com/apivault_labs/bluesky-public-posts-scraper/changelog.md

# Actor input Schema

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

Search matching posts or collect one author feed. Neither mode is a complete historical archive.

## `query` (type: `string`):

Required for search mode. Up to500characters; uses the public source search syntax.

## `actor` (type: `string`):

Author mode only, e.g. bsky.app. Do not paste a URL.

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

latest uses time-window collection by default. top retains native cursor pagination, which may be blocked by the source.

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

Optional BCP47 language code sent to search, e.g. en. Author mode does not support this field.

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

Optional ISO timestamp with timezone; source search lower bound, not a guarantee of complete historical coverage.

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

Optional ISO timestamp with timezone after since; search mode only.

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

Include reply posts. Does not fetch a thread or reply tree.

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

Include feed repost entries, deduplicated by original post URI. Repost identity is separate from original author.

## `startCursor` (type: `string`):

Opaque source cursor. Prefer copying the entire CONTINUATION.resumeInput, including exclusion URIs.

## `excludeUris` (type: `array`):

Up to20000 at:// post URIs to exclude before output. Replayed pages may still require source requests.

## `maxPosts` (type: `integer`):

Global delivery cap. A numeric bound, not a live benchmark of10000posts.

## `maxPages` (type: `integer`):

Stops after this many requests; no automatic retries.

## `pageSize` (type: `integer`):

Source page size. Filtering and deduplication may yield fewer results.

## `maxRunSeconds` (type: `integer`):

Time budget with allowance for an in-flight source request.

## `requestDelayMs` (type: `integer`):

Sequential source requests, no proxy rotation or retries.

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

Optional local filter. Unknown like count passes0, fails a positive minimum.

## `searchPagination` (type: `string`):

Latest search defaults to overlapping until windows with deduplication. Native cursors may return403. Top uses native cursors; time windows do not support top. Copy the full continuation input, which binds query/date/language/page size.

## Actor input object example

```json
{
  "mode": "search",
  "query": "python",
  "sort": "latest",
  "includeReplies": true,
  "includeReposts": false,
  "excludeUris": [],
  "maxPosts": 20,
  "maxPages": 5,
  "pageSize": 25,
  "maxRunSeconds": 120,
  "requestDelayMs": 500,
  "minLikes": 0
}
```

# Actor output Schema

## `posts` (type: `string`):

No description

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

No description

## `continuation` (type: `string`):

No description

## `errors` (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 = {
    "query": "python"
};

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

# Run the Actor and wait for it to finish
run = client.actor("apivault_labs/bluesky-public-posts-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 '{
  "query": "python"
}' |
apify call apivault_labs/bluesky-public-posts-scraper --silent --output-dataset

```

## MCP server setup

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