# Bluesky Public Posts Scraper - Feeds & Threads (`maydit/bluesky-public-posts-scraper`) Actor

Extract public Bluesky author posts and supplied threads with links, reply relationships and engagement metrics. Optional saved-observation change tracking. No login or API key.

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

## Pricing

from $1.20 / 1,000 post observations

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

![Public posts, with thread context — illustrative workflow](https://api.apify.com/v2/key-value-stores/YGxD9JRpduom8Q5Zd/records/bluesky-public-posts-scraper-09628735dee8.png?signature=hEhQmGfdh4XS5aCYD3lj)

### Public posts, with thread context

Collect public Bluesky posts for editorial research, account watchlists or structured archives. Each row represents a root post; optional parent and reply context stays nested within that row.

[Open this Actor in Console](https://console.apify.com/actors/KQRzP39gn24WO2IfI/input) · [First run](#try-this-actor) · [Results](#result-preview) · [Cost](#usage-and-pricing) · [Full guide](#full-guide)

### Try this Actor

1. **Choose a small scope.** Use **Bluesky Official Posts and Reply Context** below: it requests three posts from `bsky.app` with up to ten context posts per result. For a post-only run, clear the prefilled author list.
2. **Run and review.** Open the dataset for post observations. Check the run's key-value-store `SUMMARY`, `observationComplete` and thread coverage before treating a result as complete.
3. **Use the results.** Export JSON to retain nested author, reply, media and metric fields. The table view is useful for scanning post text and source links.

**Read this before using the data:** This is a bounded public view, not a complete conversation or firehose. Hidden replies may be absent. Comparison is optional metadata: unchanged saved observations are still emitted and billed.

**Bluesky Official Posts and Reply Context** — copy this input into the JSON editor in Console.

```json
{
  "authors": [
    "bsky.app"
  ],
  "maxPosts": 3,
  "includeThread": true,
  "maxContextPosts": 10
}
```

### Result preview

The dataset view highlights these columns:

- **Post link** · `url`
- **Author handle** · `author.handle`
- **Post text** · `text`
- **Source creation time** · `createdAt`
- **Observed likes** · `metrics.likes`
- **Observed replies** · `metrics.replies`

Use the original source links and the field definitions below when interpreting the results. CSV works well for flat columns; JSON preserves nested values.

### Usage and pricing

Base result rate on the FREE tier: **$2 per 1,000 post observations**. The [Pricing tab](https://apify.com/maydit/bluesky-public-posts-scraper/pricing) shows your plan's result rate and any Actor Start charge. Set a small result limit for the first run; the requested limit is not a guarantee of that many results.

***

### Customer workflow: An editorial watchlist with engagement checkpoints

Watch explicit public publisher accounts, retain stable post identities and source links, and separate newly observed content from ordinary engagement changes. This is an editorial queue, not network-wide listening.

**Setup:** Connect your own Apify and Google Sheets current-post register and optional observation history through [Make](https://docs.apify.com/integrations/make) or [n8n](https://docs.apify.com/integrations/n8n). Keep credentials in your connector's credential manager.

```json
{
  "authors": [
    {
      "value": "bsky.app",
      "externalId": "publisher-demo"
    }
  ],
  "postUrls": [],
  "maxPosts": 10,
  "maxPostsPerAuthor": 10,
  "maxPagesPerAuthor": 3,
  "includeReplies": false,
  "includeReposts": false,
  "includeThread": false,
  "comparePrevious": true,
  "baselineKey": "customer-editorial-v1",
  "maxRunSeconds": 240
}
```

Start with a **300-second Run timeout** and **$0.50 maximum run charge**, alongside the input's 240-second budget. Check current pricing and the selected scope before increasing either limit.

1. Create a Posts sheet keyed by `uri`, then replace the demo account with your explicit public publisher watchlist. Keep your customer reference in `externalId`. Seed the sheet from the first run before creating editorial alerts.
2. Require a healthy `SUMMARY`, no failed sources, and `observationComplete: true` before treating an observation as complete. Inspect source stop reasons: this input watches a bounded recent feed, not every historical post.
3. Upsert by `uri` using Sheets "Append or Update Row" in n8n or a lookup/update branch in Make. Flatten metrics into columns; preserve `requestedBy` as JSON because several requested sources can point to one post.
4. Reuse `baselineKey` sequentially. Route `first_observation` or `contentChanged: true` to editorial review, and `metricsChanged` to a separate engagement checkpoint. Null deltas stay unknown; unchanged posts are still saved and billed.

| Destination information | Actor fields |
| --- | --- |
| Stable key and link | `uri`, `url` |
| Author and attribution | `author.handle`, `requestedBy` |
| Content | `text`, `contentHash` |
| Engagement | `metrics.likes`, `metrics.reposts`, `metrics.replies`, `metrics.quotes` |
| Comparison | `comparisonStatus`, `contentChanged`, `metricDeltas`, `observedAt` |

**Keep the meaning:** There is no `onlyChanges` input. A recent window can miss posts between runs, and absence does not prove deletion. Thread context is off here; when enabled, it remains a bounded public view, never a guaranteed complete conversation.

**Empty or interrupted runs:** No public posts may match the selected feed scope. A healthy unchanged repeat normally still contains rows. An unavailable supplied post is an operational issue, not an automatic deletion signal.

**Reliable delivery:** Read the completed run's own SUMMARY and paginated dataset. Retain its run ID; if destination delivery fails, replay that dataset and deduplicate by run ID plus row key. Keep destination writes sequential, and write source text literally.

**Validation:** Inputs and mappings were checked locally against this Actor. The exact customer connection has not been run end to end. Choose recurrence after a manual bootstrap; this guide creates no schedule. See the [run API](https://docs.apify.com/api/v2/actors-runs-post), [pagination guide](https://docs.apify.com/api/v2/dataset-items-get), and the full Actor guide below for detailed setup and limits.

### Full guide

Extract recent public posts from supplied Bluesky accounts, or inspect known public post URLs. Each saved row is one root post with its text, source URL, stable URI/CID, links, hashtags, facet byte offsets, media references, author attribution, reply relationships and engagement counts.

The optional thread view puts bounded parent/reply context inside the same row. Optional change tracking compares a root post's content and metrics with its previously saved observation.

#### Quick start

Run with `{}` for three recent original posts from **bsky.app**, with up to 10 visible context posts each. No Bluesky login, app password, API key or proxy is required. Only the official public API is used. Source availability and public visibility can change.

#### Inputs and scope

- `authors`: up to 20 public handles, DIDs or profile URLs. Entries can be strings or `{"value":"bsky.app","externalId":"crm-123"}`.
- `postUrls`: up to 50 supplied public bsky.app post URLs or post AT-URIs; the same object form preserves an external ID.
- `maxPosts` defaults to 3, maximum 200. All sources share this limit; authors are processed before supplied posts. A post selected more than once is saved once with `requestedBy` attribution for sources reached before the global cap.
- `maxPostsPerAuthor` defaults to 25; `maxPagesPerAuthor` defaults to 3. The Actor stops at the first applicable bound and records why.
- Original non-reply posts are the default. `includeReplies` and `includeReposts` apply to author feeds. Supplied post URLs are always eligible. Repost provenance remains separate from the original author.
- `includeThread` defaults to true. Reply depth defaults to 1, parent height to 1 and retained context to 10 posts. Maximum reply depth 3, parent height 5, context 50. The context limit is shared by parents and replies; parents are visited first, then replies breadth-first.
- `maxRunSeconds` defaults to 240 and is capped at900. The Actor also stops 25 seconds before the platform deadline.

An empty authors list and empty post list use the default account. A list supplied only in postUrls does not add the default account. Invalid URLs and input types fail with a diagnostic.

#### Output and billing

**One paid result is one saved public root-post observation.** Nested thread context is included in that result. Error messages and inaccessible posts are never paid placeholder rows. Unchanged observations are still saved and billed; comparison is metadata, not an output filter.

Key fields: `url`, `uri`, `cid`, `postId`, `contentHash`, `author`, `text`, `createdAt`, `links`, `hashtags`, `facets`, `media`, `replyTo`, `threadRoot`, `metrics`, `requestedBy`, `thread`, `threadCoverage`, `observationComplete` and `observedAt`. Missing optional source fields are null. Facet positions are UTF-8 byte offsets, not JavaScript character indexes. Images/video are linked, not downloaded.

Source `createdAt` is retained as supplied and may differ from indexing time. Metrics are observations, can decrease, and are not historical lifetime guarantees. A quoted record is a reference; quoted-post bodies are not separately expanded.

#### Coverage and failures

This Actor reads a **bounded public view**. It does not provide keyword search, follower graphs, private content, a firehose, exhaustive history or a complete conversation. The API can omit moderated, deleted, blocked or otherwise hidden material. A reported reply count can exceed visible replies. `completeConversation` is always false; `contextTruncated` reports the Actor's own output cap separately.

If thread expansion fails but a valid feed post was obtained, that real post can still be saved with `observationComplete:false`. It does not update comparison history. Supplied unavailable posts and wholly failed sources produce diagnostics in `SUMMARY`. A run with no usable rows and upstream failures fails honestly. A valid empty feed or exhausted spending limit can finish with a clear empty/budget status. Intentional maxPosts/page/context caps are scope controls; they are distinct from source failures or timeouts.

The `SUMMARY` record contains saved totals, selected posts, per-source stop reasons, failed and skipped sources, unsaved counts, deadline/spend flags and baseline commits. Check it when a run is partial. The overview dataset table shows the main observation fields.

#### Change tracking

Set `comparePrevious:true` and choose a `baselineKey`. Snapshots are stored in the named key-value store **maydit-bluesky-post-observations-v1**, keyed by namespace and stable post URI. They retain only the URI, hash, metrics, version and observation time—not post text or context bodies.

First observations use `comparisonStatus:first_observation` and no numeric deltas. Later observations include `contentChanged`, `metricsChanged` and signed `metricDeltas`. The comparison covers root content and metrics only; thread context changes, handle/display-name changes and deletion are not compared. Missing posts or missing replies never produce deletion claims.

Only a confirmed saved, complete row updates its baseline. A final accepted row at the charge limit counts as saved; an unsaved row does not advance history. A storage-write failure is visible in SUMMARY. Use one writer at a time per namespace, because key-value storage provides no transaction across concurrent runs. Use a new namespace to start a fresh comparison; remove the relevant records in the named store to clear retained observations.

#### Three exact examples

##### Bluesky Official Posts and Reply Context

Three recent original posts from the public Bluesky account, with up to 10 visible thread context posts per result.

Use the [copyable first-run input](#try-this-actor) above.

##### AT Protocol Public Updates

Three recent original posts from the public AT Protocol account, with links and engagement metrics. No thread expansion.

```json
{
  "authors": [
    "atproto.com"
  ],
  "maxPosts": 3,
  "includeThread": false
}
```

##### Bluesky Public Thread with Reply Relationships

Inspect one supplied public Bluesky post and a bounded view of its reply relationships. No search or login is used.

```json
{
  "authors": [],
  "postUrls": [
    "https://bsky.app/profile/bsky.app/post/3mx5e63uvns2d"
  ],
  "maxPosts": 1,
  "includeThread": true,
  "threadDepth": 2,
  "parentHeight": 1,
  "maxContextPosts": 20
}
```

#### Source, content requests and support

Source: [Bluesky public API](https://bsky.network/docs/api-directory/), [author-feed definition](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getAuthorFeed.json), [thread definition](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getPostThread.json), [rate limits](https://bsky.network/docs/rate-limits/) and [developer guidelines](https://bsky.network/docs/developer-guidelines/).

For support or a content-removal request, open this Actor's **Issues** tab or email **bmay@mayd-it.com**. Include the public post URL and relevant run/dataset record reference; do not send credentials. We can address retained data under our control. You control your run datasets and downstream exports, and should provide a deletion/reporting method for any service you build using them. Public access does not transfer content ownership. Respect source policies and applicable rights; this Actor does not automate posting, liking, following or messaging.

### Help with your run

Open the **Issues** tab on this Actor with the run URL, the input field that caused trouble and what you expected to receive. Remove API keys and private information before posting.

[Explore more Mayd-It Actors](https://apify.com/maydit)

### Understand your run

Open **Run report** in the run’s Output tab for saved-record counts, reported source issues and limits, and suggested next steps. Use **CSV export** to take the saved results into a spreadsheet. The existing JSON results and detailed machine-readable summary remain available.

The report reflects this run’s recorded coverage. A successful run or a saved result does not establish that every possible source record was collected.

# Actor input Schema

## `authors` (type: `array`):

Up to 20 explicit public handles, DIDs or https://bsky.app/profile/<identity> URLs. Each entry can also be {"value":"bsky.app","externalId":"crm-123"}. Empty with no postUrls uses bsky.app. No follower discovery or keyword search.

## `postUrls` (type: `array`):

Up to 50 supplied https://bsky.app/profile/<identity>/post/<record> URLs or at://<identity>/app.bsky.feed.post/<record> references. Objects {value,externalId} are accepted. Authors are processed first, so leave authors empty for post-only runs.

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

Global unique root-post selection limit. Feed rows and supplied threads share this limit; author inputs are processed first. Context posts within a row do not add billable rows.

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

Include reply posts as root observations in author feeds. Supplied post URLs are always eligible even if they are replies.

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

Include third-party posts reposted by a supplied author. requestedBy marks repost provenance. A post selected more than once is saved once with source attribution.

## `maxPostsPerAuthor` (type: `integer`):

Maximum accepted observations per supplied author within maxPosts. This is a bounded recent feed, not all historical posts.

## `maxPagesPerAuthor` (type: `integer`):

Stop after this many API pages per author even when repost/reply filtering yields fewer posts. Pagination caps are recorded in SUMMARY.

## `includeThread` (type: `boolean`):

Fetch a bounded public view of parent and reply posts for each root. Hidden, deleted or blocked replies may be absent. An unavailable context fetch leaves a valid feed observation marked incomplete.

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

Requested public API reply depth. 0 excludes reply context. This does not guarantee that every reply at the chosen depth is visible.

## `parentHeight` (type: `integer`):

Requested maximum number of ancestor levels. Context output also shares maxContextPosts.

## `maxContextPosts` (type: `integer`):

Maximum visible parent/reply posts retained inside each root row. Intentional truncation is reported. These are context entries, not separate paid results.

## `comparePrevious` (type: `boolean`):

Compare root content hashes and engagement metrics with a named baseline. First observations are labeled first_observation and have no deltas. Unchanged posts are still output and billed. Only saved, complete observations update history. Missing posts/replies never mean deletion.

## `baselineKey` (type: `string`):

1–80 letters, digits, hyphens or underscores within maydit-bluesky-post-observations-v1. Use one writer per namespace. Snapshots retain URI, content hash, metrics and observation time, not post text.

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

Own deadline in seconds, additionally shortened to 25 seconds before the platform deadline. Partial runs identify failed/unfinished work in SUMMARY.

## Actor input object example

```json
{
  "authors": [
    "bsky.app"
  ],
  "postUrls": [],
  "maxPosts": 3,
  "includeReplies": false,
  "includeReposts": false,
  "maxPostsPerAuthor": 25,
  "maxPagesPerAuthor": 3,
  "includeThread": true,
  "threadDepth": 1,
  "parentHeight": 1,
  "maxContextPosts": 10,
  "comparePrevious": false,
  "baselineKey": "default",
  "maxRunSeconds": 240
}
```

# Actor output Schema

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

Successful public root-post observations. Inspect observationComplete and threadCoverage before interpreting context.

## `runReport` (type: `string`):

Readable summary of saved records, reported source coverage, limits and next steps. Available when this run saves its SUMMARY.

## `csvExport` (type: `string`):

Download saved dataset records as a CSV file for spreadsheets and other tools.

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

Saved counts, failed sources, selected scope, pagination limits and comparison commits.

# 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 = {
    "authors": [
        "bsky.app"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("maydit/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 = { "authors": ["bsky.app"] }

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

```

## MCP server setup

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