Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sandwichfarm/nostr-watch/llms.txt

Use this file to discover all available pages before exploring further.

NIP-66 defines a relay monitoring system for Nostr. Monitors periodically check relays for health, capability, and availability, then publish their findings as signed Nostr events. You can subscribe to these events directly from the relays that carry them, verify the cryptographic signatures, and aggregate the data yourself — no intermediary, no API keys, and no trust required beyond the monitor signatures you inspect.

How it works

  1. Monitors run health checks against Nostr relays: WebSocket open/read/write, DNS, SSL, geolocation, and NIP-11 info document fetching
  2. Each monitor publishes results as kind 30166 events — one per relay, updated each check cycle (typically every hour)
  3. Monitors announce themselves with kind 10166 events describing their configuration, frequency, and capabilities
  4. Status changes are logged as kind 1066 delta events for historical tracking and uptime analytics

Default NIP-66 relays

These relays carry NIP-66 monitoring data. Connect to one or more of them when building REQ filters.
RelayDescription
wss://relay.nostr.watchPrimary nostr-watch relay
wss://relaypag.esRelaypages NIP-66 relay
wss://monitorlizard.nostr1.comMonitor Lizard relay
Connect to multiple relays for redundancy. The same monitoring data is available across all three — if one is offline, the others still serve the events.

Event kinds

Kind 30166 — relay status

The primary event kind. One addressable event per relay per monitor, updated on each check cycle. The d tag contains the relay WebSocket URL.
{
  "kind": 30166,
  "pubkey": "<monitor-pubkey>",
  "content": "{\"supported_nips\":[1,11,42],\"software\":\"strfry\",\"version\":\"1.0.0\"}",
  "tags": [
    ["d", "wss://relay.damus.io"],
    ["r", "wss://relay.damus.io"],
    ["n", "clearnet"],
    ["R", "!auth"],
    ["R", "!payment"],
    ["rtt-open", "120"],
    ["rtt-read", "85"],
    ["rtt-write", "95"],
    ["N", "1"],
    ["N", "11"],
    ["N", "42"]
  ]
}
The content field contains the relay’s NIP-11 info document when available. Tags encode structured data:
TagDescription
dRelay WebSocket URL (the event’s unique identifier)
rRelay WebSocket URL (for filtering)
nNetwork type: clearnet, tor, i2p, hybrid
RRequirements: !auth, !payment, or open
rtt-openRound-trip time for WebSocket open (ms)
rtt-readRound-trip time for read check (ms)
rtt-writeRound-trip time for write check (ms)
NSupported NIP number
sSoftware family (e.g. strfry, nostr-rs-relay)
gGeohash for the relay’s location

Kind 10166 — monitor announcement

Published by each monitor to announce its configuration. Use these events to discover which monitors are active and evaluate their coverage.
// Get all monitor announcements
const monitors = await pool.querySync(relays, {
  kinds: [10166]
})

for (const event of monitors) {
  const frequency = event.tags.find(t => t[0] === 'frequency')?.[1]
  const networks = event.tags.filter(t => t[0] === 'n').map(t => t[1])
  const checks = event.tags.filter(t => t[0] === 'c').map(t => t[1])
  console.log(`Monitor ${event.pubkey.slice(0, 16)}: every ${frequency}s, networks: ${networks}`)
}

Kind 1066 — delta / history

Published when a monitor detects a state change for a relay. Use these events to track uptime over time without storing every full kind 30166 event.

Subscribing to NIP-66 events

import { SimplePool, verifyEvent } from 'nostr-tools'

const pool = new SimplePool()
const relays = ['wss://relay.nostr.watch', 'wss://relaypag.es']

// Get all relay statuses
const statuses = await pool.querySync(relays, {
  kinds: [30166]
})

// Get status for a specific relay
const damusStatus = await pool.querySync(relays, {
  kinds: [30166],
  '#d': ['wss://relay.damus.io']
})

// Get clearnet relays running strfry
const strfryRelays = await pool.querySync(relays, {
  kinds: [30166],
  '#n': ['clearnet'],
  '#s': ['strfry']
})

// Get all monitor announcements
const monitors = await pool.querySync(relays, {
  kinds: [10166]
})

Verifying signatures

Always call verifyEvent() before trusting any NIP-66 data. Signature verification is what makes raw NIP-66 access trustless — you can prove that a specific monitor pubkey signed a specific observation.
import { verifyEvent } from 'nostr-tools'

for (const event of events) {
  if (!verifyEvent(event)) {
    console.warn(`Invalid signature from ${event.pubkey}`)
    continue
  }
  // Event is cryptographically verified — safe to use
}

Aggregating across monitors

Multiple monitors report on the same relay. To get a reliable picture, aggregate their observations by grouping events per monitor and computing consensus values:
import { SimplePool, verifyEvent } from 'nostr-tools'

const pool = new SimplePool()
const relays = ['wss://relay.nostr.watch', 'wss://relaypag.es']

// Get all observations for a relay
const events = await pool.querySync(relays, {
  kinds: [30166],
  '#d': ['wss://relay.damus.io']
})

// Verify and deduplicate by monitor (keep most recent per monitor)
const verified = events.filter(e => verifyEvent(e))
const byMonitor = new Map()
for (const event of verified) {
  const existing = byMonitor.get(event.pubkey)
  if (!existing || event.created_at > existing.created_at) {
    byMonitor.set(event.pubkey, event)
  }
}

// Aggregate RTT values (median across monitors)
function getTagValue(event, tagName) {
  const tag = event.tags.find(t => t[0] === tagName)
  return tag ? Number(tag[1]) : null
}

