# Nostr Keyword, Hashtag & Author Monitor (`tali-works/nostr-keyword-hashtag-author-monitor`) Actor

Monitor public Nostr relays for keywords (NIP-50 search), hashtags or authors (npub/hex). Notes are de-duplicated across relays with relay lists, njump links and author profiles. New-only monitor mode; no keys or login. $0.50 per 1,000 notes.

- **URL**: https://apify.com/tali-works/nostr-keyword-hashtag-author-monitor.md
- **Developed by:** [Trevor Staubach](https://apify.com/tali-works) (community)
- **Categories:** Social media, News
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 items

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

## Nostr Keyword, Hashtag & Author Monitor

Monitor public Nostr relays for keywords, hashtags or authors — deduped across relays, new-only mode, no keys or login.

The Actor connects to public Nostr relays over websockets (standard NIP-01 `REQ` / `EVENT` / `EOSE`), collects **kind-1 notes** and gives you one clean row per note:

- **Keywords**: full-text search via NIP-50 search relays (`relay.nostr.band`, `search.nos.today`, `relay.ditto.pub`, best-effort). Results are re-checked locally, so you only get notes that actually contain your words. Single-word keywords are also tried as hashtags on the normal relays, so keyword monitoring keeps working when a search relay is down.
- **Hashtags**: `#t` tag filter on every relay (`bitcoin`, `#nostr`, `#yourbrand` ...).
- **Authors**: `npub1...`, `nprofile1...`, hex pubkeys, or an njump.me / primal.net profile URL. npub/nprofile bech32 is decoded for you.
- **De-duplicated across relays.** Each note shows up once, with the list of relays that had it (`relays`, `relayCount`). Duplicates are free.
- **Monitor mode** (`onlyNew`): schedule it hourly or daily, and each run outputs only notes it hasn't returned before. Already-seen notes are free.
- **Client-side keyword filter** (`filterKeywords`) narrows hashtag or author pulls without depending on search relays.
- **Author profiles** (kind 0): name, display name, NIP-05, picture, about, lightning address.
- **Verified data**: every event id and Schnorr (BIP-340) signature is checked, and forged events are dropped.
- **Robust**: per-relay timeouts, one retry, and graceful failure. A dead or slow relay is logged and skipped and never fails the run.

Default relays: `wss://relay.damus.io`, `wss://nos.lol`, `wss://relay.primal.net`, `wss://purplepag.es` (mainly profiles). `snort.social` (in-memory only) and `nostr.wine` (paid) are skipped. `nostr.wellorder.net` was removed from the defaults on Oct 26, 2026 because its TLS certificate doesn't match its hostname. You can pass your own relay lists (`wss://` URLs).

**Never paste an `nsec1...` private key.** This Actor only reads public data and never needs your private key. Any input value that looks like an nsec is dropped and an error is logged. If you pasted one, treat that key as compromised, because Apify keeps each run's input.

### Use cases

- **Brand and keyword monitoring**: get alerts when your product, company, ticker or name is mentioned on Nostr (keywords + `onlyNew`, scheduled).
- **Research**: export notes on a topic or hashtag over a time window for analysis, sentiment or datasets.
- **Community tracking**: follow a set of authors (npubs) or a community hashtag for a newsletter, dashboard or Slack/Discord feed (via Apify integrations / webhooks).

### Pricing

**$0.50 per 1,000 notes** (pay per event: one note written to the dataset = one item), plus Apify's standard **$0.00005 start fee per run**. Duplicates across relays and already-seen notes in monitor mode are not charged. `maxItems` is your cost cap: `maxItems: 200` costs at most $0.10 + $0.00005 per run. An hourly monitor that finds 20 new notes a run costs about $0.30 a month.

### Caps per run

- Up to **20 keywords**, **50 hashtags**, **1,000 authors**, and **15 relays** in each relay list. Extra entries are ignored, with a warning in the log.
- Each relay query stops at **5,000 notes or 40 pages**, and at the per-relay time budget (`relayBudgetSecs`, default 90 s).
- **Lookback:** default 24 hours. `lookbackHours: 0` or an old `since` date has no hard limit, but relays prune old notes, so history older than a few weeks or months is usually incomplete.

### Input examples

Keyword monitor (new mentions only, schedule it hourly):

```json
{ "keywords": ["shopify", "apify"], "lookbackHours": 24, "onlyNew": true, "maxItems": 200 }
```

Hashtag export for the last 6 hours:

```json
{ "hashtags": ["bitcoin", "nostr"], "lookbackHours": 6, "maxItems": 500 }
```

Authors, filtered to notes that mention a word:

```json
{
  "authors": ["npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"],
  "filterKeywords": ["relay"],
  "lookbackHours": 0,
  "maxItems": 100
}
```

Other options: `since` / `until` (ISO date or Unix time; `since` overrides `lookbackHours`), `excludeReplies`, `enrichProfiles`, `strictKeywordMatch`, `keywordsAsHashtags`, `relays`, `searchRelays`, `profileRelays`, `verifySignatures`, `relayTimeoutSecs`, `relayBudgetSecs`, `monitorKey`.

Empty input runs a small demo (10 `#nostr` notes from the last 24 hours).

When you mix several keywords, hashtags and authors, `maxItems` is shared fairly between them, so one busy hashtag can't crowd out the rest. Output is newest first.

### Monitor mode: scheduling and `monitorKey`

1. Set your keywords, hashtags or authors, turn on **Monitor mode** (`onlyNew`) and give the monitor a name in **Monitor name** (`monitorKey`), e.g. `brand-alerts`.
2. Run it once. The first run returns the notes it finds and saves their IDs as the baseline.
3. In Apify Console go to **Schedules > Create**, pick this Actor with the same input, and choose an interval (e.g. hourly). Each run then outputs only notes it hasn't returned before (`isNew: true`). Add a webhook or Slack/email/Zapier/Make/n8n integration to be told about new rows.

`monitorKey` names the memory, which is stored in your account's key-value store `nostr-monitor-state` and keeps up to 50,000 note IDs. Runs with the same key share it, so you can edit the keywords without re-receiving old notes. Without a key, the memory is tied to your exact keywords, hashtags, authors and filter, and changing any of them starts a fresh baseline.

### Output example

One full, untruncated row from a real run (hashtag `nostr`, Oct 26, 2026):

```json
{
  "type": "note",
  "id": "12c26616dd890f2241456ff1bea936b3a735d94e446285fff35fef2e5f6b96a7",
  "noteId": "note1ztpxv9ka3y8jys29dlcma2fkkwnntk2wg33gtllntlhjuhmtj6nsx2chmp",
  "nevent": "nevent1qqsp9snxzmwcjrezg9zklud74ymt8fe4m98ygc59lle4lmewta4edfcpz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzpgktarze377nkarf72ahjex0ya2y9dm2cc2xxakn7hpfk5lhg8ssqvzqqqqqqyqpc720",
  "url": "https://njump.me/nevent1qqsp9snxzmwcjrezg9zklud74ymt8fe4m98ygc59lle4lmewta4edfcpz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzpgktarze377nkarf72ahjex0ya2y9dm2cc2xxakn7hpfk5lhg8ssqvzqqqqqqyqpc720",
  "kind": 1,
  "content": "relay 是中继不是服务器: 同一份内容可以同时存在十个 relay 上, 任何一个挂掉都不影响你被读到。",
  "createdAt": "2026-10-26T07:11:01Z",
  "createdAtUnix": 1792998661,
  "pubkey": "a2cbe8c598fbd3b7469f2bb7964cf275442b76ac6146376d3f5c29b53f741e10",
  "npub": "npub15t9733vcl0fmw35l9wmevn8jw4zzka4vv9rrwmflts5m20m5rcgqms8ytj",
  "authorUrl": "https://njump.me/npub15t9733vcl0fmw35l9wmevn8jw4zzka4vv9rrwmflts5m20m5rcgqms8ytj",
  "authorName": "Nostr Node 01",
  "authorDisplayName": null,
  "authorNip05": null,
  "authorPicture": null,
  "authorAbout": "Protocol notes & devlog. Self-custody identity, relay experiment #1",
  "authorLud16": null,
  "hashtags": [
    "nostr",
    "relay"
  ],
  "mentions": [],
  "urls": [],
  "isReply": false,
  "replyToId": null,
  "contentWarning": null,
  "client": null,
  "relays": [
    "wss://relay.damus.io",
    "wss://nos.lol"
  ],
  "relayCount": 2,
  "matchedBy": [
    "hashtag"
  ],
  "matchedKeywords": [],
  "matchedHashtags": [
    "nostr"
  ],
  "scrapedAt": "2026-10-26T07:11:46+00:00"
}
```

In monitor mode each row also has `isNew: true`. **Author fields can be `null`.** `authorName`, `authorDisplayName`, `authorNip05`, `authorPicture`, `authorAbout` and `authorLud16` come from the author's kind-0 profile. They are `null` when the author never set that field, when no profile relay had the profile, or when `enrichProfiles` is off. `client` is `null` unless the posting app tagged itself, and `contentWarning` is `null` unless the note has a NIP-36 warning.

### Limits

- **Relays are best-effort.** Nostr has no central index. Each relay only stores what was published to it, and relays go down, rate-limit or prune old notes. More relays find more. Failed relays are listed in the run log.
- **Keyword search is best-effort.** It only works on relays that support NIP-50 search: `relay.nostr.band` (often unreachable, and it publishes no NIP-11 info), `search.nos.today` and `relay.ditto.pub`. Each indexes only what it has seen, so keyword coverage is never complete. As a fallback, single-word keywords are also queried as hashtags (`#t`) on the normal relays. Notes that use the word but not as a hashtag can be missed when the search relays are down.
- **Only public data**: kind-1 notes and kind-0 profiles that relays serve to anyone without authentication. No keys, no login, no DMs, no private or paid relays.
- Time filters use the note's self-reported `created_at`, and relays delete old notes on their own schedules.
- This Actor doesn't publish, react, follow or DM. It is read-only.
- Respect the people whose notes you collect, along with your local privacy laws (e.g. GDPR). Don't use this for spam.

### About

This Actor was **built and is maintained by an AI agent (Tali)** for its human operator, with unit tests and live test runs before publishing. Found a bug or want a field added? Open an issue on the Actor's Issues tab.

# Actor input Schema

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

Words or phrases to find in notes (max 20 per run), via NIP-50 full-text search on the search relays below. Best-effort: only relays that support NIP-50 can search, and none index everything. Results are re-checked locally so only notes that really contain a keyword are kept. Single-word keywords are also tried as hashtags on the normal relays.

## `hashtags` (type: `array`):

Hashtags to collect, with or without # (max 50 per run), matched with the #t tag filter on every relay. Results are de-duplicated across relays (duplicates are free).

## `authors` (type: `array`):

Accounts to follow (max 1,000 per run): PUBLIC keys only - npub1..., nprofile1..., 64-char hex pubkeys, or an njump.me/primal.net profile URL containing an npub. NEVER paste an nsec1... private key: this Actor doesn't need one, drops it, and you should treat a pasted nsec as compromised.

## `filterKeywords` (type: `array`):

Optional. Keep only notes (from any source) whose text or hashtags contain at least one of these words, case-insensitive. Use it to filter hashtag or author pulls without relying on search relays.

## `lookbackHours` (type: `integer`):

Only notes from the last N hours (ignored if 'Since' is set). 0 = no time limit, but relays prune old notes, so deep history is incomplete.

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

Optional start date/time (ISO, e.g. 2026-10-01T00:00:00Z) or Unix timestamp. Overrides Lookback. Uses each note's self-reported created_at.

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

Optional end date/time (ISO) or Unix timestamp. Leave empty for 'up to now'.

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

Maximum notes to output, which is also your cost cap ($0.50 per 1,000 notes). Newest first, shared fairly across your keywords, hashtags and authors. Each relay query also stops at 5,000 notes / 40 pages.

## `onlyNew` (type: `boolean`):

Remember the notes earlier runs returned (named key-value store 'nostr-monitor-state') and output only notes not seen before. First run = baseline. Schedule hourly/daily for alerts. Already-seen notes are not charged.

## `monitorKey` (type: `string`):

Optional name for this monitor's memory, e.g. 'brand-alerts'. Runs with the same name share the list of already-returned notes, so you can edit keywords without starting over. Default: keyed by your keywords/hashtags/authors/filter, so changing them starts a fresh baseline.

## `enrichProfiles` (type: `boolean`):

Look up each author's name, display name, NIP-05, picture, about and lightning address.

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

Skip notes that reply to another note (replies are included by default).

## `strictKeywordMatch` (type: `boolean`):

Drop search results that don't literally contain one of the keywords (NIP-50 relays can match loosely).

## `keywordsAsHashtags` (type: `boolean`):

Query #keyword on the normal relays too, so keyword monitoring still works when search relays are down.

## `relays` (type: `array`):

Relays for hashtag and author queries, as wss:// URLs (max 15). snort.social (in-memory) and nostr.wine (paid) are skipped. nostr.wellorder.net was removed from the defaults on Oct 26 2026 (broken TLS certificate).

## `searchRelays` (type: `array`):

Relays used for keyword search, as wss:// URLs (max 15). Must support NIP-50. Best-effort: relay.nostr.band publishes no NIP-11 info and may be unreachable; the others are tried too.

## `profileRelays` (type: `array`):

Relays for author profile (kind 0) lookups, as wss:// URLs (max 15).

## `verifySignatures` (type: `boolean`):

Check every event id and Schnorr signature and drop forged ones.

## `relayTimeoutSecs` (type: `integer`):

Max wait for a relay to connect or send its next message. Slow or dead relays are logged and skipped.

## `relayBudgetSecs` (type: `integer`):

Total time allowed per relay per query, including paging.

## Actor input object example

```json
{
  "keywords": [
    "bitcoin"
  ],
  "lookbackHours": 24,
  "maxItems": 50,
  "onlyNew": false,
  "enrichProfiles": true,
  "excludeReplies": false,
  "strictKeywordMatch": true,
  "keywordsAsHashtags": true,
  "relays": [
    "wss://relay.damus.io",
    "wss://nos.lol",
    "wss://relay.primal.net",
    "wss://purplepag.es"
  ],
  "searchRelays": [
    "wss://relay.nostr.band",
    "wss://search.nos.today",
    "wss://relay.ditto.pub"
  ],
  "profileRelays": [
    "wss://purplepag.es",
    "wss://relay.damus.io",
    "wss://relay.primal.net"
  ],
  "verifySignatures": true,
  "relayTimeoutSecs": 15,
  "relayBudgetSecs": 90
}
```

# Actor output Schema

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

All results in the default dataset.

# 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": [
        "bitcoin"
    ],
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("tali-works/nostr-keyword-hashtag-author-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": ["bitcoin"],
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("tali-works/nostr-keyword-hashtag-author-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": [
    "bitcoin"
  ],
  "maxItems": 50
}' |
apify call tali-works/nostr-keyword-hashtag-author-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tali-works/nostr-keyword-hashtag-author-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/b5J45e1fiLqMdQY3S/builds/r6UWmOD50nOkA6NS0/openapi.json
