Relays don't do browsing. You hand them a wish list, and they fetch exactly that!
The world’s most patient librarian
Picture a library with no shelves you can walk along. There’s just a counter and a librarian:
the relayRelay: A server that accepts events from clients, stores them and sends them to anyone who subscribes with a matching filter.. You can’t wander around. Instead you fill in a
request slip and hand it over: “notes by Alice, please” or “anything tagged #nostr since
yesterday”. The librarian goes into the back, grabs every matching item, and brings it to the
counter, newest first.
That slip is a filterFilter: A JSON object describing which events you want: by ids, authors, kinds, tags (#e, #p…), since, until and limit.. It’s the only way a
clientClient: The app you use (web, mobile or desktop) to create, sign, publish and read Nostr events. ever asks a relay for anything. Your timeline, your
notifications, a profile page, a reply thread: under the hood, every screen of every Nostr app is
just a handful of filters.
A filter is a tiny JSON object. Each key is one question about an eventEvent: The only data type in Nostr: a signed JSON object with an id, pubkey, created_at, kind, tags, content and sig.:
authors: who signed it? (a list of public keysPublic key: Your public identity: a 32-byte secp256k1 key, shown as 64 hex characters or as an npub. Safe to share.)
kinds: what sort of thing is it? (1 = note, 7 = reaction, 0 = profile…)
ids: is it one of these exact events? (by event idEvent id: The SHA-256 hash of an event's serialized fields, written as 64 lowercase hex characters. It names exactly one event.)
#e, #p, #t: does it carry a tagTag: An array of strings attached to an event, such as ["p", <pubkey>] to mention someone or ["e", <event id>] to reply to a note. pointing at this event, mentioning this person, or using this hashtag?
since, until: was it created inside this time window?
limit: how many of the newest ones do I want right now?
Build one yourself
Below is our sample relay’s whole shelf: 60 real, signed events from Alice, Bob, Carol and friends.
Every card starts lit, because an empty filter matches everything. Start adding conditions and
watch the cards that don’t fit fade away. Click any card to ask “why is this one in (or out)?”
Filter builder
Flip the switches and watch the relay's shelf: matching events light up, everything else fades.
An empty filter: give me everything you have! 60 of 60 events match
The filter, as JSON
{}
…wrapped in a REQ message
json
["REQ","ch05",{}]
60 of 60 events match
Pick any card to see which conditions it passes.
▸⚙Under the hood: the REQ on the wireShow details
The JSON in the builder goes over the WebSocketWebSocket: A long-lived two-way connection between browser and server. Clients and relays exchange Nostr messages over it as JSON arrays. wrapped in a
REQREQ: The client message that asks a relay for events: ["REQ", <subscription id>, <filter>, …]. message, together with a subscription id you pick:
The relay answers with one ["EVENT", "alice-notes", {…}] per stored match, then
["EOSE", "alice-notes"] (end of stored eventsEOSE: "End of stored events": the relay's signal that it has sent everything it had saved for a subscription; anything after is live.). The subscriptionSubscription: A standing query on a relay, opened with REQ and a subscription id: stored matches arrive first, then new events in real time. stays open: new matching events
keep arriving until you send ["CLOSE", "alice-notes"]. With nostr-tools:
import { SimplePool } from "nostr-tools/pool";import { matchFilter } from "nostr-tools/filter";const pool = new SimplePool();const sub = pool.subscribeMany( ["wss://relay.damus.io", "wss://nos.lol"], { authors: [alicePubkey], kinds: [1], limit: 20 }, { onevent: (event) => console.log(event.content), oneose: () => console.log("stored events done, now streaming live"), },);// later: sub.close();matchFilter({ kinds: [1], "#t": ["nostr"] }, someEvent); // the same check, locally
The rules for combining conditions are short enough to fit on a sticky note:
Inside one field, it’s OR."authors": [alice, bob] means by Alice or by Bob.
"kinds": [1, 7] means a note or a reaction.
Across fields, it’s AND.{"authors": [alice], "kinds": [1]} means by Alice and a note.
Every condition you add can only make the result smaller. One failing condition is enough to
leave an event out (that’s what the ✓ and ✗ chips in the “why?” panel show).
Several filters in one REQ are OR-ed. Want Alice’s notes plus everyone’s reactions to them?
Send two slips in the same envelope: ["REQ", "sub", {filter A}, {filter B}].
★
Missing field = don't care
Leaving a field out means “any value is fine”. That’s why {} returns the whole shelf, and why
you never send an empty list like "kinds": []: a list with nothing in it can never match.
Tags: following the threads
Events point at each other through tags, and filters can follow those pointers backwards. The
NIP-01NIP-01: The core spec: event structure, ids and signatures, kind ranges, filters, and the client–relay messages. trick is the # prefix: "#x": [...] matches events that
have a tag named x whose first value is in the list.
#e: everything that points at an event. Replies, reactionsReaction: A kind 7 event reacting to another event: "+" means like, "-" dislike, or any emoji.,
reposts and zaps all carry an e tag, so {"#e": [threadId]} gathers a whole conversation.
#p: everything that mentions a person. That’s your notifications tab in one line.
#t: hashtags. Clients put #nostr in the text and a ["t", "nostr"] tag, because relays
only index tags, never the content.
Try the “Alice’s thread” recipe in the builder, then add kind 7: you’ve just built a like counter.
Time windows and limits
since and until are Unix timestamps (seconds since 1 January 1970), just like an event’s created_at. An event matches
when since <= created_at <= until, both ends included. Apps use them for “load older posts” buttons
(until = the oldest thing on screen) and for catching up (since = when you last looked).
limit is different: it isn’t a condition, it’s a request about how many. The relay sorts
matches newest first and sends at most that many in the first batch. In the builder, matches that
didn’t make the cut get a dashed border and a ✂.
ids, authors, #e and #p must contain exact 64-character lowercase hex values. (Old
relays accepted prefixes; that was removed, so don’t rely on it.) The builder converts npub1…,
note1…, nprofile1… and nevent1… to hex for you, because NIP-19 strings never go on the wire.
Only single-letter tags (a–z, A–Z) are indexed, and only by their first value.
#d finds addressable events by identifier, #a follows addresses, #k finds reactions by the
kind of thing they react to.
limit only applies to the initial query and is ignored for events that arrive after EOSE.
Relays SHOULD return the newest limit events, with ties on created_at broken by lowest id,
and may return fewer. Every relay applies it independently, so asking three relays for
"limit": 10 can bring back up to 30 events (minus duplicates, which your client merges by id).
Relays may cap what they return, refuse expensive filters, or answer with
["CLOSED", subId, "reason"].
Related extensions: NIP-45 adds
["COUNT", subId, filter] to ask how many without downloading anything, and
NIP-50 adds a search field for
full-text search on relays that support it.
Filter quests
Ready to put it together? Each quest describes a set of events in plain words. Build any filter
whose results are exactly that set. There’s usually more than one right answer!
Filter builder
Flip the switches and watch the relay's shelf: matching events light up, everything else fades.
Build a filter whose results are exactly what each quest asks for. Any filter that gets there counts!
☆Applause meterFind every reaction (kind 7) to Alice's relay thread, and nothing else.
☆Hot take huntFind Bob's short notes tagged #nostr.
☆Fresh facesGet only the 3 newest profiles (kind 0).
☆New Year's EveEverything Alice signed from 31 Dec 2024, 00:00 UTC onward.
0 of 4 quests solved
Who signed the event (hex pubkey or npub1…).
What sort of event: 1 = note, 7 = reaction, 0 = profile…
Hashtags (t tags), lowercase, without the #.
Events with a p tag mentioning this person.
Events with an e tag pointing at this event: replies, reactions, reposts.
Exact event ids (hex or note1…). Tip: pick a card and pin its id.
An empty filter: give me everything you have! 60 of 60 events match
The filter, as JSON
{}
…wrapped in a REQ message
json
["REQ","ch05",{}]
60 of 60 events match
Pick any card to see which conditions it passes.
i
Take it further
The filter playground
has the same builder plus a JSON editor, and can send your filter as a real REQ. Switch on live
mode in the header and it asks real public relays (read-only; we never publish anything).
Quick check
Choose one answer
Choose one answer
Choose one answer
Takeaways
A filter is a small JSON object that describes the events you want; you send it to a relay inside a REQ message.
Inside one field, values are OR-ed (any of these authors). Across fields, conditions are AND-ed (this author AND this kind).
Several filters in the same REQ are OR-ed: the relay sends events matching any of them.
Tag filters like #e, #p and #t follow threads, mentions and hashtags; since, until and limit slice by time.
limit only shapes the first batch, newest first; after EOSE the subscription stays open for new events.