# Bluesky Keyword Monitor: AT Protocol Post Alerts (`accountable_eel/bluesky-keyword-monitor`) Actor

Bluesky keyword monitor: get Bluesky posts matching a keyword from the last 24 hours, in one language if you like, or only new posts for a keyword since the last run. One row per public post via AT Protocol searchPosts. Export a Bluesky keyword search to Google Sheets, CSV or n8n.

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

## Pricing

from $1.00 / 1,000 bluesky post returneds

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 Keyword Monitor: Public Post Search on a Schedule

Give it keywords or phrases and it searches public Bluesky posts for each one, returning a row per post with the text, the author, the link and the like, repost and reply counts. Turn on monitoring and each scheduled run returns only the posts that appeared since the previous one.

### Who it's for

**A developer-relations lead for an open-source project.** People ask questions about your library on Bluesky without ever tagging you. A search for the project's name and its common misspellings, run every few hours, shows you the unanswered ones while they are still fresh.

**A reporter covering one narrow beat.** You follow a regulation, a product recall or a court case. You want every public post that mentions it, newest first, with a link you can cite, collected in a sheet by the end of the day.

**A small-brand social manager who moved audience from another network.** You want to know when anyone writes your brand name, and whether the post is a reply or a fresh mention, without scrolling a feed.

### Why this one

- **No account, no API key.** The search is anonymous and public.
- **Newest first, filtered by time.** Posts are requested in latest-first order inside a time window you set, from one hour to thirty days.
- **Counts included.** Likes, reposts, replies and quotes arrive with each post, so you can sort by reach.
- **Monitoring that remembers.** Each keyword's last-seen post is stored in your own Apify account, so a scheduled run does not return the same post twice.

### What you get

One row per post.

| Field | Meaning |
| --- | --- |
| `keyword` | The search term that matched. |
| `postUrl` | The `bsky.app` link to the post. |
| `text` | The post text. |
| `authorHandle`, `authorDisplayName`, `authorDid` | Who wrote it. |
| `createdAt` | When the post was written, as an ISO timestamp. |
| `likeCount`, `repostCount`, `replyCount`, `quoteCount` | Engagement at the time of the run. |
| `isReply` | `true` when the post is a reply to another. |
| `cid`, `uri` | Bluesky's content id and AT Protocol address, stable identifiers for the post. |
| `isNew`, `firstSeenAt` | Monitoring flags. They are empty when monitoring is off. |

Every row also carries `postCount`, the number of posts the keyword produced in that run.

### Why search runs on api.bsky.app

Bluesky serves the same search from two public hosts, and they do not behave alike. On 2026-10-08 we sent the identical request, a search for the word "bluesky" with a limit of one, to both:

| Host | Result |
| --- | --- |
| `public.api.bsky.app` | HTTP 403 |
| `api.bsky.app` | HTTP 200 with posts |

This actor therefore calls `https://api.bsky.app/xrpc/app.bsky.feed.searchPosts` and nothing else. Two practical things follow. It needs no credentials, which is why no login field exists. And if Bluesky ever restricts that host, the symptom would be a failed-request row, which is not billed, instead of posts.

The search is requested 100 posts at a time, up to 20 pages, and stops at `maxResults`, which defaults to 100 per keyword and cannot exceed 1,000.

### Pricing

- **Bluesky post returned**: $2 per 1,000 posts

Plus a $0.002 start fee per run. Each event above is billed independently, only when it actually returns data — misses (`found:false`) are never charged.

