Problem
When querying events from relays, apps manually construct REQ filter objects. For kinds with rich tag structures (like NIP-66 kind 30166), this requires knowing which tag letters map to which concepts:
{
"kinds": [30166],
"#d": ["wss://relay.damus.io"],
"#n": ["clearnet"],
"#N": ["42"],
"#R": ["!auth"]
}
"I want clearnet relays supporting NIP-42 that don't require auth" becomes a hand-built JSON object where the dev must remember: network → #n, NIPs → #N, requirements → #R with ! prefix for negation.
This is exactly what nostr-watch PR #875 addresses — it builds WASM programs (NIP-A5) whose entire job is encoding filter construction logic for each NIP-66 kind.
Proposal
Generate typed filter constructors per kind:
export function buildKind30166Filter(params: {
relay?: string | string[];
network?: "clearnet" | "tor" | "i2p" | "loki";
supportedNips?: number[];
requires?: ("auth" | "payment" | "pow" | "ssl")[];
doesNotRequire?: ("auth" | "payment" | "pow" | "ssl")[];
software?: string;
geohash?: string;
}): NostrFilter {
const filter: NostrFilter = { kinds: [30166] };
if (params.relay) filter["#d"] = Array.isArray(params.relay) ? params.relay : [params.relay];
if (params.network) filter["#n"] = [params.network];
if (params.supportedNips) filter["#N"] = params.supportedNips.map(String);
// ... etc
return filter;
}
Challenges
- Queryable tags vary by relay: Not all relays index all tags. The schema describes what tags exist on an event, not which are queryable. Filter builders would generate filters that are structurally correct but may not work on all relays.
- Filter semantics: REQ filters are AND within a filter, OR across filters. Builders would handle single-filter construction; composition is the caller's responsibility.
- New concept: Unlike validators and builders (which map directly to schema constraints), filter construction requires understanding the tag →
#tag mapping convention, which is implicit in the Nostr protocol, not encoded in schemas.
Where the data comes from
Kind schemas define which tags exist and their value constraints. The tag name → #tagname filter key mapping is a protocol convention. The codegen planner already knows required and optional tags per kind — the same info drives which filter parameters to expose.
Effort: High | Impact: High
This is a stretch goal. The schema doesn't fully capture queryability, so generated filters are best-effort. But for well-known kinds like NIP-66, the tag-to-filter mapping is stable and well-defined. Consider scoping to kinds where the mapping is unambiguous.
Problem
When querying events from relays, apps manually construct REQ filter objects. For kinds with rich tag structures (like NIP-66 kind 30166), this requires knowing which tag letters map to which concepts:
{ "kinds": [30166], "#d": ["wss://relay.damus.io"], "#n": ["clearnet"], "#N": ["42"], "#R": ["!auth"] }"I want clearnet relays supporting NIP-42 that don't require auth" becomes a hand-built JSON object where the dev must remember: network →
#n, NIPs →#N, requirements →#Rwith!prefix for negation.This is exactly what nostr-watch PR #875 addresses — it builds WASM programs (NIP-A5) whose entire job is encoding filter construction logic for each NIP-66 kind.
Proposal
Generate typed filter constructors per kind:
Challenges
#tagmapping convention, which is implicit in the Nostr protocol, not encoded in schemas.Where the data comes from
Kind schemas define which tags exist and their value constraints. The tag name →
#tagnamefilter key mapping is a protocol convention. The codegen planner already knows required and optional tags per kind — the same info drives which filter parameters to expose.Effort: High | Impact: High
This is a stretch goal. The schema doesn't fully capture queryability, so generated filters are best-effort. But for well-known kinds like NIP-66, the tag-to-filter mapping is stable and well-defined. Consider scoping to kinds where the mapping is unambiguous.