Skip to content

feat: NIP-66 relay monitor integration#136

Merged
dergigi merged 18 commits intodergigi:masterfrom
alltheseas:feat/nip66
Mar 15, 2026
Merged

feat: NIP-66 relay monitor integration#136
dergigi merged 18 commits intodergigi:masterfrom
alltheseas:feat/nip66

Conversation

@alltheseas
Copy link
Copy Markdown
Contributor

@alltheseas alltheseas commented Mar 13, 2026

Note: This PR includes commits from #135 (its base). Only the 5 NIP-66 commits are new — review those after #135 is merged.

Why

Ants hardcodes 7 NIP-50 relay URLs. NIP-66 monitor data reveals 51 more — 8x the search surface, found at zero latency cost (sync cache lookup). Dead relays get filtered before probing, cutting HTTP requests in half.

What it does

Fetches kind 30166 events from monitor relays at connect time. Three uses:

  1. Filter dead relays before HTTP probing (safety valve skips if >80% would be removed)
  2. Fast-path NIP-50 detection — relays with NIP-50 in N tags skip HTTP probes
  3. Discover NIP-50 relays beyond the hardcoded 7

Benchmark (npm run bench:nip66)

Metric Before After
NIP-50 relays known 7 58 (+51 discovered)
HTTP probes 46 24 (-48%)
Dead relays probed 11 0
Search events 267 289 (+8%)

Files (NIP-66 only)

File Change
src/lib/constants.ts NIP-66 constants
src/lib/nip66.ts New — fetch, cache, classify relay liveness
src/lib/ndk.ts ensureNip66Data() on connect
src/lib/relays.ts filterDeadRelays + NIP-50 fast path
src/lib/search/relayManagement.ts Liveness indicator in relay status
bench/nip66.ts New — before/after benchmark
package.json tsx devDep, bench:nip66 script

Future

When NDK PR #387 lands, nip66MonitorRelays in the NDK constructor will add ~45% WebSocket connection speedup on top. See #137.

Test plan

  • npm run bench:nip66 and --search pass
  • Graceful degradation when monitors unreachable
  • Safety valve prevents over-filtering
  • Stale dead entries (>24h) degrade to 'unknown'

🤖 Generated with Claude Code

alltheseas and others added 18 commits February 18, 2026 04:42
Replace sequential for...of loop in searchByAnyTerms with
Promise.allSettled so that each OR term is searched concurrently.

Pre-resolve fallback relay set once before parallel execution to
eliminate the memoization race in ensureFallbackRelaySet. Individual
term failures are isolated and don't kill the batch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 3 serial for...of author resolution loops with Promise.all
so that multiple by: tokens are resolved concurrently. Each token
is individually wrapped in try/catch — decode failures return null
and are filtered out, matching the previous skip-and-warn behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract measureRelayPing and measureAllRelayPings into a new
src/lib/relayPing.ts module with injectable dependencies for
testability.

The key fix: each ping subscription now receives a relaySet scoped
to the single target relay (NDKRelaySet.fromRelayUrls([relayUrl])),
so the REQ is sent only to the relay being measured. Previously the
subscription went to all connected relays, making ping measurements
inaccurate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show first 50 results initially with a "Show more" button that loads
50 more at a time. Pagination resets only when the search query
changes, not when results mutate (e.g. NIP-05 verified reordering),
avoiding the infinite-reset loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The result deduplication in searchEvents used
arr.findIndex(x => x.id === e.id) which is O(n) per element,
yielding O(n^2) total. Replace with a Set<string> lookup for O(n).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NIP-51 specifies 'relay' tags for blocked relay lists (kind 10006)
and search relay lists (kind 10007). The code was only checking for
'r' tags, which is the NIP-65 convention for kind 10002.

Now checks for both 'relay' (per spec) and 'r' (for compatibility
with mis-tagged events in the wild). Without this fix, user-configured
blocked relays and search relays were silently ignored.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
filterNip50Relays was injecting fallback relays (primal, snort,
ditto) without verifying they actually support NIP-50. This caused
non-NIP-50 relays to receive search: filters and return extraneous
results.

Now runs checkNip50Support on each fallback candidate before adding
it to the supported list, matching the verification applied to the
primary relay set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Queries like 'language:en' were silently dropped because:
1. buildSearchQueryWithExtensions returned early on empty baseQuery
2. search.ts used 'cleanedQuery || undefined' which made '' falsy

