Skip to content

Filter builders: type-safe REQ filter constructors per kind #12

Description

@alltheseas

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions