Relay Declaration Specification

Specifies: kind 3 kind 10002 kind 10050 kind 10007 kind 10006 kind 10012 kind 10086 kind 10088 kind 10089 kind 30002

Nostr Relay Declaration Specification

1. Overview

A relay declaration is a signed Nostr event through which a user announces which relays they write their content to and which relays they monitor for content directed at them. Clients use these declarations to route queries and publications precisely rather than broadcasting to a fixed relay list.

This mechanism solves a fundamental discovery problem in the Nostr protocol: because users may publish to and read from any relay, a client that wishes to find a user’s content or deliver content to them must first learn which relays that user uses.

There are two structural families of relay declaration:

  • Relay List Events — a dedicated replaceable event carrying relay assignments as structured tags, with explicit read and write markers per relay.
  • Contact List Relay Hints — relay preferences embedded as JSON in the content field of a contacts event. This is a legacy format retained for compatibility with older clients and relays.

2. The Relay List Event

Rule 2.1: Event Kind

A relay list event uses kind 10002. It is a replaceable event: only the most recent relay list event per user is authoritative.

Rule 2.2: Content Field

The content field of a relay list event is always an empty string. All relay data is carried in the tags array.

Rule 2.3: Tag Format

Each relay entry is an r tag with the following structure:

["r", "<relay-url>"]
["r", "<relay-url>", "read"]
["r", "<relay-url>", "write"]

The first element is the tag name r. The second element is the relay URL. The optional third element is the marker, which declares the intended direction of use.

Rule 2.4: Marker Semantics

Tag form Meaning
["r", "<url>"] User reads from and writes to this relay
["r", "<url>", "read"] User reads from this relay only
["r", "<url>", "write"] User writes to this relay only

The absence of a marker is the canonical way to declare a relay for both reading and writing. Implementations must not require a marker to be present.

Rule 2.5: Content Field on Creation

When constructing a relay list event, the content field must be set to an empty string. The field must be present.

Rule 2.6: Marker on Creation

When constructing an r tag for a relay that is used for both reading and writing, the marker must be omitted entirely. A two-element tag is the correct and compact representation. A three-element tag is only used when the relay has a single direction.

Example event:

{
  "kind": 10002,
  "pubkey": "<hex-pubkey>",
  "created_at": 1700000000,
  "tags": [
    ["r", "wss://relay.example.com/"],
    ["r", "wss://inbox.example.com/", "read"],
    ["r", "wss://outbox.example.com/", "write"]
  ],
  "content": "",
  "id": "...",
  "sig": "..."
}

3. Parsing a Relay List Event

Rule 3.1: Kind Check

Implementations must verify that the event kind is 10002 before parsing relay entries from it. An event of any other kind must not be parsed as a relay list.

Rule 3.2: Tag Filtering

Only tags whose first element is the string r are relay entries. All other tags must be ignored.

Rule 3.3: Minimum Tag Length

Tags with fewer than two elements must be silently skipped. A valid relay entry requires at minimum a tag name and a URL.

Rule 3.4: URL Validation

Relay URLs must use the ws:// or wss:// scheme. Tags whose URL does not begin with one of these schemes must be silently discarded.

Rule 3.5: Marker Parsing

The third element of the tag determines the direction:

parse_relay_tag(tag):
  if length(tag) < 2:
    return null
  if tag[0] != "r":
    return null
  url = tag[1]
  if not is_websocket_url(url):
    return null
  marker = tag[2] if length(tag) >= 3 else null
  if marker == "read":
    return RelayEntry(url, read=true, write=false)
  if marker == "write":
    return RelayEntry(url, read=false, write=true)
  return RelayEntry(url, read=true, write=true)

Any marker value other than "read" or "write" must be treated the same as a missing marker: the relay is assigned both read and write.

Rule 3.6: Absent Relay List

