# Mastodon Keyword & Mention Monitor (`muhzuhaib/mastodon-monitor`) Actor

Watch Mastodon hashtags, keywords and accounts. Scheduled runs remember what they already saw and deliver only NEW matching posts, with optional webhook alerts (Discord, Slack, generic) and a digest mode. Built on the documented Mastodon API.

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

## Pricing

from $500.00 / 1,000 daily monitoring of one watches

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

## Mastodon Keyword & Mention Monitor

Watch Mastodon hashtags, keywords and accounts on a schedule. Each run remembers exactly
which posts it has already seen and delivers only the NEW matches, so you can poll every
few minutes without seeing the same post twice.

Built entirely on the documented Mastodon REST API. No scraping, no unofficial endpoints.

![Two scheduled runs: the second delivers only new posts](https://api.apify.com/v2/key-value-stores/fzPqfSUYe9NdjD4vQ/records/mastodon-shot-1.png)

### What it does

- **Hashtag watches** work with zero setup: every new post under a hashtag, straight from
  the public timeline endpoint.
- **Keyword watches** do full-text search across the instance: `"exact phrase"`,
  `-exclusions`, plain terms. They need a free access token from your own Mastodon account
  (Preferences, Development, New application, scope read:search).
- **Account watches** deliver every new post by one account (public lookup + statuses).
  Handles look like `user@instance` or a profile URL.
- **Cross-run memory:** seen posts are stored between runs; a scheduled run emits only
  posts that are genuinely new. Reblogs are credited to the original post, so the same
  post never arrives twice.
- **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, if you
  would rather read one post than many.
- **Filters:** minimum favourites, languages, exclude replies, exclude reblogs,
  per-watch caps, look-back window.
- **Any instance:** default https://mastodon.social; point `instance` at any server that
  exposes the standard Mastodon API. The run preflights the instance and fails with a
  clear message if it is unreachable.

### Quick start

1. Run the actor with the default input. It watches the hashtag `opensource` and the
   official `@mastodon` account (both work with no account and no token), and writes
   matching posts to the dataset plus a run-stats summary.
2. Open the **Input** panel and replace the watchlist with your own hashtags, 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.

![New matching posts in the dataset](https://api.apify.com/v2/key-value-stores/fzPqfSUYe9NdjD4vQ/records/mastodon-shot-2.png)

### Access token (only for keyword watches)

Hashtag and account watches need no account at all. Full-text keyword search on Mastodon
requires an access token: on your server open Preferences, Development, New application
(scope read:search), create it, and paste it into the input's secret field. The token is
stored only in your own actor input and is sent only to your instance's API.

### Pricing (pay per event)

One event, one price:

| Event | Price | When |
|---|---|---|
| watch-monitored-day | $0.50 | once per watch, per UTC day, on the first run that scans it successfully |
| apify-actor-start | $0.00005 | platform default, per GB per run |

- **The first day of every new watch is free.** Add a watch, see exactly what it finds,
  and decide before anything is charged.
- **The number of posts never changes the price.** A quiet day and a day with three
  hundred new posts cost the same, because you are paying for the watch being kept.
- **Run it as often as you like.** Hourly costs the same as daily: the day is charged
  once per watch.
- **A watch that fails is never charged.** If your instance is unreachable, or a keyword
  watch has no token, that watch costs nothing for that day.

So four watches on any schedule cost $2 a day, about $60 a month, whatever the volume.

### 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

- **"Keyword watches need an access token":** keyword search is token-only on Mastodon.
  Create the token (see above) or switch those watches to hashtag watches, which are
  token-free. One misconfigured watch never stops the others.
- **429 rate limit:** the documented ceiling is 300 requests per 5 minutes; a normal
  monitor uses a handful of calls per run. The actor retries with backoff and fails with
  a clear message only if the limit persists. Authenticating with a token gets its own,
  higher limits.
- **Instance unreachable:** check the `instance` field for typos; the run preflights
  /api/v1/instance before anything else and tells you exactly what failed.
- **No matches but the hashtag is active:** check the look-back window (`sinceMinutes`):
  only posts newer than the window are considered. 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 access token is stored only in your own Actor input (use the secret field) and is
sent only to your Mastodon instance. Webhook URLs are sent only the alert payloads
described above. The actor stores only post IDs and timestamps between runs.

# Actor input Schema

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

What to watch. Hashtag watches work with no setup. Keyword watches do full-text search and need an access token. Account watches deliver every new post by one account (accepts user@instance or a profile URL).

## `instance` (type: `string`):

Base URL of the server to watch, e.g. https://mastodon.social (default). Public data comes from this server's API.

## `mastodonToken` (type: `string`):

Create it on your Mastodon server: Preferences, Development, New application (scope read:search), then copy the token here. Hashtag and account watches work without it.

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

Optional webhook delivery. Leave the mode on 'none' to write results to the dataset only. Any target that resolves to a private, loopback or link-local address is refused, and redirects are never followed.

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

Instead of one dataset item per match, write ONE summary item per run (matches listed inside it). It does not change what you pay: billing is per watch per day either way.

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

Skip posts that are replies to other posts.

## `excludeReblogs` (type: `boolean`):

Skip boosted posts. When off, a reblog is credited to the original post so it is never charged twice.

## `minFavourites` (type: `integer`):

Only deliver posts with at least this many favourites (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`):

Only posts newer than this window are considered. Keep it a bit longer than your schedule interval.

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

Write extra diagnostic lines to the run log. It changes nothing about what is delivered or charged.

## `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.

## `maxEventsPerRun` (type: `integer`):

Hard spend guard: the most watch-days this run may charge. The dataset still receives everything found, and a watch-day the cap held back is billed on the next run instead of being lost. Default 200.

## Actor input object example

```json
{
  "watchlist": [
    {
      "type": "hashtag",
      "tag": "opensource"
    },
    {
      "type": "account",
      "handle": "https://mastodon.social/@mastodon"
    }
  ],
  "instance": "https://mastodon.social",
  "digest": false,
  "excludeReplies": true,
  "excludeReblogs": true,
  "minFavourites": 0,
  "languages": [],
  "maxPostsPerWatch": 100,
  "sinceMinutes": 1440,
  "debug": false,
  "stateStoreName": "mastodon-monitor-state",
  "maxEventsPerRun": 200
}
```

# Actor output Schema

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

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

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

Per-watch match counts, API calls made, dedupe hits, events charged, whether the spend cap was reached, 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": "hashtag",
            "tag": "opensource"
        },
        {
            "type": "account",
            "handle": "https://mastodon.social/@mastodon"
        }
    ],
    "instance": "https://mastodon.social",
    "languages": [],
    "maxEventsPerRun": 200
};

// Run the Actor and wait for it to finish
const run = await client.actor("muhzuhaib/mastodon-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": "hashtag",
            "tag": "opensource",
        },
        {
            "type": "account",
            "handle": "https://mastodon.social/@mastodon",
        },
    ],
    "instance": "https://mastodon.social",
    "languages": [],
    "maxEventsPerRun": 200,
}

# Run the Actor and wait for it to finish
run = client.actor("muhzuhaib/mastodon-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": "hashtag",
      "tag": "opensource"
    },
    {
      "type": "account",
      "handle": "https://mastodon.social/@mastodon"
    }
  ],
  "instance": "https://mastodon.social",
  "languages": [],
  "maxEventsPerRun": 200
}' |
apify call muhzuhaib/mastodon-monitor --silent --output-dataset

```

## MCP server setup

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