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.

Both @nostrwatch/nocap and @nostrwatch/route66 use an adapter pattern to keep core orchestration logic separate from the concrete implementations that perform network I/O. In nocap, adapters supply the actual DNS, SSL, WebSocket, geo, and NIP-11 check logic. In route66, adapters supply local storage and WebSocket connectivity. You can swap any of these independently — replacing only the part relevant to your runtime, network topology, or service provider — without touching the rest of the stack.

nocap adapters

nocap separates check orchestration (timeouts, result aggregation, check ordering) from check execution (making DNS queries, opening sockets, fetching NIP-11 documents). All check execution lives in adapters. The Nocap class reads each adapter’s static type property to dispatch the right adapter for each requested check key.

AbstractAdapter base class

Every nocap adapter must extend AbstractAdapter:
abstract class AbstractAdapter implements IAdapter {
  static type: AdapterType             // one of: 'websocket'|'dns'|'geo'|'info'|'ssl'
  readonly slug: string                // human-readable adapter identifier
  protected _base: Base                // the Nocap instance this adapter belongs to
  get base(): Base                     // accessor for _base

  constructor(base: Base)
  abstract initialize(): void          // called automatically on construction
}
The base getter exposes the parent Nocap instance. Call this.base.finish(key, result) inside any check method to resolve that check — adapters do not return values from check methods directly.

IAdapter interface

All adapters must implement IAdapter:
interface IAdapter {
  readonly base: Base        // the Nocap instance (provided by AbstractAdapter)
  readonly slug: string      // human-readable adapter name
  initialize(): void         // called on construction

  // optional lifecycle hooks
  count?: TAdapterCount
}

Per-type interfaces

Each adapter type has its own interface specifying which check method(s) the adapter must implement. All check methods have the signature (): Promise<void> — they resolve the check by calling this.base.finish() rather than returning a value.
TypeInterfaceRequired methods
websocketIWebsocketAdaptercheck_open(), check_read(), check_write()
dnsIDnsAdaptercheck_dns()
infoIInfoAdaptercheck_info()
sslISslAdaptercheck_ssl()
geoIGeoAdaptercheck_geo()

Custom DNS adapter example

The following is a complete, minimal DNS adapter taken from the nocap source:
import {AbstractAdapter, type IAdapter, type AdapterType} from '@nostrwatch/nocap'

export class MyDnsAdapter extends AbstractAdapter implements IAdapter {
  static type: AdapterType = 'dns'
  readonly slug: string = 'MyDnsAdapter'

  constructor(base: any) {
    super(base)
  }

  initialize(): void {}

  async check_dns(): Promise<void> {
    // Perform your DNS lookup here
    const host = new URL(this.base.url).hostname
    const ipv4 = ['1.2.3.4'] // your actual lookup result

    // Call base.finish() to resolve the check.
    // First argument: the check key.
    // Second argument: IResultData with data, duration, and status.
    this.base.finish('dns', {
      data: {ipv4},
      duration: 50,
      status: 'success'
    })
  }
}
The IResultData shape expected by base.finish():
interface IResultData {
  data: boolean | object | null
  duration: number        // milliseconds; -1 if timed out or errored
  status?: string         // 'success' | 'error'
  message?: string        // error message if status is 'error'
}

Registering nocap adapters

Register adapters before calling check(). You can register one adapter at a time or pass an array or object:
import Nocap from '@nostrwatch/nocap'
import {MyDnsAdapter} from './MyDnsAdapter'

const nocap = new Nocap('wss://relay.damus.io')

// Register a single adapter
await nocap.useAdapter(MyDnsAdapter)

// Register multiple adapters at once
await nocap.useAdapters([MyDnsAdapter, MyWebsocketAdapter])

// Or using an object — type names map to adapter classes
await nocap.useAdapters({dns: MyDnsAdapter, websocket: MyWebsocketAdapter})

const result = await nocap.check('dns')
console.log(result.dns)
// { data: { ipv4: ['1.2.3.4'] }, duration: 50, status: 'success' }
useAdapter and useAdapters throw if an adapter of that type has already been registered. Each type slot can only hold one adapter at a time.

Default adapters (reference implementations)

Five adapters ship with nocap in libraries/nocap/adapters/default/. Use these as-is or as reference implementations when building custom adapters:

WebsocketAdapterDefault

Opens a WebSocket connection and performs open, read, and write checks. Uses @nostrwatch/websocket for cross-platform socket support.

DnsAdapterDefault

DNS lookup via Cloudflare DNS-over-HTTPS (1.1.1.1). Handles clearnet and IP-addressed relays.

SslAdapterDefault

TLS certificate validation using Node.js TLS APIs. Not available in browser environments.

GeoAdapterDefault

Geographic IP lookup. Depends on the DNS check result for the relay’s resolved IP.

InfoAdapterDefault

Fetches the NIP-11 relay information document from the relay’s HTTP endpoint.
The ssl check uses Node.js TLS APIs and cannot run in a browser. If check('ssl') is called from a browser context, nocap logs a warning and skips the check rather than throwing.

route66 cache adapters

Cache adapters in route66 store NIP-66 relay check events locally and answer queries from RelayService. They implement ICacheAdapter and extend the CacheAdapter base class.

CacheAdapter base class and ICacheAdapter interface

import {CacheAdapter, type ICacheAdapter} from '@nostrwatch/route66'
import type {IEvent} from '@nostrwatch/route66'

export class MyCacheAdapter extends CacheAdapter implements ICacheAdapter {
  readonly slug = 'MyCacheAdapter'

  async ready(): Promise<void> {
    // Signal that the adapter is initialized and ready to serve queries
  }

  async REQ(filters: any[]): Promise<IEvent[]> {
    // Execute a Nostr REQ query and return matching events
    return []
  }

  async COUNT(filters: any[]): Promise<number> {
    // Return the count of events matching the given filters
    return 0
  }

  async DELETE(filters: any[]): Promise<string[]> {
    // Delete events matching the given filters; return deleted event IDs
    return []
  }

  async DUMP(): Promise<Uint8Array> {
    // Export the full event store as a binary blob
    return new Uint8Array()
  }

  async CLOSE(subId: string): Promise<boolean> {
    // Close a subscription by ID; return true if closed successfully
    return true
  }

  async WIPE(): Promise<boolean> {
    // Clear all stored events; return true if successful
    return true
  }

  async addEvent(event: IEvent): Promise<void> {
    // Insert a single event; no-op if already present
  }

  async addEvents(events: IEvent[]): Promise<void> {
    // Batch insert events
  }

  async putEvent(event: IEvent): Promise<void> {
    // Upsert a single event (insert or replace)
  }
}
The method contract in brief:
MethodDescription
REQ(filters)Execute a Nostr REQ query; return matching events
COUNT(filters)Return the count of matching events
DELETE(filters)Delete matching events; return deleted IDs
DUMP()Export the full store as a binary blob
CLOSE(subId)Close a subscription by ID
WIPE()Clear all stored events
addEvent(event)Insert a single event; no-op if already present
addEvents(events)Batch insert events
putEvent(event)Upsert a single event (insert or replace)

Registering a cache adapter

Pass the adapter instance to Route66 at construction time:
import {Route66} from '@nostrwatch/route66'

const cacheAdapter = new MyCacheAdapter()
const r66 = new Route66({cacheAdapter, websocketAdapter})
await r66.boot()
The reference implementation is @nostrwatch/route66-cacheadapter-nostrsqlite — an SQLite-backed adapter using @nostrwatch/worker-relay in a web worker that works in both browser and Node.js.

route66 WebSocket adapters

WebSocket adapters in route66 open connections to Nostr monitoring relays and deliver incoming NIP-66 events to the rest of route66. They implement IWebsocketAdapter and extend the WebsocketAdapter base class.

Registration pattern

Like cache adapters, WebSocket adapters are provided at construction time:
import {Route66} from '@nostrwatch/route66'

const websocketAdapter = new MyWebsocketAdapter()
const r66 = new Route66({cacheAdapter, websocketAdapter})
await r66.boot()

Reference implementation

The existing @nostrwatch/route66-wsadapter-nostrtools is the canonical reference for implementing a WebSocket adapter. It wraps nostr-tools and nostr-fetch to open pool connections and subscribe to NIP-66 event streams. Reference implementations for both adapter types are in libraries/route66/adapters/.
Both adapter dimensions are independently swappable. You can use the default SQLite cache adapter while providing a custom WebSocket adapter, or vice versa — mix and match to fit your environment.

Build docs developers (and LLMs) love