# Bluesky Posts Search Scraper - AT Protocol API, No Key (`lost.fany/bluesky-posts-search-scraper`) Actor

Search Bluesky posts and author feeds through the public AT Protocol endpoints: text, handle, timestamps, likes, reposts, replies, links and images. No login or key. dryRun prices a run before you pay; charged only for posts delivered.

- **URL**: https://apify.com/lost.fany/bluesky-posts-search-scraper.md
- **Developed by:** [Natalia Ivanova](https://apify.com/lost.fany) (community)
- **Categories:** Social media, News, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 post rows

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 Posts & Search Scraper - AT Protocol, No API Key

Search every public post on **Bluesky** by keyword, or pull the full public feed of any account, and get clean rows: post text, permalink, author handle, timestamps, likes, reposts, replies, quotes, declared languages, outbound links, link cards, images with alt text, hashtags and mentions. It talks to the official public AT Protocol endpoints — `app.bsky.feed.searchPosts`, `app.bsky.feed.getAuthorFeed` and `app.bsky.actor.getProfile` — so there is no login, no app password, no API key and no browser to babysit.

### What you can do with it

- **Brand and competitor monitoring** — every mention of your product, your domain or a rival's name since yesterday, straight into Slack or a Google Sheet through Apify integrations.
- **Hashtag and campaign tracking** — all posts carrying `#yourconference` in a date window, with engagement counts so you can rank them.
- **Link and traffic research** — which Bluesky posts link to `example.com`, who posted them and how far they travelled.
- **Account archives** — the complete public post history of an account, replies and reposts included or excluded as you like.
- **Datasets for analysis** — a language-filtered slice of public conversation about a topic, ready for sentiment work or topic modelling.
- **AI agents** — "what is Bluesky saying about X this week?" through the Apify MCP server.

### Bluesky search: find posts by keyword

Set `mode` to `search` and give `queries` one or more terms. Every term is searched separately, results are merged and deduplicated by post URI, and `matchedQuery` on each row tells you which term produced it. Bluesky's own search syntax works inside a query too — `"exact phrase"`, `from:handle`, `domain:example.com` — and `sort` picks between newest first and Bluesky's relevance ranking.

```json
{ "mode": "search", "queries": ["apify", "web scraping"], "sinceDate": "7 days", "language": "en", "maxItems": 1000 }
```

### Bluesky posts from one account: the author feed

Set `mode` to `author-feed` and list `authors` as handles, profile URLs or DIDs. Each feed is read newest first until the `sinceDate` boundary, so an incremental daily run is cheap. The account's public profile (display name, bio, follower counts, post count) lands in the run summary.

```json
{ "mode": "author-feed", "authors": ["bsky.app"], "includeReplies": true, "includeReposts": true, "maxItems": 1000 }
```

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | select | `search` | `search` (keyword search, needs `queries`) or `author-feed` (posts of given accounts, needs `authors`). |
| `queries` | array | `[]` | **Required in `search` mode.** Words, phrases, hashtags or domains; merged and deduplicated. |
| `authors` | array | `[]` | **Required in `author-feed` mode.** Handles (`bsky.app`), profile URLs or DIDs. |
| `sort` | select | `latest` | `latest` or `top`. Search mode only; author feeds are always newest first. |
| `sinceDate` / `untilDate` | string | — | `2026-10-01`, `today`, `yesterday`, `24 hours`, `7 days`, `1 year` or a full ISO stamp. |
| `language` | string | — | Two-letter code the post declares: `en`, `pt`, `ja`. `pt` also matches `pt-BR`. |
| `searchAuthor` | string | — | Search mode: keep only results from this handle or DID. |
| `includeReplies` | boolean | `true` | Keep posts that reply to another post. |
| `includeReposts` | boolean | `false` | Author-feed mode: also return reposts (`isRepost`, `repostedBy`). |
| `maxItemsPerQuery` | number | `1000` | Cap per query or per author. |
| `maxItems` | number | `1000` | Cap for the whole run. |
| `proxyConfiguration` | object | off | Optional. Needed only if the public hosts refuse your run with HTTP 403. |

More examples:

```json
{ "mode": "search", "queries": ["#buildinpublic"], "sort": "top", "sinceDate": "30 days", "includeReplies": false }
```

```json
{ "mode": "search", "queries": ["domain:example.com"], "sinceDate": "90 days", "maxItemsPerQuery": 500 }
```

```json
{ "mode": "search", "queries": ["postgres"], "searchAuthor": "bsky.app", "language": "en" }
```

### Output

One row per post:

```json
{
  "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3mx5e63uvns2d",
  "cid": "bafyreidlxv7m376e3knixcu5ylvglltzkotygwtvh5ifsg4zcfow2z6rp4",
  "url": "https://bsky.app/profile/bsky.app/post/3mx5e63uvns2d",
  "text": "Congratulations to Nobel Prize winner @deisseroth.bsky.social!",
  "createdAt": "2026-10-05T16:39:54.000Z",
  "indexedAt": "2026-10-05T16:39:55.112Z",
  "author": { "handle": "bsky.app", "displayName": "Bluesky", "did": "did:plc:z72i7hdynmk6r22z27h6tvur", "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkrei....jpeg" },
  "authorHandle": "bsky.app",
  "likeCount": 2841,
  "repostCount": 312,
  "replyCount": 47,
  "quoteCount": 9,
  "langs": ["en"],
  "isReply": false,
  "parentUri": null,
  "rootUri": null,
  "isRepost": false,
  "repostedBy": null,
  "hashtags": [],
  "mentions": [{ "handle": "deisseroth.bsky.social", "did": "did:plc:qmmohi6hskhsvqbw46eflibx" }],
  "links": [],
  "externalLinks": [],
  "images": [],
  "hasVideo": false,
  "quotedPostUri": "at://did:plc:ilmbc32kos5zwmkqh2nxamwc/app.bsky.feed.post/3mx4smdh4el2k",
  "quotedPostUrl": "https://bsky.app/profile/nobelprize.org/post/3mx4smdh4el2k",
  "quotedPostAuthor": "nobelprize.org",
  "quotedPostText": "The 2026 Nobel Prize announcements start today.",
  "matchedQuery": "nobel",
  "scrapedAt": "2026-10-12T11:37:45Z"
}
```

`links` holds URLs written in the post text, `externalLinks` holds the link card the post attached (`url`, `title`, `description`, `thumbnailUrl`), and `images` holds `url`, `thumbnailUrl`, `alt`, `width`, `height` per image. `mentions` carries both the handle, read from the post's richtext facet, and the stable DID. The `OUTPUT` record in the key-value store lists posts per query or author, duplicates skipped, posts filtered out, API requests made, author profiles and any errors.

### A Bluesky scraper without API key: how it works

Bluesky's AppView serves its read endpoints publicly: no session, no app password, no developer registration, no rate-limit key. This Actor calls them directly over HTTPS and maps the lexicon's `postView` records into flat rows. That means your results are exactly what the official client sees, you are never asked for your Bluesky credentials, and nothing breaks when the website's HTML changes.

### Why the AT Protocol endpoints and not the website

Scraping `bsky.app` in a browser is slower, heavier and fragile. The AT Protocol lexicons are versioned, documented and stable, and they hand over the structured fields — byte-indexed richtext facets, embed views, engagement counts — that an HTML page only hints at. Hashtags and mentions here come from the facets the author actually published, not from a regular expression over the text, so `#tag` inside a URL never becomes a false hashtag.

### Pricing

Pay per event: a small fee per Actor start plus a fee per post returned. Current prices are in the **Pricing** tab.

### Good to know

- Search covers Bluesky's public index. Very old posts and posts from accounts that opted out of public indexing may not appear; an author feed is the exhaustive route for one account.
- Engagement counts are a snapshot taken at scrape time. Run again later for fresh numbers.
- `langs` is what the author's client declared, not detected language. Posts that declare nothing are skipped when you set `language` — leave it empty to keep them.
- Search never returns reposts, so `includeReposts` only matters for author feeds.
- Bluesky's public AppView fronts its endpoints with a CDN that answers HTTP 403 for some endpoint and network combinations — search is the one that gets blocked in practice. The Actor falls back to a second public AT Protocol host automatically and records in the run summary which host served the data; if both refuse, switch on `proxyConfiguration` (residential group) and run again.
- A handle that does not resolve is reported as one error in the summary and does not stop the other authors.

### Use with AI agents (MCP)

Add the Actor to the [Apify MCP server](https://apify.com/apify/actors-mcp-server) and ask: "What is Bluesky saying about SQLite this week?" → `{ "mode": "search", "queries": ["sqlite"], "sinceDate": "7 days" }`.

### Public data and personal data

This Actor returns **only what the authors published publicly**: post text, their chosen handle and display name, their public avatar URL, public timestamps and public engagement counts. It never logs in, never reads direct messages, follower lists or blocked lists, never touches anything behind a login, and collects no e-mail addresses, phone numbers, locations or any other field aimed at identifying a person beyond the public handle they chose themselves.

Public does not mean unregulated. Posts are personal data under the GDPR and comparable laws when they are about identifiable people, and the content belongs to its authors. **You, the operator of the run, are the controller of whatever you collect** and are responsible for having a lawful basis, for honouring deletion requests, for retention limits and for complying with Bluesky's terms. Do not use the output to profile individuals, to build identity dossiers, to train on personal data without a basis, or for anything else the authors would not expect.

### Related Actors

- [Hacker News Search API](https://apify.com/lost.fany/hacker-news-search-scraper)
- [SEC EDGAR API](https://apify.com/lost.fany/sec-edgar-filings)
- [Website Tech Stack Detector](https://apify.com/lost.fany/website-tech-stack-detector)

### Disclaimer

Not affiliated with Bluesky Social PBC or the AT Protocol project. Data comes from the public AT Protocol AppView endpoints; all content belongs to its authors.

### Cost control

Pay per event only works if you can see the bill before you pay it:

- Set `dryRun` to count every result and print what a full run would cost - nothing is charged, and only a few sample rows are stored.
- You are charged only for rows actually delivered to the dataset; a run that finds nothing costs nothing beyond the start event.
- `maxItems` is respected exactly, and the run stops before the platform's Max total charge for the run.
- Every run writes a `COST` record to the key-value store: chargeable events, charged events, the amount in USD and whether a limit was hit.

# Actor input Schema

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

search: full-text search across all public Bluesky posts (app.bsky.feed.searchPosts) - needs 'queries'. author-feed: every public post of given accounts (app.bsky.feed.getAuthorFeed) - needs 'authors'. Leave empty and the Actor picks for you: 'author-feed' when you pass only authors, 'search' otherwise.

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

Required in 'search' mode. Words, phrases, hashtags (#apify) or domains. Each is searched separately; results are merged and deduplicated by post URI, and 'matchedQuery' tells you which one matched. Bluesky's search syntax works here too: "exact phrase", from:handle, domain:example.com.

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

Required in 'author-feed' mode. Handles (bsky.app), profile URLs (https://bsky.app/profile/bsky.app) or DIDs (did:plc:...). Each account's public posts are pulled newest first.

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

latest: newest posts first. top: Bluesky's own relevance/engagement ranking. Ignored in 'author-feed' mode, which is always newest first.

## `sinceDate` (type: `string`):

Only posts created after this: 2026-10-01, today, yesterday, '24 hours', '7 days', '1 year', or a full ISO stamp. Empty = no lower bound.

## `untilDate` (type: `string`):

Only posts created before this moment, same formats as 'Since'. Empty = now.

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

Two-letter language code the post declares, e.g. en, pt, ja, de. 'pt' also matches 'pt-BR'. Posts that declare no language are skipped when this is set. Empty = every language.

## `searchAuthor` (type: `string`):

Search mode only: keep results from this handle or DID. Handy for "what did this account say about X". Leave empty to search all of Bluesky.

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

Keep posts that reply to another post ('isReply' and 'parentUri' are filled in). Turn off for original posts only.

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

Author-feed mode only: also return posts the account reposted ('isRepost' and 'repostedBy' are filled in). Search never returns reposts.

## `maxItemsPerQuery` (type: `integer`):

Stop after this many posts for each entry in 'queries' / 'authors'. Keeps one busy keyword from eating the whole run.

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

Stop after this many posts in total, across all queries or authors.

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

Optional. The public AT Protocol hosts refuse some networks and datacenter IP ranges with HTTP 403. If a run fails with "No public AT Protocol host accepted the request", switch on Apify Proxy (the residential group works best) and run again.

## `dryRun` (type: `boolean`):

Count every result and show what a full run would cost, without charging for it. Only a few sample rows are stored. Use it to check the price before you pay.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "apify",
    "web scraping"
  ],
  "authors": [
    "bsky.app"
  ],
  "sort": "latest",
  "sinceDate": "7 days",
  "includeReplies": true,
  "includeReposts": false,
  "maxItemsPerQuery": 1000,
  "maxItems": 1000,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "dryRun": false
}
```

# Actor output Schema

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

One row per public Bluesky post: text, permalink, author handle, timestamps, engagement counts, languages, links, images, hashtags, mentions and which query matched.

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

Posts per query or author, duplicates and filtered-out counts, the AT Protocol host used, author profiles and any errors.

# 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": "search",
    "queries": [
        "apify",
        "web scraping"
    ],
    "authors": [
        "bsky.app"
    ],
    "sinceDate": "7 days"
};

// Run the Actor and wait for it to finish
const run = await client.actor("lost.fany/bluesky-posts-search-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": "search",
    "queries": [
        "apify",
        "web scraping",
    ],
    "authors": ["bsky.app"],
    "sinceDate": "7 days",
}

# Run the Actor and wait for it to finish
run = client.actor("lost.fany/bluesky-posts-search-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": "search",
  "queries": [
    "apify",
    "web scraping"
  ],
  "authors": [
    "bsky.app"
  ],
  "sinceDate": "7 days"
}' |
apify call lost.fany/bluesky-posts-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lost.fany/bluesky-posts-search-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/FYqKySNdPr38AjCSO/builds/hObk6otvUBYwU9Ero/openapi.json
