# Bluesky Posts & Profiles Search (`grit-77/bluesky-posts-profiles-search`) Actor

Bluesky author-feed and profile scraper for public handles. Export posts and profiles as JSON. Public keyword search can be denied; source errors are recorded.

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

## Pricing

Pay per event

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 Search

Bluesky posts scraper returns public posts and profiles from requested feeds or handles. Bluesky API keyword search also returns public posts when its endpoint allows access.

**Public keyword search returned HTTP 403 during the 2026-10-05 live check.** This actor records that denial as an uncharged error and continues other sources. It does not log in or switch hosts to evade denied access. For a working public-data run, supply author feeds or profile handles.

### What it returns

Each result has a `type`: `post`, `profile` or `error`. Posts are deduplicated by their AT URI, profiles by DID. The first source to return a duplicate supplies its provenance. Reposts retain the original author's identity and the reposter's identity separately.

| Field | Type | Example value |
|---|---|---|
| `type` | string | `"error"` |
| `source` | string | `"search"` |
| `input` | string | `"bluesky"` |
| `error_code` | string | `"public_access_denied"` |
| `http_status` | integer | `403` |
| `errors` | array | `[]` |
| `fetched_at` | string | `"2026-10-05T13:19:01.033361Z"` |
| `did` | string | `"did:plc:z72i7hdynmk6r22z27h6tvur"` |
| `handle` | string | `"bsky.app"` |
| `url` | string | `"https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur"` |
| `display_name` | string | `"Bluesky"` |
| `description` | string | `"official Bluesky account (check username👆)\n\nBugs, feature requests, feedback: support@bsky.app"` |
| `avatar` | string | `"https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihwihm6kpd6zuwhhlro75p5qks5qtrcu55jp3gddbfjsieiv7wuka"` |
| `banner` | string | `"https://cdn.bsky.app/img/banner/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreichzyovokfzmymz36p5jibbjrhsur6n7hjnzxrpbt5jaydp2szvna"` |
| `followers_count` | integer | `35154701` |
| `follows_count` | integer | `15` |
| `posts_count` | integer | `865` |
| `created_at` | string | `"2023-04-12T04:53:57.057Z"` |
| `indexed_at` | string | `"2025-10-27T21:05:26.152Z"` |
| `pinned_post_uri` | string | `"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l"` |
| `labels` | array | `[]` |
| `verification` | object | `{"verifications": [], "verifiedStatus": "none", "trustedVerifierStatus": "valid"}` |
| `uri` | string | `"at://did:plc:ifn645rwvsuolxg7o3w7ouo4/app.bsky.feed.post/3mwqgsd4cgs2a"` |
| `cid` | string | `"bafyreiesf6zo5wqojuoxdekbqap7pqev6t6xa2q5kzayntr6bgrt2ixi7m"` |
| `text` | string | `"If you're in line to vote for @bsky38.com, please stay in line! Polls close in just under 5 hours."` |
| `author_did` | string | `"did:plc:ifn645rwvsuolxg7o3w7ouo4"` |
| `author_handle` | string | `"aster.id"` |
| `author_display_name` | string | `"Aster ID"` |
| `author_avatar` | string | `"https://cdn.bsky.app/img/avatar/plain/did:plc:ifn645rwvsuolxg7o3w7ouo4/bafkreicid7hoyx5d2if24eryyhb3lg4ioy2mdrmiriga3k2cay6k5io4ta"` |
| `like_count` | integer | `740` |
| `repost_count` | integer | `148` |
| `reply_count` | integer | `18` |
| `quote_count` | integer | `34` |
| `bookmark_count` | integer | `154` |
| `languages` | array | `["en"]` |
| `links` | array | `[]` |
| `hashtags` | array | `[]` |
| `mentions` | array | `[]` |
| `images` | array | `[]` |
| `video` | null | `null` |
| `external` | object | `null` |
| `quoted_uri` | string | `"at://did:web:bsky38.com/app.bsky.feed.post/3muphbj35cs2h"` |
| `reply_parent_uri` | null | `null` |
| `reply_root_uri` | null | `null` |
| `is_repost` | boolean | `true` |
| `reposted_by_handle` | string | `"bsky.app"` |
| `reposted_by_did` | string | `"did:plc:z72i7hdynmk6r22z27h6tvur"` |
| `reposted_at` | string | `"2026-09-30T18:15:05.366Z"` |

