# Nostr Public Notes Monitor - Verified Relay Watchlists

Export signature-verified public Nostr notes from two relays. Filter by author, hashtag or keyword, deduplicate IDs, retain provenance and optionally skip previously saved notes. No key needed.

Canonical product page: https://mayd-it.com/data-tools/actors/nostr-public-notes-monitor/
Public Actor: https://apify.com/maydit/nostr-public-notes-monitor

## Example input

```json
{
  "relays": [
    "wss://relay.damus.io",
    "wss://nos.lol"
  ],
  "perRelayLimit": 10,
  "maxResults": 10,
  "hashtags": [
    "nostr"
  ]
}
```

## Full guide

![Collect signed public notes — illustrative workflow](https://api.apify.com/v2/key-value-stores/YGxD9JRpduom8Q5Zd/records/nostr-public-notes-monitor-886b2c8e2ca6.png?signature=yoRqHZS45V1ElifcTx8o)

[Open this Actor in Console](https://console.apify.com/actors/gc4K7YPDwmEStApSI/input) · [Try a small run](#try-a-small-run) · [Cost](#cost)

**Mayd-It · Verified public notes**

Collect a bounded watchlist of public Nostr notes with signed text and relay provenance. Filter explicit author keys or hashtag tags, deduplicate observations across two relays, and optionally skip notes already saved.

**Source scope:** Public kind 1 notes from relay.damus.io and nos.lol. Signatures are verified; verification establishes the signing key, not a person’s identity or a post’s truth.

## What you get

- Distinct note IDs with original signed content and verified signatures.
- Author, tag, time and literal keyword filters with observed relay provenance.
- Saved-history deduplication for recurring collection without a WebSocket client.

## Try a small run

Copy this exact input into the Actor’s JSON input editor, then run it.

```json
{
  "relays": [
    "wss://relay.damus.io",
    "wss://nos.lol"
  ],
  "perRelayLimit": 10,
  "maxResults": 20,
  "hashtags": [
    "nostr"
  ]
}
```

This example requests a ten-event window per relay and saves at most 20 distinct matching notes tagged `nostr`. No account or API key is required. `{}` uses the same tag and a seven-day lookback; set your own filters for a watchlist, or explicitly pass `"hashtags": []` for an unfiltered recent window. Open the dataset for verified rows and `SUMMARY` for accepted, rejected and incomplete-source counts.


## Understand your run

Open **Run report** in the run’s Output tab for saved-record counts, reported source issues and limits, and suggested next steps. Use **CSV export** to take the saved results into a spreadsheet. The existing JSON results and detailed machine-readable summary remain available.

The report reflects this run’s recorded coverage. A successful run or a saved result does not establish that every possible source record was collected.

## Cost

**FREE / BRONZE: $0.002 per saved observation ($2 per 1,000).** SILVER: $1.60 per 1,000. GOLD / PLATINUM / DIAMOND: $1.20 per 1,000.

The Actor Start event is **$0.00005 per charged start unit**, with one unit per GB of Actor memory and a minimum of one. The Pricing tab shows the rate for your Apify tier. New, changed and ordinarily saved unchanged observations use the same result rate. Skipped unchanged rows, rejected events, duplicates and error diagnostics are not paid result rows. Use `onlyChanges` for repeated monitoring after establishing history.

## A real result

Selected fields from a recorded run on **10 October 2026**. This is an example of the output shape; source content and counters can change.

```json
{
  "id": "19befb884f45b68a482cde7cc5a47c4049fba50e0ff97001fa303c9bc6222d60",
  "kind": 1,
  "createdAt": "2026-10-10T23:14:02.000Z",
  "hashtags": [
    "bitcoin",
    "moscowtime",
    "nostr"
  ],
  "sourceRelays": [
    "wss://nos.lol"
  ],
  "signatureVerified": true
}
```

This retained sample came from the one-relay community example below. The complete row also includes signed content, its author public key and signature. Relay provenance records where the Actor observed the note.

## More examples

### Public Nostr Bitcoin Hashtag Notes

Request notes with the exact, case-sensitive `bitcoin` tag.

```json
{
  "hashtags": [
    "bitcoin"
  ],
  "perRelayLimit": 10,
  "maxResults": 20
}
```

### Nostr Community Tag on nos.lol

Use just nos.lol for the exact `nostr` tag, with at most ten saved notes.

```json
{
  "relays": [
    "wss://nos.lol"
  ],
  "hashtags": [
    "nostr"
  ],
  "perRelayLimit": 10,
  "maxResults": 10
}
```

---

This is an experimental market with limited observed Apify demand; no audience-growth or revenue promise is implied. Every event ID is recomputed and its Schnorr signature is checked before a paid row is saved.

## Input and coverage

| Input | Behavior |
| --- | --- |
| `relays` | One or both of `wss://relay.damus.io` and `wss://nos.lol`; both by default. Other hosts, credentials, paths and custom ports are rejected. |
| `authors` | Up to 20 exact hexadecimal public keys. No username or `npub` resolution. Multiple authors use OR. |
| `hashtags` | Up to 10 exact, case-sensitive `t` tag values, with an optional leading `#`. Multiple tags use OR. With no author, hashtag or keyword at all, `nostr` is sampled by default. |
| `keywords` | Up to 10 case-insensitive substrings matched with OR **within the retrieved window**. This is not full-text search across a relay. |
| `since`, `until` | Inclusive ISO8601 timestamps with timezone. Omitted end freezes at run start; omitted start is `lookbackHours` earlier. Maximum span 366 days. |
| `lookbackHours` | Default 168 (seven days), used when `since` is omitted. |
| `perRelayLimit` | Default 10, maximum 100 stored events per relay. Invalid, duplicate or filtered events can reduce saved results. |
| `maxResults` | Default 20 confirmed saved notes, maximum 1,000. Two supported relays can provide at most 200 candidates per run. |
| `relayTimeoutSeconds` | Default 12 seconds per relay, including DNS and connection; maximum 30. |
| `comparePrevious`, `onlyChanges`, `snapshotName` | Optional confirmed-output history described below. |
| `maxRunSeconds` | Default 240 seconds, 30–3,600 allowed, further bounded by the platform timeout minus 25 seconds. |

Author, hashtag, keyword and time groups combine with AND. All filters are checked locally even when sent to a relay. Notes are sorted newest first, then by event ID for ties. Public text can include spam, empty text, links and arbitrary content. Media and linked pages are not fetched, and keyword matching does not infer sentiment or identity.

The Actor waits for **EOSE**, the relay's acknowledgement that its stored-event response has ended. Missing EOSE, refusals and source errors are recorded as partial or failed coverage. Unsupported, oversized, malformed or unverifiable events are always excluded from output and billing. A completed response with usable verified notes may succeed despite these exclusions, with a warning and categorized rejection counts. If every received event from a source is rejected, that source is marked unusable. Rejected content is never retained; diagnostics contain only the event ID when validly shaped and a reason code.

EOSE completes only the requested bounded response, including processing and rejecting unsupported events. It does not establish all matching history, all public Nostr activity, acceptance of every received event, or the absence of deleted notes. The Actor never publishes events, signs in, authenticates to paid relays, reads private messages, or enriches profiles. Public DNS is checked and pinned for TLS connections; redirects are not followed.

A temporary HTTP 502, 503 or 504 handshake response gets at most one retry after a 500 ms pause, within the original relay time allowance. The attempt outcomes remain in `SUMMARY.sourceCoverage`. Authentication, access restrictions, redirects, subscription refusals and protocol errors are not retried.

Each message is capped at 128 KiB, each relay response at 4 MiB, and each content field at 64,000 UTF-8 bytes. Up to 500 tags are accepted per event. Oversized or unverifiable notes are not billed. These limits are deliberately smaller than some relay limits and are reported in `SUMMARY` if encountered.

## Results

One dataset row represents one distinct verified note in this run:

| Field | Meaning |
| --- | --- |
| `id`, `pubkey`, `kind` | Signed event ID, author public key and kind 1. |
| `createdAt`, `createdAtUnix` | Publisher's signed timestamp in UTC and Unix seconds; separate from retrieval time. |
| `content`, `tags`, `hashtags` | Original signed text and ordered tags; extracted `t` tag values. |
| `signature`, `signatureVerified` | Source signature and successful verification flag. |
| `sourceRelays` | Relays that returned this verified ID during this run. Absence from this list is not proof that another relay lacks it. |
| `rootEventId`, `replyToEventId` | Unambiguous explicit NIP-10 `root`/`reply` markers, otherwise null. Legacy unmarked references are preserved without guessing. |
| `referencedEventIds`, `threadReferenceMode` | Event references and how they were marked. Referenced posts are not fetched. |
| `links` | At most 100 deduplicated HTTP(S) text links; not visited or safety-certified. |
| `nostrUri`, `url` | Standard `nostr:note...` URI and a generated public njump.me viewer link. njump.me is not the fetched data source. |
| `observedAt`, `contentHash` | Retrieval observation time and SHA256 of the stable signed snapshot. |
| `comparisonStatus`, `changedFields`, `previous`, `previousObservedAt` | Optional comparison evidence; `not_requested` without history, otherwise `new`, `changed` or `unchanged`. |

`SUMMARY` includes `rowsSaved`, `rowsObserved` (distinct matching candidates considered for output), `verifiedMatchingNotes`, `unchangedSkipped`, `sourcesSucceeded`, `sourceCoverage`, `invalidEvents`, `rejectionsByReason`, `filtered`, `failedSources`, `windowComplete`, limits and status. `invalidEvents` counts every validation exclusion, including validly signed events exceeding supported size limits; it is not a count of forged signatures. Each source includes reason counts and at most 20 `{id, reason}` rejection samples. `windowComplete` describes transport and verification processing, not acceptance of all received events. Source counters are not audience or engagement metrics. A healthy empty query or unchanged repeat warns and succeeds. A zero-output operational failure fails the run. A zero-row spending-limit stop is a normal budget outcome.

## Saved history

Set `comparePrevious: true` or `onlyChanges: true`, and choose a `snapshotName`. The **first comparison run warns and returns matching notes as new**. History is stored in the named `nostr-public-notes-monitor-history` key-value store and separated by relay, filter and time-window configuration.

Only confirmed saved rows advance their own history. Positive observations from a partial relay response may be saved and remembered, with the run explicitly marked partial; this never establishes completeness or deletions. Unchanged repeats are skipped with `onlyChanges`. Since signed kind 1 event content is immutable, the same valid ID normally remains unchanged. Relay provenance changes do not create false content changes. No automatic deletions are emitted.

For a repeatable historical comparison, use the same fixed `since`/`until`, filters and `perRelayLimit`, and make `maxResults` large enough for the whole bounded window. Otherwise later runs may find previously unsaved notes. Relay retention and late-arriving events can also change the observed window.

Use **one writer per exact history scope**. The Actor checks the stored value again before replacing it and refuses an observed conflicting change; the key-value store offers no atomic compare-and-swap, so this is not a distributed lock. Malformed history is preserved with a diagnostic; use a new `snapshotName` to begin a fresh scope. At 20,000 remembered IDs, choose a new name or narrower watchlist. Named storage and data retention are under your Apify account's control.

## Troubleshooting and support

| If you see… | Check this |
| --- | --- |
| Fewer notes are saved than requested | Limits bound candidates, not guaranteed matches. Duplicates, filters, unsupported sizes and failed verification can reduce output; inspect categorized rejections in `SUMMARY`. |
| The relay did not send EOSE | The stored-event response did not complete. Treat coverage as partial even when usable verified notes were saved. |
| A repeat still returns new IDs | Use identical fixed `since`/`until`, filters and per-relay limits, with room to save the full bounded window. Late arrivals and relay retention can still change that window. |

For help, open an Actor issue or contact **bmay@mayd-it.com**. Include the run ID, selected sources and the relevant `SUMMARY` message. Do not send credentials. This Actor is independent of the platform.

Content remains subject to its creators’ rights and applicable law. Use public data for a legitimate purpose, honor deletion requests and avoid attempting to identify anonymous authors. Storage charges follow your Apify plan.

Protocol references: [NIP-01 events and subscriptions](https://github.com/nostr-protocol/nips/blob/master/01.md), [NIP-10 marked reply references](https://github.com/nostr-protocol/nips/blob/master/10.md), [NIP-11 relay information](https://github.com/nostr-protocol/nips/blob/master/11.md), [nostr-tools](https://github.com/nbd-wtf/nostr-tools). Relay operation, retention and access policies may change; requests that require authentication or are refused are not bypassed.