If no relay list event exists for a given user, implementations must treat this as a normal condition. The absence of a relay list is not an error. Implementations should fall back to a default behavior such as querying a known set of well-connected relays.

4. Replaceability

Rule 4.1: One Authoritative Event Per User

Only one relay list event per user is considered current at any time. A relay list event with a higher created_at timestamp supersedes all prior relay list events from the same user.

Rule 4.2: Stale Event Rejection

When storing a relay list event, implementations must check whether a more recent event for the same user already exists. If the stored event has a strictly greater created_at, the incoming event must be discarded.

on_receive_relay_list_event(incoming):
  existing = get_stored_relay_list(incoming.pubkey)
  if existing != null and existing.created_at >= incoming.created_at:
    discard(incoming)
    return
  replace_with(incoming)

Rule 4.3: Delete Before Store

When replacing a stored relay list event with a newer one, the old event must be removed before the new one is written. Both events must never be simultaneously queryable.

5. Legacy Format: Contact List Relay Hints

Some older clients embed relay preferences in the content field of a contacts event (kind 3) rather than publishing a dedicated relay list event. Implementations that wish to support these clients may read relay data from this source.

Rule 5.1: Format

The content field of a kind 3 event may contain a JSON-encoded object. Each key is a relay URL string. Each value is an object with boolean read and write fields:

{
  "wss://relay.example.com": { "read": true, "write": true },
  "wss://inbox.example.com": { "read": true, "write": false },
  "wss://outbox.example.com": { "read": false, "write": true }
}

Rule 5.2: Parsing

parse_contact_list_relays(event):
  if event.kind != 3:
    return null
  if event.content is empty:
    return null
  try:
    relay_map = JSON.parse(event.content)
  catch:
    return null
  result = []
  for url, flags in relay_map:
    if not is_websocket_url(url):
      continue
    if flags.read and flags.write:
      result.add(RelayEntry(url, read=true, write=true))
    else if flags.read:
      result.add(RelayEntry(url, read=true, write=false))
    else if flags.write:
      result.add(RelayEntry(url, read=false, write=true))
    // if both are false: omit the entry
  return result

Rule 5.3: Priority

When both a relay list event and contact list relay hints exist for the same user, the relay list event takes precedence. The contact list relay hints should only be used when no relay list event is available.

Rule 5.4: Read Only

Implementations that support the legacy format should read relay hints from it but must not write relay data into the contact list content field for new events. New relay declarations must be published as relay list events.

6. Validation Requirements

Rule 6.1: Signature Verification

A relay declaration event must pass standard Nostr event signature verification before its contents are acted upon. Events that fail signature verification must be discarded.

Rule 6.2: ID Verification

A relay declaration event must pass standard Nostr event ID verification. The computed ID must match the id field. Events that fail ID verification must be discarded.


Optional Features

The following rules describe behaviors that are not universally required but that implementations may adopt.

Optional: URL Normalization

Optional Rule 7.1: Canonical Form

Implementations may normalize relay URLs before storing or comparing them, to prevent the same relay from appearing as multiple distinct entries due to superficial formatting differences.

