Filters
A Filter describes a set of events without prescribing which relay should answer the query. Keeping those concerns
separate lets the same filter target the whole pool, one relay, or a manually selected group.
Filter composition follows two rules:
- constraints inside one filter are combined with AND;
- multiple filters in one request are combined with OR.
For example, one filter containing an author and a text-note kind asks for text notes by that author. Two separate filters, one for the author and one for the kind, also match every event by that author and every text note by anyone.
A filter can constrain IDs, authors, kinds, timestamps, tags, and the number of stored results requested from each relay.
Construct a bounded filter
The example limits the author, kind, lower time boundary, and number of stored results requested from each relay:
use nostr::prelude::*;
fn filter_example() {
let public_key = Keys::generate().public_key();
let filter = Filter::new()
.author(public_key)
.kind(Kind::TextNote)
.since(Timestamp::now())
.limit(20);
println!("Filter: {}", filter.as_json());
}
from nostr_sdk import Event, EventBuilder, Filter, Kind, KindStandard, Keys, PublicKey, Timestamp
def filter_example() -> None:
public_key = Keys.generate().public_key()
filter = (
Filter()
.author(public_key)
.kind(Kind.from_std(KindStandard.TEXT_NOTE))
.since(Timestamp.now())
.limit(20)
)
print(f"Filter: {filter.as_json()}")
import {
Event,
EventBuilder,
Filter,
Kind,
KindStandard,
Keys,
PublicKey,
Timestamp,
} from "@nostrdevkit/nostr-sdk-node";
function filterExample() {
const publicKey = Keys.generate().publicKey();
const filter = new Filter()
.author(publicKey)
.kind(Kind.fromStd(KindStandard.TextNote))
.since(Timestamp.now())
.limit(20n);
console.log("Filter:", filter.asJson());
}
import {
Event,
EventBuilder,
Filter,
Kind,
KindStandard,
Keys,
PublicKey,
Timestamp,
uniffiInitAsync,
} from "@nostrdevkit/nostr-sdk-web";
await uniffiInitAsync();
function filterExample() {
const publicKey = Keys.generate().publicKey();
const filter = new Filter()
.author(publicKey)
.kind(Kind.fromStd(KindStandard.TextNote))
.since(Timestamp.now())
.limit(20n);
console.log("Filter:", filter.asJson());
}
import {
Event,
EventBuilder,
Filter,
Kind,
KindStandard,
Keys,
PublicKey,
Timestamp,
} from "@nostrdevkit/nostr-sdk-react-native";
export function filterExample() {
const publicKey = Keys.generate().publicKey();
const filter = new Filter()
.author(publicKey)
.kind(Kind.fromStd(KindStandard.TextNote))
.since(Timestamp.now())
.limit(20n);
console.log("Filter:", filter.asJson());
}
import org.nostrdevkit.sdk.*
fun filterExample() {
val publicKey = Keys.generate().publicKey()
val filter = Filter()
.author(publicKey)
.kind(Kind.fromStd(KindStandard.TEXT_NOTE))
.since(Timestamp.now())
.limit(20u)
println("Filter: ${filter.asJson()}")
}
import NostrSDK
func filterExample() throws {
let publicKey = Keys.generate().publicKey()
let filter = Filter()
.author(author: publicKey)
.kind(kind: Kind.fromStd(e: .textNote))
.since(timestamp: Timestamp.now())
.limit(limit: 20)
print("Filter: \(try filter.asJson())")
}
using Nostr.Sdk;
public static class CoreExample
{
public static void FilterExample()
{
var publicKey = Keys.Generate().PublicKey();
var filter = new Filter()
.Author(publicKey)
.Kind(Kind.FromStd(KindStandard.TextNote))
.Since(Timestamp.Now())
.Limit(20);
Console.WriteLine($"Filter: {filter.AsJson()}");
}
}
This asks each relay for at most 20 text notes by the selected key at or after the timestamp. All four constraints are in one filter, so they are combined with AND.
Compose AND and OR deliberately
Suppose an application needs text notes from Alice or Bob. One filter with both authors expresses that request:
authors = [Alice, Bob] AND kinds = [TextNote]
Suppose it instead needs Alice’s text notes plus Bob’s metadata. That requires two filters:
(authors = [Alice] AND kinds = [TextNote])
OR
(authors = [Bob] AND kinds = [Metadata])
Each additional filter is another OR branch and can expand the response.
Tag filters follow the same rule. Multiple accepted values for one tag name are alternatives within that constraint;
constraints for different tag names must all match. Use the typed single-letter tag APIs where available instead of
assembling # field names as strings.
Time windows
since and until are inclusive. Use them to bound a request to the history relevant to the application.
When maintaining local history, reconcile a bounded filter with Negentropy sync instead of walking through relay history page by page. Sync compares event IDs and transfers only the missing events.
For a live subscription, since also prevents an unbounded initial history. limit bounds stored results from each
relay but does not replace a time window.
Understand pool-wide results
The limit is applied by each relay, not globally across the pool. Results can overlap or arrive out of order; the SDK validates and deduplicates them by event ID. Relay selection remains separate and is added by the request target.
Filters become network work only when paired with a client and request target.