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 events are stored on Nostr relays just like any other kind of Nostr event. You query them using standard NIP-01 REQ filters. This approach gives you the raw, cryptographically signed observations from each monitor — useful when you need per-monitor attribution, want to verify signatures yourself, or need historical data from kind 1066 events. If you want pre-aggregated data without handling raw events, the REST API and CVM tools are easier starting points.
NIP-66 relays
Connect to one or more of these relays that index NIP-66 events:
wss://relay.nostr.watch
wss://relaypag.es
wss://monitorlizard.nostr1.com
Use multiple relays for redundancy. If one is unreachable, the others still serve the data.
The nostr-tools library provides a SimplePool that manages connections to multiple relays and handles REQ/EOSE for you.
import { SimplePool } from 'nostr-tools'
const pool = new SimplePool()
const relays = ['wss://relay.nostr.watch', 'wss://relaypag.es']
Get all relay statuses
// Fetch all current relay status events
const statuses = await pool.querySync(relays, {
kinds: [30166]
})
console.log(`${statuses.length} relay status events`)
Get status for a specific relay
Use the #d filter to match the parameterized replaceable d tag. This returns one event per monitor that has checked the relay.
const damusStatus = await pool.querySync(relays, {
kinds: [30166],
'#d': ['wss://relay.damus.io']
})
console.log(`${damusStatus.length} monitors have checked relay.damus.io`)
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)}... checks every ${frequency}s`)
console.log(` Networks: ${networks.join(', ')}`)
console.log(` Check types: ${checks.join(', ')}`)
}
Filtering by relay attributes
NIP-66 tag filters let you query relays by their properties server-side. Always filter on the relay rather than fetching everything and filtering client-side.
// Clearnet relays running strfry
const strfryRelays = await pool.querySync(relays, {
kinds: [30166],
'#n': ['clearnet'],
'#s': ['strfry']
})
// Relays supporting NIP-42
const nip42Relays = await pool.querySync(relays, {
kinds: [30166],
'#N': ['42']
})
// Relays not requiring auth or payment
const openRelays = await pool.querySync(relays, {
kinds: [30166],
'#R': ['!auth', '!payment']
})
// Relays supporting both NIP-1 and NIP-42
const multiNipRelays = await pool.querySync(relays, {
kinds: [30166],
'#N': ['1', '42'] // AND: must support both
})
Tag filter values are always strings. Use '#N': ['42'] not '#N': [42]. Multiple values in one array use OR logic (match any). Multiple separate tag keys use AND logic (match all).
Aggregating across monitors
Multiple monitors report on the same relay. To get a reliable picture, aggregate their observations. Verify signatures, keep only the most recent event per monitor, then compute consensus values.
import { SimplePool, verifyEvent } from 'nostr-tools'
const pool = new SimplePool()
const relays = ['wss://relay.nostr.watch', 'wss://relaypag.es']
async function getConsensusState(relayUrl) {
// Fetch all observations for this relay
const events = await pool.querySync(relays, {
kinds: [30166],
'#d': [relayUrl]
})
// Verify signatures
const verified = events.filter(e => verifyEvent(e))
// Keep the most recent event per monitor
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)
}
}
const observations = [...byMonitor.values()]
// Aggregate RTT values using median
function getTagValue(event, tagName) {
const tag = event.tags.find(t => t[0] === tagName)
return tag ? Number(tag[1]) : null
}
function median(values) {
const sorted = [...values].sort((a, b) => a - b)
return sorted[Math.floor(sorted.length / 2)]
}
const rttValues = observations
.map(e => getTagValue(e, 'rtt-open'))
.filter(v => v !== null)
const medianRtt = rttValues.length > 0 ? median(rttValues) : null
// Count how many monitors consider the relay open
const openCount = observations.filter(e =>
e.tags.some(t => t[0] === 'R' && t[1] === 'open')
).length
return {
relayUrl,
monitorCount: observations.length,
openRatio: openCount / observations.length,
medianRttOpen: medianRtt,
}
}
const state = await getConsensusState('wss://relay.damus.io')
console.log(`Median open RTT: ${state.medianRttOpen}ms from ${state.monitorCount} monitors`)
console.log(`Open ratio: ${(state.openRatio * 100).toFixed(0)}%`)
Signature verification
Always verify event signatures before trusting the data. This is the key advantage of raw NIP-66 — you get cryptographic proof 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.slice(0, 16)}`)
continue
}
// Event is cryptographically verified
processEvent(event)
}
Live subscriptions
Use Nostr subscriptions to receive real-time updates as monitors publish new data.
import { SimplePool, verifyEvent } from 'nostr-tools'
const pool = new SimplePool()
const relays = ['wss://relay.nostr.watch', 'wss://relaypag.es']
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)
}
}
})
// Close when done
// sub.close()
Querying history with kind 1066
Kind 1066 events are append-only deltas. Use them to build historical timelines.
// Status changes for a relay over the last 24 hours
const deltaFilter = {
kinds: [1066],
'#r': ['wss://relay.damus.io'],
since: Math.floor(Date.now() / 1000) - 86400
}
const deltas = await pool.querySync(relays, deltaFilter)
console.log(`${deltas.length} state changes in the last 24 hours`)
NIP-66 relays may store thousands of events. Use limit 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
})
Python example
import json
import websocket
relay_url = "wss://relay.nostr.watch"
ws = websocket.create_connection(relay_url)
# Request all current relay statuses
req = json.dumps(["REQ", "sub1", {"kinds": [30166]}])
ws.send(req)
# Read events until EOSE
events = []
while True:
msg = json.loads(ws.recv())
if msg[0] == "EVENT":
events.append(msg[2])
elif msg[0] == "EOSE":
break
# Close subscription
ws.send(json.dumps(["CLOSE", "sub1"]))
ws.close()
print(f"Received {len(events)} relay status events")
Tips
- Use multiple relays for redundancy. Connect to at least two NIP-66 relays so you still get data if one is down.
- Verify before trusting. Always call
verifyEvent() on received events before processing them.
- Filter server-side. Use tag filters in your REQ rather than fetching everything and filtering client-side — it is faster and reduces bandwidth.
- Check
created_at against the monitor’s declared frequency. Discard events older than the monitor’s interval — they are likely stale.
- Cache locally. NIP-66 data updates roughly hourly. There is no need to re-query constantly.
If you need aggregated values without doing the aggregation yourself, use the REST API or CVM tools. They run the aggregation for you and expose clean JSON responses.