# Bluesky Post Monitor (unofficial) (`plusonets/bluesky-post-monitor`) Actor

Unofficial monitor for public Bluesky author feeds, keywords, and hashtags. Drops deleted posts and can return only new posts across runs. Not affiliated with Bluesky.

- **URL**: https://apify.com/plusonets/bluesky-post-monitor.md
- **Developed by:** [Olalekan Johnson](https://apify.com/plusonets) (community)
- **Categories:** Social media, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 posts

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 Post Monitor (unofficial)

Watch public Bluesky posts from an author feed, a keyword, or a hashtag, and keep only the posts that are still on the network when the run fetches them. Optional new-post tracking skips URIs this Actor already saved.

This Actor is not affiliated with, endorsed by, or maintained by Bluesky, Bluesky Social, PBC, or the AT Protocol team. It is an independent client of Bluesky's public AppView. It never posts, likes, follows, replies, or sends a notification.

### What you can do with it

- Snapshot a public profile feed, starting with the default handle `bsky.app`, with no Bluesky account.
- Search recent posts for a phrase, or collect a hashtag.
- Run several handles or phrases in one go. Each value is requested on its own, then rows are de-duplicated by post URI.
- Schedule **New posts only** so the next run returns posts you have not already saved for that state namespace.
- Drop posts whose text contains an exclude keyword.

Social listening, PR checks, and research exports are the intended use. The Actor is read-only.

### What works without a login

Checked against the live AppView on 23 October 2026:

- `GET https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed` returns posts with no token. **Author mode is the default**, and it is what the Apify daily test runs.
- `GET https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts` currently returns an HTML 403 from the edge. That response is not a JSON "please log in" error.
- `GET https://api.bsky.app/xrpc/app.bsky.feed.searchPosts` returns search results with no token. Search requires the `q` parameter. Hashtags are sent as `q` plus `tag`.

Search and hashtag modes call `api.bsky.app` directly so they do not depend on the blocked public host. If Bluesky later refuses those calls with HTTP 401 or 403, add your handle and an **app password** (created in the Bluesky app's settings, not your main password). The password field is secret. It is not printed in the log, not saved on the post, and not written into the seen-URI store. Leave both login fields empty for the public author feed.

The Actor follows Bluesky's developer guidelines for a read-only client: it does not generate interactions, and it does not keep a copy of a post the AppView no longer returns as a live record. Deleted, missing, and blocked post views are dropped. Past dataset rows from earlier runs stay where they are, because Apify datasets are append-only; this run simply will not write that post again.

### Input

Author feed, no login:

```json
{
  "mode": "author",
  "queries": ["bsky.app"],
  "maxPosts": 5,
  "newPostsOnly": false
}
```

Keyword watch across runs:

```json
{
  "mode": "search",
  "queries": ["apify", "web scraping"],
  "excludeKeywords": ["hiring"],
  "since": "2026-10-01T00:00:00Z",
  "newPostsOnly": true,
  "stateNamespace": "brand-watch",
  "maxPosts": 50
}
```

Hashtag:

```json
{
  "mode": "hashtag",
  "queries": ["python"],
  "maxPosts": 20
}
```

Optional login, only if search is refused:

```json
{
  "mode": "search",
  "queries": ["bluesky"],
  "identifier": "yourname.bsky.social",
  "appPassword": "xxxx-xxxx-xxxx-xxxx"
}
```

| Field | Notes |
| --- | --- |
| Mode | `author`, `search`, or `hashtag` |
| Queries | Handles, keywords, or tags. The `@` and a leading `#` are optional |
| Exclude keywords | Case-insensitive, applied to post text |
| Posted since | ISO date or datetime. Also sent to search |
| New posts only | Skip URIs already saved. Default off, so the daily test stays non-empty |
| State namespace | Separate watches in one account |
| Maximum posts | 1 to 200, default 5 |
| Maximum charge (USD) | Extra cap, with the Apify run cap. The lower one wins |
| Handle and app password | Optional. Both or neither |

A new-posts run that finds nothing new succeeds. Any other run that saves nothing fails, including a login failure or a handle the AppView does not know.

### Output

```json
{
  "uri": "at://did:plc:example/app.bsky.feed.post/3abc",
  "cid": "bafyrei...",
  "url": "https://bsky.app/profile/bsky.app/post/3abc",
  "authorHandle": "bsky.app",
  "authorDid": "did:plc:example",
  "authorDisplayName": "Bluesky",
  "text": "Post text as returned by the AppView.",
  "createdAt": "2026-10-22T18:00:00.000Z",
  "indexedAt": "2026-10-22T18:00:01.000Z",
  "langs": ["en"],
  "tags": ["example"],
  "likeCount": 12,
  "repostCount": 2,
  "replyCount": 1,
  "quoteCount": 0,
  "replyTo": null,
  "embedType": null,
  "matchedQuery": "bsky.app"
}
```

`SUMMARY` in the default key-value store has the post count, the estimated charge, whether the run authenticated, and errors. It does not contain the app password or the access token.

### Pricing

Pay per event. The only custom event is `post`. Do not price `apify-default-dataset-item`, or each saved post would be billed twice. Set `post` as the primary event.

| Event | Price | When it fires |
| --- | --- | --- |
| `apify-actor-start` | $0.005 | Once per run, by the platform (memory is capped at 1 GB, so once). The Actor does not call `Actor.charge` for it. |
| `post` | $0.001 | One live public post was saved |

One hundred posts cost $0.005 + 100 × $0.001 = $0.105. Platform usage is not billed to you. The Actor stops when the next post would pass the run maximum charge or `maxChargeUsd`. Posts that were deleted, blocked, excluded by keyword, older than `since`, or already seen are not charged.

### FAQ

**Is this an official Bluesky product?**
No. It calls Bluesky's public HTTP API the same way any other read-only client can. The listing must not imply a partnership.

**Why does the default use an author feed instead of search?**
The daily test has to return data with the prefilled input and no secrets. Author feeds on `public.api.bsky.app` do that today. Search is available as a mode, on the host that currently allows it without a token.

**Do I have to create a Bluesky account?**
No. Add a handle and app password only if a search or hashtag request comes back unauthorized. The password input is marked secret and is not required, so the daily test is not skipped.

**What happens to deleted posts?**
If the AppView returns a not-found or blocked view, or a record that is no longer a post, that post is not saved. The seen store holds URIs, not text, so this Actor is not a private archive of deleted posts. Rows written by an earlier run remain in that run's dataset.

**Can one run watch several keywords?**
Yes. Put them in **queries**. A post that matches two queries is saved once.

**Does it respect Bluesky's developer guidelines?**
It is a read-only monitor: no automated follows, likes, replies, or other interactions that would notify someone. It drops posts the API says are gone. Broader duties in those guidelines (in-app reporting tools, a public contact address) apply to products that publish user-generated content of their own. This Actor does not.

### Limits

- Public AppView reads only. No private profiles, no mutes, no DMs.
- Search availability can change. The 23 October 2026 check found unauthenticated search on `api.bsky.app` and an edge block on `public.api.bsky.app`. The optional app password is the fallback.
- At most five pages per query, and at most 200 saved posts per run.
- Seen URIs are capped at 10,000 per namespace.
- Counts (likes, reposts, replies) are whatever the AppView returned at fetch time.
- The client waits half a second between requests to the same host and retries server errors. It does not try to look like a browser.
- Platform usage is not billed to you; you pay only the events above.

### Run it locally

From this directory, with `monitor_core` vendored beside the package:

```bash
mkdir -p storage/key_value_stores/default
printf '%s\n' '{"mode":"author","queries":["bsky.app"],"maxPosts":3}' > storage/key_value_stores/default/INPUT.json
python -m bluesky_post_monitor
```

### Publish notes

Use limited permissions. Copy `.actor/pay_per_event.json` into pay-per-event monetization, with `post` as the primary event, and leave `apify-default-dataset-item` at $0 or remove it. Keep "unofficial" in the title.

# Actor input Schema

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

author reads a public profile feed and works without a login. search finds posts by keyword. hashtag finds posts with a tag facet. Search and hashtags call the public AppView and do not need a login today; if Bluesky starts requiring one, add a handle and app password below.

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

In author mode, Bluesky handles such as bsky.app (the @ is optional). In search mode, keywords. In hashtag mode, tags with or without a leading #. Each value is requested separately. Results are de-duplicated by post URI.

## `excludeKeywords` (type: `array`):

Drop a post when its text contains any of these words. Matching is case-insensitive.

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

Keep posts created on or after this ISO date or datetime. Search and hashtag requests also send this to the AppView.

## `newPostsOnly` (type: `boolean`):

Skip post URIs already saved for this state namespace. Leave this off for a one-shot export and for the daily test. A scheduled watch can turn it on; a run that finds nothing new still succeeds.

## `stateNamespace` (type: `string`):

Separate saved-URI lists when you watch more than one query in the same account.

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

Stop after this many posts have been saved.

## `maxChargeUsd` (type: `number`):

Optional extra cap. The Actor also stops at the run maximum charge Apify passes in ACTOR_MAX_TOTAL_CHARGE_USD. The lower of the two wins.

## `identifier` (type: `string`):

Your Bluesky handle, only if you want authenticated requests. Leave empty for the public author feed. Not an official Bluesky login form; this Actor is independent.

## `appPassword` (type: `string`):

App password for the handle above. Stored encrypted by Apify. It is not written to the dataset, the log, or the seen-post store. Leave empty unless search requires a login.

## Actor input object example

```json
{
  "mode": "author",
  "queries": [
    "bsky.app"
  ],
  "newPostsOnly": false,
  "stateNamespace": "default",
  "maxPosts": 5
}
```

# Actor output Schema

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

Public posts in the default dataset.

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

Post count, estimated charge, and errors. The summary does not contain the app password.

# 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": "author",
    "queries": [
        "bsky.app"
    ],
    "newPostsOnly": false,
    "stateNamespace": "default",
    "maxPosts": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("plusonets/bluesky-post-monitor").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": "author",
    "queries": ["bsky.app"],
    "newPostsOnly": False,
    "stateNamespace": "default",
    "maxPosts": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("plusonets/bluesky-post-monitor").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": "author",
  "queries": [
    "bsky.app"
  ],
  "newPostsOnly": false,
  "stateNamespace": "default",
  "maxPosts": 5
}' |
apify call plusonets/bluesky-post-monitor --silent --output-dataset

```

## MCP server setup

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

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/x0ORqyWYe3HxJQd9a/builds/eMGtOPDcBYQxaO1Cq/openapi.json