Normalization steps:

  • Convert the scheme to lowercase (WSS://wss://)
  • Convert the hostname to lowercase
  • Remove the default port if present (wss://relay.example.com:443/wss://relay.example.com/)
  • Collapse redundant path separators

Example:

Input:  WSS://Relay.Example.COM:443/path//to
Output: wss://relay.example.com/path/to

Optional Rule 7.2: Bare Hostname Expansion

Implementations may expand bare hostnames to full WebSocket URLs by prepending wss://:

Input:  relay.example.com
Output: wss://relay.example.com

This should only be applied when the input contains no scheme. Inputs with a non-WebSocket scheme should be rejected rather than rewritten.

Optional: Address Filtering

Optional Rule 8.1: Localhost Exclusion

Implementations may discard relay entries pointing to localhost addresses before distributing them to other users or using them as relay hints in event tags. A localhost address is one that resolves only on the local machine and is not reachable by other network participants.

This filter should be applied when selecting relays to advertise to others, not necessarily when connecting locally for personal use.

Optional Rule 8.2: Onion Address Handling

Relay entries whose hostnames end in .onion are only reachable over the Tor network. Implementations may handle these in one of three ways:

  • Discard them unconditionally
  • Use them only when a Tor-capable transport is available
  • Pass them through without filtering

Implementations that discard onion addresses must still preserve them in the user’s own stored relay list, so the entries survive a round-trip through the editing interface without data loss.

Optional: Timestamp Tiebreaking

Optional Rule 9.1: Equal Timestamp Resolution

When two relay list events from the same user share an identical created_at value, implementations must choose exactly one to retain. Two strategies are in use:

Lower ID wins: The event with the lexicographically smaller id string is kept. This is deterministic and produces the same result across all relays regardless of arrival order.

First seen wins: The event already in storage is kept and the incoming event is discarded. This is simpler but may produce different outcomes across relays depending on which event arrived first.

Implementations should document which strategy they apply.

Optional: Bootstrap and Discovery

Optional Rule 10.1: Indexer Relay Bootstrap

When a user’s relay list is not yet known, implementations may query a configured set of well-known relays as a starting point. These relays are typically chosen because they aggregate relay declaration events from a wide range of users.

Once a relay list is retrieved for a user, subsequent queries for that user’s content should be directed to the relays declared in their list rather than to the bootstrap relays.

fetch_relay_list(pubkey):
  if cached(pubkey):
    return cache.get(pubkey)
  // Not yet known: use bootstrap relays
  event = query(bootstrap_relays, {kinds: [10002], authors: [pubkey], limit: 1})
  if event != null:
    cache.set(pubkey, parse_relay_list(event))
  return cache.get(pubkey)

Optional Rule 10.2: Multi-Relay Fetch Deduplication

When fetching a relay list event for a user from multiple relays simultaneously, multiple versions of the event may be returned — including older superseded versions from relays that have not yet received the latest update. Implementations should buffer all responses for a given user during the fetch window and select only the event with the highest created_at before acting on the result.

fetch_from_multiple(pubkey, relay_urls):
  candidates = []
  for relay in relay_urls:
    event = query(relay, {kinds: [10002], authors: [pubkey], limit: 1})
    if event != null:
      candidates.add(event)
  if candidates is empty:
    return null
  return max(candidates, key = event.created_at)

Optional Rule 10.3: Relay List Safety Cap

When a user’s declared relay list contains an unusually large number of write relays, this may indicate a misconfigured client that bulk-imported an unintended list. Implementations may define a maximum number of write relays above which the list is considered suspect, and fall back to a default well-known relay set for that user rather than using the declared list.

When this fallback is applied, the original declared entries must still be preserved in full so that the editing interface can display them and the user can correct the configuration.

Optional: Routing

Optional Rule 11.1: Outbox Routing

To retrieve a user’s published events, implementations should query that user’s write relays — the relays they have declared for writing — rather than a fixed or arbitrary relay list. These are the relays the user actively publishes to and where their content is most reliably found.

fetch_user_events(pubkey, filter):
  relay_list = fetch_relay_list(pubkey)
  write_relays = relay_list.write + relay_list.both
  return query(write_relays, filter)

Optional Rule 11.2: Inbox Routing

When publishing an event that is addressed to or mentions another user — such as a reply, reaction, or direct message — implementations should deliver the event to that user’s read relays: the relays they have declared for reading. These are the relays the user monitors for incoming content.

publish_to_user(event, recipient_pubkey):
  relay_list = fetch_relay_list(recipient_pubkey)
  read_relays = relay_list.read + relay_list.both
  publish(event, own_write_relays + read_relays)

Optional Rule 11.3: Relay Hint Selection

When including a relay hint URL in an event tag referencing another user, the best hint is a relay that appears in both the recipient’s read set and the sender’s write set. Such a relay is reachable by both parties, maximizing the likelihood that the reference can be resolved.

Priority order for hint selection:

  1. A relay in both the recipient’s read set and the sender’s write set
  2. Any relay in the recipient’s read set
  3. Any relay accumulated from prior observations of the recipient’s events
  4. Any relay in the sender’s write set

Optional: Purpose-Specific Relay Lists

Optional Rule 12.1: Dedicated Relay Events by Purpose

Implementations may publish and consume separate relay declaration events for distinct relay use cases, each identified by its own event kind. All such events follow the same structural rules as the relay list event: an empty content field, relay URLs carried as tags, and replaceable semantics. Unlike the relay list event, entries in purpose-specific relay list events use relay as the tag name rather than r, and carry no read/write marker. The semantics — what the relay is used for — are defined entirely by the event kind, not by a tag marker.

Common purposes for which separate events are defined:

Purpose Kind
Receive private messages 10050
Execute search queries 10007
Blocked relays 10006
User-curated favorites 10012
Content indexing and discovery 10086
Broadcast and distribution 10088
Trusted relays 10089

Optional Rule 12.2: Parsing Purpose-Specific Events

Purpose-specific relay list events use relay as the tag name rather than r. Parsers for these events must look for relay tags, not r tags. The two tag names must not be treated as interchangeable.

parse_purpose_relay_tag(tag):
  if length(tag) < 2:
    return null
  if tag[0] != "relay":
    return null
  url = tag[1]
  if not is_websocket_url(url):
    return null
  return url

Optional Rule 12.3: Priority for Specific Purposes

When routing for a specific purpose, an implementation should prefer the purpose-specific relay list over the general relay list if one exists. For example, when delivering a private message, a relay declared in the private message delivery list should take priority over a relay declared in the general relay list.

Optional: Named Relay Sets

Optional Rule 13.1: Addressable Relay Collections

Implementations may support a parameterized replaceable event representing a user-defined, named group of relay URLs. Unlike the relay list event, multiple named sets can coexist for the same user because each is identified by a unique string identifier carried in a d tag.

Event structure:

{
  "kind": 30002,
  "tags": [
    ["d", "<unique-identifier>"],
    ["title", "<human-readable-name>"],
    ["relay", "wss://relay.example.com/"],
    ["relay", "wss://relay2.example.com/"]
  ],
  "content": ""
}

Optional Rule 13.2: Identifier

The d tag value is the unique identifier for the set within the user’s namespace. Two named relay sets from the same user with the same d tag value are versions of the same set; the one with the higher created_at supersedes the other.

Optional Rule 13.3: Relay Entries

Relay entries in a named set use relay as the tag name and carry no direction marker. The set is a flat collection of URLs with no read/write distinction.

Optional: Private Relay Entries

Optional Rule 14.1: Encrypted Private Entries

Implementations may allow a relay list event to carry a subset of its relay entries in encrypted form in the content field. The plaintext of the content field is a JSON array of tags using the same format as the public tags array. This content is encrypted so that only the key holder can decrypt and read it.

The public tags array holds entries intended to be visible to others. The encrypted content holds entries the user wishes to keep private. When the user reads their own relay list, both sets are combined.

full_relay_list = parse_public_tags(event.tags)
                + decrypt_and_parse(event.content, private_key)

Optional Rule 14.2: Publishing with Private Entries

When constructing a relay list event with private entries, the private tag array must be serialized to JSON and encrypted before being placed in the content field. The public tags array must be left in plaintext. The event must be signed after encryption is complete.

Optional Rule 14.3: Graceful Decryption Failure

If decryption of the content field fails, implementations must not silently discard the private entries and continue as if the list is complete. Implementations should surface the failure to the caller so that a mutation operation does not inadvertently overwrite the private entries with an empty set.


No comments yet.