# Bluesky Posts & Profiles Scraper (No Login) (`unrealevents22/bluesky-public-data`) Actor

Scrape Bluesky posts by keyword or hashtag, user feeds, profiles, followers and following via Bluesky's official public API. No login, no proxies. Monitor mode returns only new posts.

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

## Pricing

from $1.00 / 1,000 results

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: official API, no login, pay only for new posts

Collect public **Bluesky** data from **Bluesky's own public API**: posts matching keywords or #hashtags,
any account's posts, profiles, and follower/following lists. No login, no cookies, no proxies, no browser.

Turn on **monitor mode** and schedule it. Each run returns **only posts this monitor hasn't delivered
before**, so you never pay twice for the same post. It also **tells you when you might have missed posts**
(Bluesky caps logged-out search at 100 posts per query), so you know when to schedule runs more often.

- ✅ **Pay only for new posts.** Monitor mode remembers what it already delivered (per query/account, per filter set). Re-runs with nothing new cost only the $0.00005 start fee.
- ✅ **Honest about Bluesky's limits.** Gap warnings, per-query `hitsTotal`, and a clear `OUTPUT` record of what was skipped and why. No silent truncation.
- ✅ **Official API only:** `public.api.bsky.app` / `api.bsky.app`, the same AppView the Bluesky app uses. Nothing to break when the website changes.
- ✅ **Respects user privacy choices.** Accounts that asked Bluesky not to show their content to logged-out viewers are never returned (and never charged).
- ✅ **One clean row per result.** No summary rows, no empty rows, no error rows in your dataset. You pay only for real posts, profiles and accounts.

### What can I use it for?

- **Brand and keyword monitoring:** hourly runs on your brand, product or competitors, with only new mentions delivered to Slack, Sheets or a webhook.
- **Hashtag and event tracking:** `#hashtag` with a date window.
- **Creator and competitor tracking:** new posts from a list of accounts, with engagement counts.
- **Audience research and lead lists:** profiles, followers and following, with bios and counts.
- **Research and AI pipelines:** clean JSON with language, hashtags, mentions, links, media and labels.

### Modes

| `mode` | Input | One result = |
|---|---|---|
| `searchPosts` | `queries`: keywords, phrases or a single `#hashtag` per line | one matching post |
| `userPosts` | `accounts`: handles, DIDs or profile URLs | one post from that account (reposts optional) |
| `profiles` | `accounts` | one profile |
| `followers` | `accounts` | one follower of that account |
| `following` | `accounts` | one account it follows |

### Input