Missing optional values are null or empty collections, depending on the field. Missing counts remain null. Counts and content are snapshots and can change between requests. Fields for other result types are absent.

### Input example

```json
{
  "handles": [
    "bsky.app"
  ],
  "maxItems": 10
}
```

Use the input schema for the remaining filters and defaults.

### Output example

This is a selected-field excerpt of the first object in [sample_output.json](sample_output.json).

```json
{
  "type": "error",
  "source": "search",
  "input": "bluesky",
  "error_code": "public_access_denied",
  "http_status": 403,
  "fetched_at": "2026-10-05T13:19:01.033361Z"
}
```

### Pricing

Proposed custom pay-per-event prices: **$0.0005 per post** and **$0.0005 per profile** ($0.50 per 1,000 successful results). No custom fee for errors, duplicates or empty results. Platform compute/storage usage is additional. Pricing is a proposal for the owner to configure before publication.

The highest-use comparable Bluesky post extractor in the [Store API query](https://api.apify.com/v2/store?search=bluesky\&limit=1000) on 2026-10-05 was [lexis-solutions/bluesky-posts-scraper](https://apify.com/lexis-solutions/bluesky-posts-scraper), with 322 total users: $0.0011/result on FREE, $0.00072 on its lowest tier and $0.004/start. The proposal is below its result price on every listed tier. See [PRICING.md](PRICING.md) for the comparison and publishing configuration.

### Limits and reliability

- Reads only the three official public AppView endpoints named below, without credentials.
- Pagination follows supplied cursors; an empty page, source cap or repeated cursor ends that source. Search pagination does not guarantee the entire result set.
- Each HTTP request has a 20-second timeout. Network failures, HTTP 429 and server errors receive at most three retries with exponential backoff and bounded Retry-After handling. HTTP 401/403 is not retried.
- A target failure or malformed individual post produces an uncharged error row while other targets continue. Storage and billing failures fail the run instead of being disguised as source errors.
- `maxItems` includes error rows. The actor also stops when the SDK reports that the configured event charge limit is reached.
- Deleted accounts/posts, public visibility restrictions, API changes, rate limits and search denial can reduce available results. Media URLs can expire; the actor exports references, not downloaded files.
- Cloud execution, actual paid billing and Docker execution have not been exercised locally.

### FAQ

**What limits the output?** The maxItems input caps output rows; its schema maximum is 100,000. Source availability and errors may yield fewer rows.

**Does it need a proxy, login, or API key?** The actor uses public AppView without login. Public keyword search can be denied; denied searches produce error rows.

**How is it priced?** PRICING.md proposes $0.0005 per unique post or profile row. Error and duplicate rows do not trigger those custom events; this proposal is unpublished.

**What happens on rate limits?** The client retries throttling and server failures with backoff and Retry-After. A denied public search is reported as an error.

**How fresh is the data?** Rows reflect the public AppView response at run time. Use fetched_at to judge retrieval freshness.

**Does it include private posts?** No login is used; the actor retrieves what the public AppView supplies.

### Use with AI agents / MCP

After the owner publishes the actor, add its actor identifier to an Apify MCP integration or invoke it through the Apify API. This project does not implement its own MCP server and has not been published. An agent can provide `handles` or `profileHandles` for public-account retrieval, cap the output with `maxItems`, then process JSON rows by `type`. Treat `type: error` as a source failure and preserve `fetched_at` for freshness.

The output schema exposes the default dataset items URL as `{{links.apiDefaultDatasetUrl}}/items`. Do not ask an agent to supply login credentials or bypass a public search denial.

### Development

Use the existing project Python environment without installing packages into it:

```text
C:/GritWork/earn/.venv/Scripts/python.exe -X utf8 -m pytest -q
C:/GritWork/earn/.venv/Scripts/python.exe -X utf8 -m pytest -q --live -m live -s
apify validate-schema
C:/GritWork/earn/.venv/Scripts/python.exe -X utf8 scripts/run_local.py examples/input.json --storage storage-demo
```

Run these commands from the actor directory. The local helper requires a fresh storage directory, sets `APIFY_LOCAL_STORAGE_DIR`, writes `key_value_stores/default/INPUT.json` and starts the same interpreter with `python -m src`. It clears inherited Apify cloud settings for local execution. No Apify CLI is used to run the actor. The standard storage layout is in `storage/`; `RUN_SUMMARY` records output counts, duration and event calls. Local charging is disabled by default, so a local event call is not evidence of paid cloud billing.

Real public response fixtures, fetch timestamps and source URLs are saved in `tests/fixtures/`. Controlled pagination and malformed variants are created in tests from those responses. The live smoke test is explicitly marked live and is opt-in. Docker installs the pinned runtime requirements from `requirements.txt` on `apify/actor-python:3.12`.

Official sources: [public API routing](https://github.com/bluesky-social/bsky-docs/blob/main/docs/advanced-guides/api-directory.mdx), [searchPosts lexicon](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/searchPosts.json), [getAuthorFeed lexicon](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/getAuthorFeed.json), [getProfile lexicon](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/actor/getProfile.json), [Apify output schema](https://docs.apify.com/actors/development/actor-definition/output-schema).

# Actor input Schema

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

Keyword or phrase searches. Calls searchPosts only when publicly allowed; a denial produces an uncharged error row. Exact duplicate queries are removed.

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

User-named public handles, did:plc/did:web identifiers, or https://bsky.app/profile/<account> URLs. Fetches posts and reposts using getAuthorFeed.

## `profileHandles` (type: `array`):

User-named handles, DIDs, or Bluesky profile URLs. Retrieves detailed public profiles using getProfile. Does not discover profiles by keyword.

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

Hard cap shared across post, profile and error rows. Duplicates are removed. Concurrent sources can finish in different orders.

## `maxPostsPerSource` (type: `integer`):

Limit on entries examined per query or author feed, including duplicates and malformed entries. Prevents endless pagination. Profiles do not use this option.

## `concurrency` (type: `integer`):

Maximum sources processed at once. Requests retry transient failures with bounded backoff.

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

Order requested from searchPosts. Does not change author feed ordering.

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

Filter sent to getAuthorFeed. Author feeds may include reposts; original author and reposter are separate fields.

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

Optional YYYY-MM-DD or ISO timestamp with timezone, such as 2026-10-01T00:00:00Z. Search only, based on server search timestamp.

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

Optional YYYY-MM-DD or ISO timestamp with timezone. Must be later than since. Search only.

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

Optional language tag such as en or tr. Sent to searchPosts; does not filter author feeds.

## Actor input object example

```json
{
  "queries": [],
  "handles": [
    "bsky.app"
  ],
  "profileHandles": [
    "bsky.app"
  ],
  "maxItems": 100,
  "maxPostsPerSource": 100,
  "concurrency": 3,
  "sort": "latest",
  "feedFilter": "posts_with_replies",
  "since": "",
  "until": "",
  "lang": ""
}
```

# Actor output Schema

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

Default dataset items with public post and profile results, plus 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 = {
    "queries": [],
    "handles": [
        "bsky.app"
    ],
    "profileHandles": [
        "bsky.app"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("grit-77/bluesky-posts-profiles-search").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": [],
    "handles": ["bsky.app"],
    "profileHandles": ["bsky.app"],
}

# Run the Actor and wait for it to finish
run = client.actor("grit-77/bluesky-posts-profiles-search").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": [],
  "handles": [
    "bsky.app"
  ],
  "profileHandles": [
    "bsky.app"
  ]
}' |
apify call grit-77/bluesky-posts-profiles-search --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,grit-77/bluesky-posts-profiles-search"
        }
    }
}
```

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/5YviE5a85ZTKlF5TQ/builds/pgfKc4LyRGXbYUV6f/openapi.json
