Relay Declaration Specification
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:
- A relay in both the recipient’s read set and the sender’s write set
- Any relay in the recipient’s read set
- Any relay accumulated from prior observations of the recipient’s events
- 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.
Looking for comments…
Searching Nostr relays. This may take a moment the first time this article is opened.
No comments yet.