| Field | Type | Description |
|---|---|---|
| `mode` | string | `searchPosts` (default), `userPosts`, `profiles`, `followers`, `following` |
| `queries` | array | searchPosts: one search per line. `#tag` = hashtag search. Bluesky search syntax works (e.g. `"exact phrase"`) |
| `accounts` | array | `bsky.app`, `@bsky.app`, `did:plc:…`, `https://bsky.app/profile/…`; a bare `jay` means `jay.bsky.social` |
| `onlyNewSinceLastRun` | boolean | **Monitor mode** (searchPosts, userPosts): emit only posts not delivered before. The first run is the baseline |
| `stateKey` | string | Monitor name. One per schedule/watchlist (default `default`) |
| `since` / `until` | string | Post time window: `YYYY-MM-DD`, ISO timestamp, or relative `24h` / `7d` / `2w`. `since` is inclusive, `until` exclusive |
| `sort` | string | searchPosts: `latest` (default) or `top` |
| `lang` | string | searchPosts: language code, e.g. `en` |
| `author` / `mentions` | string | searchPosts: only posts by / mentioning this handle or DID |
| `domain` | string | searchPosts: only posts linking to this domain |
| `feedFilter` | string | userPosts: `posts_and_author_threads` (default, like the app's Posts tab), `posts_with_replies`, `posts_no_replies`, `posts_with_media` |
| `includeReposts` | boolean | userPosts: include reposts (`isRepost: true`), default `false` |
| `skipAdultContent` | boolean | Drop posts labeled porn / sexual / nudity / graphic-media (default `true`; skipped posts aren't charged) |
| `maxItemsPerSource` | integer | Cap per query or account (default 100) |
| `maxItems` | integer | Hard cap per run. **Use it to control cost** |

Examples:

```json
{ "mode": "searchPosts", "queries": ["apify", "#opensource"], "since": "7d", "lang": "en", "maxItemsPerSource": 25 }
```

```json
{ "mode": "searchPosts", "queries": ["\"my brand\"", "#mybrand"], "onlyNewSinceLastRun": true, "stateKey": "brand-hourly" }
```

```json
{ "mode": "userPosts", "accounts": ["bsky.app", "https://bsky.app/profile/apify.com"], "onlyNewSinceLastRun": true, "includeReposts": true }
```

```json
{ "mode": "followers", "accounts": ["apify.com"], "maxItemsPerSource": 1000 }
```

### Output

One dataset item per result. Posts have the same shape in `searchPosts` and `userPosts`:

```json
{
  "type": "post",
  "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3mwolmfws5k2r",
  "cid": "bafyreicquqnkjvugubmjyfiorc3nbrxuu7y4faicpulqmxb4ua73tpb564",
  "postId": "3mwolmfws5k2r",
  "url": "https://bsky.app/profile/bsky.app/post/3mwolmfws5k2r",
  "text": "Happy opening day of hockey season, NHL fans! \n\nKeep up with all 1,344 regular season games with this starter pack of hockey writers.",
  "createdAt": "2026-09-29T19:43:14.855Z",
  "indexedAt": "2026-09-29T19:43:15.262Z",
  "langs": ["en"],
  "authorDid": "did:plc:z72i7hdynmk6r22z27h6tvur",
  "authorHandle": "bsky.app",
  "authorDisplayName": "Bluesky",
  "authorAvatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihwihm6kpd6zuwhhlro75p5qks5qtrcu55jp3gddbfjsieiv7wuka",
  "authorVerified": false,
  "replyCount": 72, "repostCount": 140, "likeCount": 983, "quoteCount": 17, "bookmarkCount": 56,
  "isReply": false, "replyParentUri": null, "replyRootUri": null,
  "isQuote": false, "quotedPost": null,
  "hashtags": [], "mentions": [], "links": [],
  "images": [], "video": null, "externalLink": null,
  "labels": [],
  "isRepost": false, "repostedByHandle": null, "repostedByDid": null, "repostedAt": null,
  "mode": "userPosts",
  "input": "bsky.app",
  "scrapedAt": "2026-10-08T02:18:34.662Z"
}
```

| Field | Notes |
|---|---|
| `mentions` | `[{did, handle}]`, with handles as written in the post |
| `quotedPost` | `{uri, url, authorHandle, text, available}` for quote posts |
| `images` / `video` / `externalLink` | `[{url, thumb, alt}]` / `{playlist, thumbnail, alt}` / `{uri, title, description}` |
| `labels` | Moderation/self labels on the post |
| `isRepost`, `repostedBy*`, `repostedAt` | userPosts with `includeReposts` |
| `changeType`, `firstSeenAt` | Monitor mode only (`"new"`, and when this monitor first delivered it) |
| `input` | The query or account this row came from |

Profiles (`type: "profile"`): `did, handle, displayName, description, url, avatar, banner, followersCount, followsCount, postsCount, listsCount, feedsCount, starterPacksCount, isLabeler, isBot, verified, pinnedPostUri, labels, createdAt, indexedAt`.
Followers / following (`type: "follower"` / `"following"`): `ofHandle, ofDid, did, handle, displayName, description, url, avatar, isBot, verified, labels, createdAt, indexedAt`.

Each run also writes an **`OUTPUT`** record to the default key-value store (free, not a dataset row): per query/account
results, errors (e.g. "Account not found"), skip counts (opted-out, adult, outside date range, already delivered),
Bluesky's `hitsTotal` for searches, monitor `baseline` / `possibleGap` flags, and whether a run limit was hit.
Export the dataset as JSON, CSV, Excel or XML, or read it through the Apify API.

### Monitor mode (pay only for new posts)

Set `onlyNewSinceLastRun: true` and schedule the Actor (e.g. every hour).

- **First run** = baseline: current matching posts are delivered with `changeType: "new"`.
- **Later runs** deliver only posts this monitor hasn't delivered before. Engagement-count changes don't re-deliver a post.
- Each query/account **plus its filters** has its own history, so changing `lang` or dates starts a fresh baseline for that combination. Use a different `stateKey` per schedule.
- **Gap warning:** logged-out Bluesky search returns at most the newest 100 posts per query. If all 100 are new, older new posts may have been missed. `OUTPUT` then shows `possibleGap: true`, and you should schedule that monitor more often. For account feeds, the Actor pages back to where the last run stopped (up to `maxItemsPerSource` new posts).
- If a run is cut short by `maxItems`, your spending limit, or an API error, the undelivered posts are offered again next run.
- State lives in the named key-value store `bluesky-public-data-state`. It holds only post IDs and timestamps, never post text.

### Pricing (pay per result)

**$0.001 per result** ($1 per 1,000 dataset items), plus a **$0.00005 start fee** per run.

- One result = one post, profile, follower or followed account in the dataset. Nothing else is ever written to the dataset.
- Errors, accounts not found, opted-out accounts, adult-filtered posts and already-delivered posts produce no rows, so they cost nothing.
- Monitor runs with nothing new cost only the $0.00005 start fee.

Examples: 1,000 posts from a keyword search across several queries ≈ **$1**. A brand monitor that runs hourly and finds about 20 new posts per hour delivers about 480 posts a day ≈ **$0.48/day** (plus 24 start fees ≈ $0.0012). Without de-duplication, re-pulling the newest 100 posts every hour would be 2,400 rows a day.
Set `maxItems` and `maxItemsPerSource` to cap any run. The Actor also stops cleanly at your run's maximum charge limit.

### Limits (Bluesky's, not ours)

- **Search: max 100 posts per query per run.** Bluesky doesn't allow logged-out clients to page further in post search. Use narrower queries, date windows (`since`/`until`), or monitor mode on a schedule.
- **Search can be switched off by Bluesky.** Bluesky sometimes disables anonymous post search under heavy load. The query then shows an error in `OUTPUT`, and no rows (and no charges) are produced. Account feeds, profiles and follower lists are not affected.
- Bluesky's search matching, language detection and `top` ranking are Bluesky's own. Results can differ slightly from the app.
- Follower/following lists contain only accounts that are currently viewable. Deleted, deactivated and opted-out accounts are skipped, so counts can be lower than `followersCount`.
- No private data: no DMs, no mutes or blocks, no likes lists, nothing that needs a login.

### Compliance

- Uses only Bluesky's **official public AppView API** without authentication, as documented in [API Hosts and Auth](https://docs.bsky.app/docs/advanced-guides/api-directory) and [Rate Limits](https://docs.bsky.app/docs/advanced-guides/rate-limits). No scraping, no login, no proxies.
- Read-only: it never posts, likes, follows or messages anyone, in line with the [Bluesky Developer Guidelines](https://docs.bsky.app/docs/support/developer-guidelines).
- Honors the `!no-unauthenticated` self-label ("discourage apps from showing my account to logged-out users"), as Bluesky [recommends](https://docs.bsky.app/docs/advanced-guides/moderation). Those accounts' posts and profiles are never returned.
- Polite by design: at most 3 concurrent requests and 4 requests per second, exponential backoff on 429/5xx, and an identifying User-Agent.
- You are responsible for how you use the data. Bluesky content belongs to its authors. Honor deletion requests, and follow applicable privacy laws (e.g. GDPR) and the terms of wherever you republish it.

### Contact

Questions, bug reports, or a takedown/deletion request (including from Bluesky): **unrealevents22@gmail.com** You can also open an issue on the Actor's Issues tab.

### FAQ

**Why did a search return fewer than 100 posts?** Opted-out accounts and (by default) adult-labeled posts are removed. `OUTPUT` shows the counts. Bluesky may also have fewer matches (`hitsTotal`).

**Can I get more than 100 posts for one keyword?** Run several narrower date windows (`since`/`until`) or language filters, or use monitor mode on a schedule to collect continuously.

**Do I need a Bluesky account, cookies or proxies?** No.

**A search query failed with HTTP 403.** Bluesky has temporarily disabled anonymous search. Try again later. Nothing was charged for that query.

### Run locally (no Apify account needed)

```bash
npm install
mkdir -p storage/key_value_stores/default
echo '{"mode":"searchPosts","queries":["#bluesky"],"maxItemsPerSource":10}' > storage/key_value_stores/default/INPUT.json
node src/main.js     # results in storage/datasets/default, OUTPUT in storage/key_value_stores/default
npm test             # offline tests on recorded API responses
```

# Actor input Schema

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

searchPosts: posts matching keywords or #hashtags. userPosts: posts from accounts. profiles: one profile row per account. followers / following: one row per follower / followed account.

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

One search per line. A single #hashtag (e.g. #bluesky) searches that hashtag; anything else is a keyword/phrase search (Bluesky search syntax, e.g. "exact phrase"). Bluesky returns at most 100 posts per query to logged-out clients.

## `accounts` (type: `array`):

Handles (bsky.app or @bsky.app), DIDs (did:plc:…) or profile URLs (https://bsky.app/profile/…). A bare name like 'jay' means jay.bsky.social.

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

searchPosts and userPosts only. Emits only posts this monitor hasn't delivered before (changeType = 'new'). The first run is the baseline. Schedule the Actor (e.g. hourly) and you never pay twice for the same post. State lives in the named key-value store 'bluesky-public-data-state'.

## `stateKey` (type: `string`):

Use a different name per schedule so their histories don't mix. Letters, digits, '-' and '\_'.

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

Only posts at or after this time. YYYY-MM-DD, an ISO timestamp, or a relative window like 24h, 7d, 2w.

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

Only posts before this time. Same formats as Since.

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

searchPosts only.

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

searchPosts only. Two-letter language code, e.g. en, de, ja.

## `author` (type: `string`):

searchPosts only. Handle or DID.

## `mentions` (type: `string`):

searchPosts only. Handle or DID.

## `domain` (type: `string`):

searchPosts only, e.g. nytimes.com.

## `feedFilter` (type: `string`):

userPosts only.

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

userPosts only. Reposts are emitted with isRepost = true and repostedBy fields.

## `skipAdultContent` (type: `boolean`):

Drop posts labeled porn, sexual, nudity or graphic-media (by Bluesky moderation or the author). Skipped posts are not charged.

## `maxItemsPerSource` (type: `integer`):

Cap per search query or account. Search returns at most 100 per query regardless.

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

Hard cap on results in this run (each result = one dataset row = one billed result). Leave empty for no cap beyond your spending limit.

## Actor input object example

```json
{
  "mode": "searchPosts",
  "queries": [
    "apify",
    "#bluesky"
  ],
  "accounts": [
    "bsky.app"
  ],
  "onlyNewSinceLastRun": false,
  "stateKey": "default",
  "sort": "latest",
  "feedFilter": "posts_and_author_threads",
  "includeReposts": false,
  "skipAdultContent": true,
  "maxItemsPerSource": 100
}
```

# Actor output Schema

## `results` (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 = {
    "queries": [
        "apify",
        "#bluesky"
    ],
    "accounts": [
        "bsky.app"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("unrealevents22/bluesky-public-data").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 = {
    "queries": [
        "apify",
        "#bluesky",
    ],
    "accounts": ["bsky.app"],
}

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

```

## MCP server setup

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

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/CxojSIuuizhiZwUzE/builds/K5Ld56KTnpZbmEkj6/openapi.json
