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.

Internal packages in the nostr-watch monorepo are shared infrastructure that sits at the base of the dependency graph. They are not published to npm and are not intended for standalone use outside the monorepo. Every consuming package adds them as a pnpm workspace dependency. The strict layering rule is that internal packages have no dependencies on libraries/ or apps/ packages — only the reverse direction is permitted.
internal  →  libraries  →  apps
This means you can safely import any internal package from a library or app, but internal packages themselves stay lean and side-effect-free.

@nostrwatch/logger

Structured, level-gated logging. Winston wrapper with browser fallback and per-namespace labeling.

@nostrwatch/utils

Shared utility functions — key conversion, array helpers, URL parsing, signing, and runtime detection.

@nostrwatch/controlflow

BullMQ-backed job queues and exponential-backoff retry management for relay check workers.

@nostrwatch/seed

Bootstrap relay lists from config, static files, databases, REST APIs, or Nostr events.

@nostrwatch/publisher

Build and publish NIP-66 relay status and monitor registration events. See publishing events.

@nostrwatch/announce

Publish the three monitor announcement events (kind 0, 10002, 10166) on every boot.

@nostrwatch/logger

@nostrwatch/logger wraps Winston to provide structured, level-gated logging for both Node.js and browser environments. On Node.js it uses Winston with colorized, timestamped output; in the browser it falls back to the native console. Every consuming package instantiates Logger with a namespace string, giving each log line a [label] prefix that identifies which module produced it.
import Logger from '@nostrwatch/logger'

const log = new Logger('my-module', 'INFO')

log.info('relay connected')
// 2026-03-04T20:00:00.000Z [my-module] info: relay connected

log.debug('processing event')
// (no output — debug is below the INFO threshold)

log.warn('relay slow to respond')
// 2026-03-04T20:00:01.000Z [my-module] warn: relay slow to respond

Logger class

class Logger {
  constructor(name: string, log_level?: string, split_logs?: boolean)
  fatal(message: any): void
  error(message: any): void
  err(message: any): void     // alias for error()
  warn(message: any): void
  info(message: any): void
  debug(message: any): void
}
ParameterTypeDefaultDescription
namestringLabel printed with every log line
log_levelstring'INFO'Minimum level to emit: FATAL, ERROR, WARN, INFO, DEBUG
split_logsbooleanfalseReserved for future use
The debug level also checks whether the debug npm package’s namespace is enabled via the DEBUG environment variable, allowing selective per-namespace debug output:
DEBUG=my-module node index.js

@nostrwatch/utils

@nostrwatch/utils provides roughly 15 utility modules shared across the monorepo. Each module is independently importable — import only what you need. The package builds for both Node.js and browser environments.
import {nsecToHex, tryNsecToHex} from '@nostrwatch/utils'
import {shuffleArray, chunkArray} from '@nostrwatch/utils'
import {isBrowser} from '@nostrwatch/utils'

// Convert an nsec private key to hex (throws on invalid input)
const hexKey = nsecToHex('nsec1...')

// Safe version — returns empty string on failure
const safeHex = tryNsecToHex(process.env.NOSTR_KEY)

// Shuffle and chunk an array
const relays = ['wss://relay.damus.io', 'wss://nos.lol']
chunkArray(relays, 10) // => [['wss://relay.damus.io', 'wss://nos.lol']]

// Detect runtime environment
if (isBrowser()) {
  console.log('running in browser')
}

Key exports

Keys (keys.ts)
function nsecToBytes(key: string): Uint8Array
function nsecToHex(key: string): string
function tryNsecToHex(key: string | undefined): string
Converts Nostr private keys between formats. Accepts nsec1… bech32 strings or 64-char hex. nsecToBytes and nsecToHex throw on invalid input; tryNsecToHex returns an empty string — use it when reading from environment variables. Signing (signing.ts)
interface EventSigner {
  pubkey: string
  sign(event: UnsignedEvent): VerifiedEvent
}

function createSigner(key: string): EventSigner
Creates an EventSigner from an nsec or hex private key. The returned signer holds the derived public key and can sign any unsigned Nostr event. Wraps nostr-tools/pure internally. Arrays (array.ts)
function shuffleArray<T>(array: T[]): void
function chunkArray<T>(arr: T[], chunkSize: number): T[][]
shuffleArray shuffles in place using Fisher-Yates. chunkArray shuffles the array first, then splits it into chunks of the given size. URL (url.ts)
function parseUrl(url: string): URL | null
function normalizeUrl(url: string): string
parseUrl returns null on invalid input instead of throwing. normalizeUrl returns the canonical URL string, or the original input if parsing fails. Browser detection (browser.ts)
function isBrowser(): boolean
Returns true when running in a browser or Web Worker context. Safe to call in any environment.