Now buildSearchQueryWithExtensions produces extension-only strings
when extensions are present, and the caller always invokes it so
that extension-only queries reach the relay as valid search filters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds mentions:<user> syntax to find events that mention a specific user
via NIP-27 p-tags. Supports multiple mentions: tokens and combines with
by: and free-text search terms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Highlights matching search terms in note content with a blue background.
Extracts terms from the query (excluding filter tokens like by:, kind:,
mentions:) and wraps matches in <mark> elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace raw nevent identifiers with resolved display names for reply
parent authors. Uses e-tag[4] pubkey when available (NIP-10), falls back
to fetching the parent event with relay hint from e-tag[2]. Includes
module-level profile cache with TTL, in-flight deduplication, and
IntersectionObserver for lazy resolution of off-screen notes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract resolveProfileName to shared profileUtils so NoteHeader,
InlineNostrToken, and EventCard all use the same cached resolver.
When default relays return no profile, retry against profile-specific
relays (purplepag.es, search.nos.today, relay.nostr.band). Also fixes
double npub:npub1... prefix in fallback display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 6s timeout to parent event fetch to prevent hanging
- Try relay hint first, then fall back to default relays
- Use relaySets.profileSearch() instead of hardcoded RELAYS.PROFILE_SEARCH
  to respect user's blocked relay preferences (NIP-51 kind 10006)
- EventCard InlineAuthor now uses shared resolveProfileName for
  consistent caching and profile relay fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Configuration for NIP-66 relay liveness integration:
- 30-min cache/refresh interval (monitors publish hourly)
- 15s fetch timeout
- 80% safety threshold to prevent catastrophic filtering
- 24h max age for dead relay entries before degrading to unknown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New module that fetches kind 30166 events from relay monitors to
determine relay liveness, RTT, and NIP support. Key design decisions:

- Asymmetric staleness: alive entries trusted indefinitely (false
  positive = just a timeout), dead entries expire after 24h and
  degrade to 'unknown' (false negative = missed results)
- Safety valve: if filtering would remove >80% of relays, skip
  entirely to prevent catastrophic failure from bad monitor data
- Multiple sources: queries known monitor relays + user's connected
  NDK pool relays for broader coverage
- Persistent cache via localStorage with 30-min background refresh
- Concurrent call coalescing to prevent duplicate fetches

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire NIP-66 data into the relay selection pipeline:

- filterNip50Relays: pre-filter dead relays before probing, use
  NIP-66 N tags as fast path to skip HTTP NIP-11 probes
- getNip50SearchRelaySet: enrich candidates with NIP-66-discovered
  NIP-50 relays, remove dead debug loop
- getBroadRelaySet: filter dead relays from broad set
- getOutboxSearchCapableRelays: pre-filter author's write relays
  before expensive NIP-11 probing
- connect(): fire-and-forget ensureNip66Data() so cache is populated
  before first search
- Relay monitoring lifecycle: start/stop NIP-66 background refresh

All integration points use only cached data (synchronous lookups).
NIP-66 never blocks search — if cache is empty, all functions fall
through to existing behavior unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Display monitor data next to each relay in the UI:
- Green [45ms] when monitor reports alive with RTT
- Red [dead] when monitor reports dead
- Nothing when no monitor data available

Visual indicator only — no behavioral change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Self-contained benchmark script measuring NIP-66 relay liveness
filtering impact across four phases:

- Phase A: HTTP-only NIP-11 probing (3-endpoint, no NIP-66)
- Phase B: NIP-66 pre-filtering + NIP-50 fast path
- Phase C: actual NIP-50 search comparison (opt-in via --search)
- Phase D: WebSocket connection timing with/without filtering

Key findings from benchmark runs:
- 51 NIP-50 relays discovered via NIP-66 beyond 7 hardcoded
- 48% fewer HTTP probes (dead relay filtering + fast path)
- +8% search event recall from extra NIP-50 relay discovery
- 1 NIP-50 relay rescued per run (HTTP timeout, NIP-66 knows)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 13, 2026

@alltheseas is attempting to deploy a commit to the dergigi's projects Team on Vercel.

A member of the Team first needs to authorize it.

@alltheseas
Copy link
Copy Markdown
Contributor Author

cc @dskvr

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ants Ready Ready Preview, Comment Mar 15, 2026 11:19am

Request Review

Copy link
Copy Markdown
Owner

@dergigi dergigi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the NIP-66 specific additions — clean integration.

Highlights:

  • Asymmetric trust policy (include-by-default, only exclude when confident) is exactly right for relay filtering
  • Safety valve at 80% threshold prevents catastrophic filtering on bad monitor data
  • Dead entry TTL (24h → degrade to unknown) avoids permanent blacklisting
  • Fast-path NIP-50 detection skips unnecessary HTTP probes
  • Coalesced inflight fetches prevent duplicate requests
  • Going from 7 hardcoded NIP-50 relays to 58 discovered is a big win

Benchmark numbers are compelling: 48% fewer HTTP probes, zero dead relays probed, +8% search events.

Merge order: #135 first, then this one.

@dergigi dergigi merged commit e48a8f3 into dergigi:master Mar 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants