Skip to content

Personal

nostr-tools

An async Python library for the Nostr protocol that measures whether a relay can connect, read, and write instead of trusting what its NIP-11 document advertises.

Aug 2025Nostr

An async Python library developed for BigBrotr’s earlier Python relay collection and monitoring implementation. It implements events, filters, subscriptions and Schnorr signing, alongside probes that measure whether a relay accepts connections, returns events and accepts writes. This library is separate from the current Go implementation.

Implements
NIP-01 · NIP-11 · NIP-66 · BIP-340 · bech32
Probes
Openable, readable, writable, each with its own timing
Compatibility
Python 3.9–3.13
Repository
BigBrotr/nostr-tools
BigBrotr/nostr-toolsAn async Python library for Nostr events, filters, subscriptions, signing, and relay capability probing.Python

The problem

A relay’s NIP-11 document is a self-description. It lists the protocol extensions the operator advertises as supported. That declaration does not establish what a remote client can actually do at the time of a measurement.

The library captures the document separately from the probes. The advertised list does not drive their results: each records the outcome of a concrete operation.

What I built

Three probes, each answering one question, each with its own timing: can a connection be opened, will a subscription return events, and will a write be accepted. The write probe publishes a parameterized-replaceable event keyed by the relay’s own URL, so repeatedly measuring the same relay updates one record rather than accumulating a trail in somebody else’s database.

Above them sits a single call that returns the NIP-11 document and the measurements together with a timestamp, which is the actual unit an observatory stores.

Around that is the ordinary protocol surface: events with their identifier and Schnorr signature, filters, subscriptions, an eighteen-type exception hierarchy, and a client that refuses to be constructed for a Tor relay without a proxy configured, so the failure happens at construction rather than at connection.

Hard parts

A measurement that half-succeeded is worse than one that failed. A latency figure alongside a verdict of “not readable” is a contradiction, and it shipped: the probe recorded a round-trip on the first message received, before checking what that message was, so a relay that answered by closing the subscription produced a timing for a read that never happened. Downstream validation rejected the record. The fix makes the invariant explicit – a failed check reports no timing at all – and it is a patch, not foresight.

Supporting Python 3.9 costs the toolchain. The floor is deliberate, for reach, and it holds the test framework several major versions back with the reason annotated next to each pin.