@nostrwatch/controlflow

@nostrwatch/controlflow provides BullMQ-backed job queues and a cache-based retry manager for the relay check pipeline. It solves two recurring problems: distributing relay-check work across workers with guaranteed delivery, and backing off automatically when a relay consistently fails to respond. Prerequisites: A running Redis instance. Set REDIS_HOST, REDIS_PORT, and REDIS_PASSWORD (if applicable) in the consuming application’s environment.
import {NocapdQueue, RetryManager} from '@nostrwatch/controlflow'

// Initialize a named BullMQ queue
const {$Queue, $QueueEvents, Worker} = NocapdQueue('my-worker')

// Add a relay-check job to the queue
await $Queue.add('check', {url: 'wss://relay.damus.io'})

// Process jobs in a worker
new Worker('my-worker', async (job) => {
  console.log('Checking:', job.data.url)
  // run your check here
})

Queue factories

All queue factory functions return a { $Queue, $QueueEvents, Worker } triple. Calls with the same name return the same cached singleton.
FactoryDescription
TrawlQueue(qopts?)Singleton queue for relay trawling jobs
NocapdQueue(name?, qopts?)Queue for nocap relay-check jobs; defaults to 'NocapdQueue'
PersistQueue(name?, qopts?)Queue for database persistence jobs; defaults to 'PersistQueue'
QueueInit(key, qopts?)Low-level factory for creating a named queue not covered by the above
BullMQRe-exports raw BullMQ Queue, QueueEvents, and Worker classes

RetryManager

RetryManager tracks per-relay retry counts in cache and maps them to exponential delay intervals. Use it to decide how long to wait before re-queuing a relay that has failed:
class RetryManager {
  constructor(caller: string, config?: RetryConfig, rcache?: RelayCache)
  cacheId(url: string): string
  expiry(retries: number | null): number
  getRetries(url: string): Promise<number | null>
  getExpiry(url: string): Promise<number>
  setRetries(url: string, success: boolean): Promise<string | null>
}
Call setRetries(url, true) to reset a relay’s counter after a successful check, or setRetries(url, false) to increment it. Call getExpiry(url) to get the current backoff delay in milliseconds. Default retry escalation ladder:
RetriesDelay
1–31 hour
4–624 hours
7–137 days
14–1728 days
18–2990 days

@nostrwatch/seed

@nostrwatch/seed collects relay URLs from multiple discovery sources to bootstrap the relay monitoring pipeline. Results are deduplicated, sanitized, and filtered by network type before being handed off to the monitor.
import {RelaySeeder} from '@nostrwatch/seed'

const seeder = new RelaySeeder({
  interval: 3600000, // re-seed every hour
  sources: ['config', 'static', 'api'],
  options: {
    allowedNetworks: ['clearnet'],
    config: ['wss://relay.damus.io', 'wss://nos.lol'],
    static: {path: './relays.yaml'},
    api: {rest_api: 'https://api.nostr.watch/v1'}
  }
})

await seeder.start()

const relays = seeder.getRelays()
console.log(`Seeded ${relays.length} relays`)
// Seeded 247 relays

Supported source types

SourceDescription
configRelay URLs passed directly as an array in options.config
staticRelay URLs read from a YAML or JSON file at options.static.path
dbRelay URLs queried from a SQLite database (filters by allowedNetworks)
apiRelay URLs fetched from a REST API at options.api.rest_api/online
eventsRelay URLs extracted from NIP-66 kind 30166 events by specified pubkeys
cacheRelay URLs queried from the local @nostrwatch/db relay status table
Set allowedNetworks to ['clearnet'], ['tor'], ['i2p'], or any combination. Defaults to ['clearnet'] if omitted.

@nostrwatch/announce

@nostrwatch/announce generates and publishes three Nostr events each time a monitor starts: a kind 10166 monitor registration event, a kind 10002 relay list event, and a kind 0 profile event. Together these events allow other clients and monitors to discover this monitor’s identity and capabilities automatically. For full usage details, see publishing events.

Deprecated packages

Two internal packages are deprecated and maintained as stubs for migration guidance:
PackageReplacement
internal/nwcacheDirect cache integrations in consuming packages
apps/nocapdapps/relaymon
@nostrwatch/nwcache is referenced by @nostrwatch/controlflow’s RetryManager but should not be used in new code. Migrate to direct cache integrations.

Build docs developers (and LLMs) love