const rttValues = [...byMonitor.values()]
  .map(e => getTagValue(e, 'rtt-open'))
  .filter(v => v !== null)
  .sort((a, b) => a - b)

const medianRtt = rttValues[Math.floor(rttValues.length / 2)]
console.log(`Median open RTT: ${medianRtt}ms from ${rttValues.length} monitors`)

Subscribing to live updates

Use Nostr subscriptions to receive updates in real time as monitors publish new observations:
const sub = pool.subscribeMany(relays, [
  { kinds: [30166], '#d': ['wss://relay.damus.io'] }
], {
  onevent(event) {
    if (verifyEvent(event)) {
      console.log(`Update from monitor ${event.pubkey.slice(0, 8)}:`, event)
    }
  }
})

// Later: close the subscription
sub.close()

Common use cases

// Find relays supporting NIP-42 (auth) on clearnet
const events = await pool.querySync(nip66Relays, {
  kinds: [30166],
  '#N': ['42'],
  '#n': ['clearnet']
})

const verified = events.filter(e => verifyEvent(e))

// Deduplicate by relay URL, keep most recent across all monitors
const relayMap = new Map()
for (const event of verified) {
  const relayUrl = event.tags.find(t => t[0] === 'd')?.[1]
  if (!relayUrl) continue
  const existing = relayMap.get(relayUrl)
  if (!existing || event.created_at > existing.created_at) {
    relayMap.set(relayUrl, event)
  }
}

// Further filter for relays also supporting NIP-50
const results = [...relayMap.values()].filter(event => {
  const nips = event.tags.filter(t => t[0] === 'N').map(t => t[1])
  return nips.includes('50')
})

console.log(`Found ${results.length} relays supporting NIP-42 + NIP-50`)
// Get clearnet relays with no auth or payment requirements
const events = await pool.querySync(nip66Relays, {
  kinds: [30166],
  '#n': ['clearnet'],
  '#R': ['!auth', '!payment']
})

const verified = events.filter(e => verifyEvent(e))

// Aggregate RTT per relay
const relayRtt = new Map()
for (const event of verified) {
  const url = event.tags.find(t => t[0] === 'd')?.[1]
  const rttOpen = event.tags.find(t => t[0] === 'rtt-open')?.[1]
  if (!url || !rttOpen) continue
  if (!relayRtt.has(url)) relayRtt.set(url, [])
  relayRtt.get(url).push(Number(rttOpen))
}

// Rank by median latency, require at least 2 monitor observations
const ranked = [...relayRtt.entries()]
  .map(([url, rtts]) => {
    rtts.sort((a, b) => a - b)
    return { url, medianRtt: rtts[Math.floor(rtts.length / 2)], monitors: rtts.length }
  })
  .filter(r => r.monitors >= 2)
  .sort((a, b) => a.medianRtt - b.medianRtt)

for (const relay of ranked.slice(0, 5)) {
  console.log(`${relay.url}${relay.medianRtt}ms (${relay.monitors} monitors)`)
}
const now = Math.floor(Date.now() / 1000)
const oneWeekAgo = now - 7 * 86400

// Get delta events for a relay over the past week
const deltas = await pool.querySync(nip66Relays, {
  kinds: [1066],
  '#r': ['wss://relay.damus.io'],
  since: oneWeekAgo
})

const verified = deltas.filter(e => verifyEvent(e))
  .sort((a, b) => a.created_at - b.created_at)

for (const event of verified) {
  const rTags = event.tags.filter(t => t[0] === 'R')
  const isOpen = rTags.some(t => t[1] === 'open')
  console.log({
    time: new Date(event.created_at * 1000).toISOString(),
    monitor: event.pubkey.slice(0, 8),
    open: isOpen,
  })
}

Pagination

NIP-66 relays may store thousands of events. Use limit in your REQ filters and paginate with until:
// First page
const page1 = await pool.querySync(relays, {
  kinds: [30166],
  limit: 100
})

// Next page (older events)
const oldestTimestamp = Math.min(...page1.map(e => e.created_at))
const page2 = await pool.querySync(relays, {
  kinds: [30166],
  limit: 100,
  until: oldestTimestamp
})

When to use raw NIP-66

Raw NIP-66 access is the right choice when:
  • Your app already speaks Nostr — if you are using nostr-tools, NDK, or a similar library, subscribing to kind 30166 events requires minimal additional code
  • Privacy is a priority — direct relay connections expose no IP to an intermediary API server
  • You need trustless verification — cryptographic signature verification proves that a named monitor produced a specific observation at a specific time
  • You are building a relay selection algorithm — raw observation data gives you full control over aggregation logic, outlier detection, and quorum thresholds
  • You need the freshest data possible — receive updates as monitors publish them rather than waiting for an aggregation layer
If you want structured relay intelligence without building your own aggregation pipeline, consider the REST API or CVM instead. They both derive from the same raw NIP-66 events.

Tips

  • Use multiple relays for redundancy — if one relay is down, others still serve the data
  • Cache locally — NIP-66 data updates hourly, so re-fetching constantly wastes bandwidth
  • Always verify — call verifyEvent() on every event before using the data
  • Filter server-side — use tag filters in REQ (#n, #N, #s, #d, etc.) rather than fetching everything and filtering client-side
  • Check created_at — discard events older than the monitor’s declared frequency — they may be stale

Next steps

NIP-66 event kinds

Deep dive into kind 10166, 30166, and 1066 tag schemas.

NIP-66 querying

REQ filter patterns and aggregation strategies.

REST API

Standard HTTP access to aggregated relay data.

CVM (MCP over Nostr)

21 structured MCP tools for relay intelligence over Nostr.

Build docs developers (and LLMs) love