# Bluesky Account & Keyword Monitor (`muhzuhaib/bluesky-keyword-monitor`) Actor

Watch Bluesky accounts and keywords on a schedule and get only posts you have not seen before. Cross-run memory, Discord and Slack alerts, digest mode. Account watching needs no credentials.

- **URL**: https://apify.com/muhzuhaib/bluesky-keyword-monitor.md
- **Developed by:** [Muhammad Zuhaib Zahid](https://apify.com/muhzuhaib) (community)
- **Categories:** Social media, Automation, News
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 new match delivereds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bluesky Account & Keyword Monitor

Watch Bluesky accounts and keywords on a schedule. Each run remembers exactly which posts it has
already delivered and returns only the NEW ones, so you can poll every few minutes without noise and
without paying twice for the same post.

Built entirely on the open AT Protocol API (public AppView). No scraping, no unofficial endpoints.

![New posts only](https://api.apify.com/v2/key-value-stores/fzPqfSUYe9NdjD4vQ/records/bluesky-01-new-matches)

![Cross-run memory](https://api.apify.com/v2/key-value-stores/fzPqfSUYe9NdjD4vQ/records/bluesky-02-cross-run-memory)

![Discord alert](https://api.apify.com/v2/key-value-stores/fzPqfSUYe9NdjD4vQ/records/bluesky-03-webhook-alert)

### Read this before you start: which watches need a login

| Watch type | Credentials | Works out of the box |
|---|---|---|
| **Account watches** (every new post by a handle) | none | **Yes** |
| **Keyword watches** (search queries) | a free Bluesky app password | No |

Bluesky restricts anonymous use of its **search** endpoint from datacentre networks, which is where
this Actor runs. Account watches use a different endpoint and are unaffected.

To enable keyword watches, create a free app password in Bluesky under Settings, App Passwords, then
put your handle in `blueskyHandle` and the app password in `blueskyAppPassword`. An app password is
revocable and is not your account password.

The default input watches active accounts, so a run with no configuration works immediately, with
no credentials, from any network.

### What it does

- **Keyword watches** use the full Bluesky search syntax: `"exact phrase"`, `from:handle`,
  `mentions:handle`, `-negative`, `lang:en`, `has:media`, and any combination.
- **Account watches** deliver every new post by one account, straight from the stable
  author-feed endpoint.
- **Cross-run memory** - seen posts are stored between runs; a scheduled run emits only
  posts that are genuinely new.
- **Webhook alerts** - post each batch (or each match) to Discord, Slack, or your own
  receiver as ready-to-use payloads.
- **Digest mode** - write one summary item per run instead of one item per match; the
  cheapest way to poll frequently.
- **Filters** - minimum likes, languages, exclude replies, exclude reposts, per-watch caps.

### Quick start

1. Run the actor with the default input. It watches two active accounts (The New York Times
   and NPR), fetches their latest posts, and writes new posts to the dataset plus a run-stats
   summary item. Account watches need no credentials.
2. Open the **Input** panel and replace the watchlist with your own keywords and accounts.
3. Open the actor's **Schedule** tab and set an interval (15 minutes is a good start). Keep
   the look-back window (`sinceMinutes`) a bit longer than your interval.

### Authenticating (recommended)

Add your Bluesky handle and an **app password** (Bluesky: Settings, Privacy and security,
App passwords) to the input. Authentication raises API rate limits and avoids edge blocks on
unauthenticated traffic. Use an app password, never your main password - and you can revoke
it at any time in Bluesky settings.

### Pricing (pay per event)

You pay only for what the run actually delivers:

| Event | Price | When |
|---|---|---|
| new-match | $0.003 | each new matching post written to the dataset |
| digest-delivered | $0.01 | each digest-mode run that found new matches |
| alert-delivered | $0.01 | each webhook POST actually delivered (2xx) |
| apify-actor-start | $0.00005 | platform default, per GB per run |

A keyword with a few hundred new mentions per month costs well under a dollar. The per-run
spending cap shown on the run dialog always protects you. Re-running on the same data
charges nothing new, because already-seen posts are never emitted.

### Input reference

See the **Input schema** on the right for every field with descriptions and prefilled
examples. The default input is a working configuration.

### Troubleshooting

- **403 edge block** - Bluesky sometimes blocks unauthenticated API traffic from datacenter
  IPs. Add your handle and app password (see above); authenticated calls take the normal
  route. The actor stops with a clear message instead of retrying into a wall.
- **429 rate limit** - lower your schedule frequency (15 minutes unauthenticated, 1 minute
  authenticated are safe), or authenticate. The actor already retries with backoff.
- **No matches but the keyword is active** - check the look-back window: keyword search only
  returns posts newer than `sinceMinutes`. Raise it to cover your schedule gap.
- **Webhook fails** - the URL must be a public http(s) endpoint; private and local addresses
  are rejected by design. Delivery failures are logged in the run-stats errors list and never
  break the run.

### Privacy

Your app password is stored only in your own Actor input (use the secret field) and is sent
only to Bluesky's sign-in endpoint. Webhook URLs are sent only the alert payloads described
above. The actor stores only post URIs and timestamps between runs.

# Actor input Schema

## `watchlist` (type: `array`):

What to watch. ACCOUNT watches deliver every new post by a handle and work with no credentials. KEYWORD watches use Bluesky search syntax ("exact phrase", from:handle, mentions:handle, -exclude, lang:en, has:media) and require a Bluesky handle and app password below, because Bluesky restricts anonymous search from datacentre networks.

## `blueskyHandle` (type: `string`):

Your Bluesky handle, e.g. yourname.bsky.social. Providing it together with an app password raises API rate limits. Create the app password in Bluesky: Settings, Privacy and security, App passwords.

## `blueskyAppPassword` (type: `string`):

App password created in Bluesky settings. Use an app password, never your main password. Secret field.

## `alerts` (type: `object`):

Where to send new matches. Set a webhook URL and pick generic, Discord or Slack. Leave empty to write matches to the dataset only.

## `digest` (type: `boolean`):

Instead of one dataset item per match, write ONE summary item per run (matches listed inside it) and charge a single digest-delivered event. Cheapest way to poll frequently.

## `excludeReplies` (type: `boolean`):

Skip posts that are replies to other posts.

## `excludeReposts` (type: `boolean`):

Skip repost entries in account feeds.

## `minLikes` (type: `integer`):

Only deliver posts with at least this many likes (0 = all).

## `languages` (type: `array`):

Only deliver posts tagged with these ISO 639-1 codes, e.g. en, de. Empty = all languages.

## `maxPostsPerWatch` (type: `integer`):

Caps how many new posts one watch can deliver in a single run (and therefore the per-run cost).

## `sinceMinutes` (type: `integer`):

Keyword searches only return posts newer than this window. Keep it a bit longer than your schedule interval.

## `debug` (type: `boolean`):

Log per-watch API usage and cost-relevant counters.

## `stateStoreName` (type: `string`):

The named key-value store holding which posts have already been delivered. Change it to keep separate watch lists apart, for example one per client. Leave it alone unless you need that.

## Actor input object example

```json
{
  "watchlist": [
    {
      "type": "account",
      "handle": "nytimes.com",
      "label": "The New York Times"
    },
    {
      "type": "account",
      "handle": "npr.org",
      "label": "NPR"
    }
  ],
  "digest": false,
  "excludeReplies": true,
  "excludeReposts": true,
  "minLikes": 0,
  "languages": [],
  "maxPostsPerWatch": 100,
  "sinceMinutes": 1440,
  "debug": false,
  "stateStoreName": "bluesky-monitor-state"
}
```

# Actor output Schema

## `matches` (type: `string`):

Posts matching a watch that have not been delivered before. Each carries the author, text, timestamp, engagement counts, the watch it matched and a permalink.

## `runState` (type: `string`):

Per-watch match counts, API calls made, dedupe hits and any per-watch 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 = {
    "watchlist": [
        {
            "type": "account",
            "handle": "nytimes.com",
            "label": "The New York Times"
        },
        {
            "type": "account",
            "handle": "npr.org",
            "label": "NPR"
        }
    ],
    "languages": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("muhzuhaib/bluesky-keyword-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 = {
    "watchlist": [
        {
            "type": "account",
            "handle": "nytimes.com",
            "label": "The New York Times",
        },
        {
            "type": "account",
            "handle": "npr.org",
            "label": "NPR",
        },
    ],
    "languages": [],
}

# Run the Actor and wait for it to finish
run = client.actor("muhzuhaib/bluesky-keyword-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 '{
  "watchlist": [
    {
      "type": "account",
      "handle": "nytimes.com",
      "label": "The New York Times"
    },
    {
      "type": "account",
      "handle": "npr.org",
      "label": "NPR"
    }
  ],
  "languages": []
}' |
apify call muhzuhaib/bluesky-keyword-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,muhzuhaib/bluesky-keyword-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/2u4t7lWCfHE0INC2V/builds/Ev1s6amMa7XVA0PJu/openapi.json