**Empty results are not billed.** A keyword that produces no posts still returns a single row with `found: true` and `postCount: 0`, but that row is not charged. A monitored run in which nothing is new is not charged either; you pay only the start fee. This was a billing bug in earlier builds (an empty keyword was charged one post, $0.002), fixed and confirmed with a local run on 2026-10-08.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `keywords` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~bluesky-keyword-monitor/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"keywords":["atprotocol"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

### Input

```json
{
  "keywords": [
    "atprotocol"
  ]
}
```

One keyword or search phrase per line. Accepted formats: atprotocol, "open source", apify.

Other settings:

- `sinceHours` is the look-back window for the first search, from 1 to 720 hours; the default is 24.
- `language` is a two-letter code such as `en` or `de` that limits results to that language.
- `maxResults` is the cap per keyword, 100 by default.
- `deltaMode` switches on monitoring. `deltaName` names the watchlist.
- `includeKeywords` keeps only the rows that mention one of your words, and `excludeKeywords` drops any row that mentions one. In a monitored run the lists are applied before the watch records a post, so a post they hide is not remembered and appears as new if you loosen the lists later.
- `testRun` runs only the first five keywords, and `onlyFound` hides rows for keywords with no result.

### Sample output

| query | found | status | postCount | keyword | postUrl | text | authorHandle | authorDisplayName | authorDid | createdAt | cid | uri | likeCount | repostCount | replyCount | quoteCount | isReply | isNew | firstSeenAt | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| atprotocol | true | OK | <posts returned> | atprotocol | <post url> | <post text> | <author handle> | <author name> | <author did> | <created at> | <cid> | <at uri> | <likes> | <reposts> | <replies> | <quotes> | <is reply> | <is new> | <first seen> | 1970-01-01T00:00:00.000Z |

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~bluesky-keyword-monitor/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"keywords":["atprotocol"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~bluesky-keyword-monitor/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"keywords":["atprotocol"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~bluesky-keyword-monitor/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"keywords":["{{keyword}}"]}`, mapping the row's keyword into the `keywords` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Bluesky Keyword Monitor | Apify" — the agent will find and run this actor.

### Tips

- **Always set `deltaName` for a real watchlist.** If you leave it empty, the monitoring record is keyed on a hash of your whole keyword list. Add or remove one keyword and the key changes, which restarts the baseline for every keyword.
- **Monitoring is more than the flag.** Setting `deltaName` alone also records posts, but every post is still returned and billed with `isNew` marking the new ones. Set `deltaMode` to return only the new ones.
- **The first monitored run uses the time window.** It returns posts from the last `sinceHours` hours. Later runs ask Bluesky only for posts after the newest one already recorded.
- **Quote exact phrases.** A multi-word term such as "open source" is passed to Bluesky as written; how Bluesky matches it is Bluesky's behaviour.
- **Filter noise with `excludeKeywords`.** Add words like "giveaway" to drop the posts you never want to pay for.

### vs. alternatives

| | What it costs | What you get | Trade-off |
| --- | --- | --- | --- |
| **This actor** (`bluesky-keyword-monitor`) | $0.002 per post returned, $0.002 per run | A dataset of posts you read or pipe wherever you like, with a remembered baseline per keyword | It does not push messages to a chat app, and a quiet run costs only the start fee |
| **Bluesky Mentions Monitor** (`bluesky-mentions-monitor`) | $0.002 per post, the same rate | Keyword alerts delivered to Slack, Discord or Telegram, per its own page | If you want alerts in a chat channel, use it. This actor is for a spreadsheet, a database or your own workflow. |

### FAQ

#### Can it see private or followers-only posts?

No. It searches the public index only.

#### Why did the first run return old posts?

The first search covers the look-back window, 24 hours by default. Lower `sinceHours` to narrow it.

#### Do likes and reposts update later?

No. The counts are a snapshot taken when the run fetched the post.

#### Are replies included?

Yes, and `isReply` tells you which rows are replies.

### Related actors

- [Bluesky Mentions Monitor](https://apify.com/accountable_eel/bluesky-mentions-monitor) sends new-post alerts to Slack, Discord or Telegram.

# Actor input Schema

## `keywords` (type: `array`):

One keyword or search phrase per line to monitor natively across AT Protocol posts on Bluesky. Accepted formats: atprotocol, "open source", apify. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

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

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Caps how many posts to retrieve per keyword query.

## `deltaMode` (type: `boolean`):

Turns each keyword search into a monitor. Stores the latest post CID/indexedAt timestamp in the Apify KV store and returns only posts strictly newer than the stored baseline.

## `deltaName` (type: `string`):

Custom watchlist key for storing state in the Apify KV store.

## `sinceHours` (type: `integer`):

Time window for initial search, from 1 to 720 hours.

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

A two-letter language code such as en or de.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `expandRows` (type: `boolean`):

When on, each post found gets its own row instead of being grouped under its keyword. You're still only charged once per keyword, no matter how many rows it produces.

## `maxConcurrency` (type: `integer`):

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

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

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "keywords": [
    "atprotocol"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "maxResults": 100,
  "deltaMode": false,
  "deltaName": "",
  "sinceHours": 24,
  "language": "",
  "columns": [
    "keyword",
    "postUrl",
    "text",
    "authorHandle",
    "authorDisplayName",
    "createdAt",
    "cid",
    "uri",
    "likeCount",
    "repostCount",
    "replyCount",
    "isNew",
    "firstSeenAt"
  ],
  "expandRows": true,
  "maxConcurrency": 2,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# 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 = {
    "keywords": [
        "atprotocol"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/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 = {
    "keywords": ["atprotocol"],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/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 '{
  "keywords": [
    "atprotocol"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/bluesky-keyword-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/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/7ckArAB4FpvdCihA7/builds/Unc13Fknj69E92T4n/openapi.json
