Relay Information Document Specification
Nostr Relay Information Document Specification
1. Overview
A Nostr relay may expose a machine-readable JSON document over HTTP that describes its identity, operator, capabilities, and operational constraints. Clients can retrieve this document to discover what a relay supports before or after connecting to it.
The document is served from the same base URL used for WebSocket connections, distinguished from other request types by an Accept header. All fields in the document are optional — the document is a best-effort, self-reported description, and no field is guaranteed to be present.
2. Retrieving the Document
Rule 2.1: HTTP Request
The client retrieves the document by sending an HTTP GET request to the relay’s base URL with the following header:
Accept: application/nostr+json
Example:
GET / HTTP/1.1
Host: relay.example.com
Accept: application/nostr+json
Rule 2.2: Accept Header Matching
The relay identifies an information document request by checking the Accept header against the exact string application/nostr+json. This is a strict equality check — not a substring match, not content negotiation, and not wildcard matching.
request.header("Accept") == "application/nostr+json"
Requests that send Accept: application/nostr+json, */* or any other value must not be treated as information document requests. Clients must send the header with precisely this value.
Rule 2.3: URL Protocol Conversion
Relay URLs use the WebSocket protocol (wss:// or ws://). Before making the HTTP request, the client converts the URL to its HTTP equivalent:
wss:// → https://
ws:// → http://
The host, path, port, and query string are left unchanged.
Example:
wss://relay.example.com → https://relay.example.com
ws://localhost:8080/nostr → http://localhost:8080/nostr
Rule 2.4: Multiplexing with WebSocket
The relay serves both its WebSocket endpoint and the information document from the same base URL. Dispatch is determined by request headers:
on GET /:
if Upgrade == "websocket":
→ handle WebSocket connection
else if Accept == "application/nostr+json":
→ serve information document
else:
→ serve fallback response (landing page, redirect, etc.)
3. Response Format
Rule 3.1: Status and Headers
A successful response must include:
HTTP/1.1 200 OK
Content-Type: application/nostr+json
Access-Control-Allow-Origin: *
The Access-Control-Allow-Origin: * header is required so that browser-based clients can fetch the document from any origin without CORS restrictions.
Rule 3.2: Body
The response body is a single JSON object. All fields are optional. Clients must not treat a missing or null field as an error.
Example response:
{
"name": "My Relay",
"description": "A public Nostr relay.",
"pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
"contact": "mailto:admin@example.com",
"supported_nips": [1, 2, 9, 11, 40],
"software": "https://github.com/example/myrelay",
"version": "1.4.2",
"limitation": {
"max_message_length": 131072,
"max_subscriptions": 20,
"auth_required": false,
"payment_required": false
}
}
Rule 3.3: Unknown Fields
Clients must silently ignore any fields in the response that they do not recognize. The document format evolves over time, and relay implementations may include experimental or future fields.
4. Document Fields
Rule 4.1: Identity Fields
These fields describe the relay and its operator.
| Field | Type | Description |
|---|---|---|
name |
string | Human-readable name for the relay |
description |
string | Description of the relay’s purpose or audience |
pubkey |
string | Relay operator’s public key, lowercase hex-encoded |
contact |
string | Contact address for the operator (e.g. mailto: or https:) |
icon |
string | URL to a square image representing the relay |
banner |
string | URL to a banner image for the relay |
The pubkey field, when present, must be a raw 32-byte public key encoded as 64 lowercase hexadecimal characters. Bech32-encoded keys must not appear in this field.
Rule 4.2: Software Fields
These fields identify the relay implementation.
| Field | Type | Description |
|---|---|---|
software |
string | URL or identifier for the relay software |
version |
string | Version string of the relay software |
Rule 4.3: Capability Fields
These fields declare what protocol features the relay supports.
| Field | Type | Description |
|---|---|---|
supported_nips |
array of numbers | Protocol feature numbers this relay implements |
Clients should consult supported_nips before using protocol features that require explicit relay support.
Because some relay implementations serialize a single-element list as a bare scalar, clients should accept both "supported_nips": [1] and "supported_nips": 1 as equivalent.
Rule 4.4: Limitation Fields
The optional limitation object describes constraints the relay enforces on clients. All subfields are optional.
Access control:
| Field | Type | Description |
|---|---|---|
auth_required |
boolean | Whether authentication is required before any action |
payment_required |
boolean | Whether payment is required before any action |
restricted_writes |
boolean | Whether some write condition must be fulfilled |
Size and throughput limits:
| Field | Type | Description |
|---|---|---|
max_message_length |
integer | Maximum byte length of any incoming WebSocket message |
max_subscriptions |
integer | Maximum concurrent subscriptions per connection |
max_filters |
integer | Maximum filters per subscription request |
max_limit |
integer | Maximum value of the limit filter field |
max_subid_length |
integer | Maximum character length of a subscription ID |
min_prefix |
integer | Minimum hex prefix length for ID and pubkey filters |
max_event_tags |
integer | Maximum number of tags on a single event |
max_content_length |
integer | Maximum character length of an event’s content field |
Proof-of-work:
| Field | Type | Description |
|---|---|---|
min_pow_difficulty |
integer | Minimum proof-of-work difficulty required on incoming events |
Event validity bounds:
| Field | Type | Description |
|---|---|---|
created_at_lower_limit |
integer | Earliest Unix timestamp accepted in created_at |
created_at_upper_limit |
integer | Latest Unix timestamp accepted in created_at |
Example:
{
"limitation": {
"max_message_length": 65536,
"max_subscriptions": 10,
"max_filters": 5,
"max_limit": 1000,
"max_subid_length": 128,
"min_prefix": 4,
"max_event_tags": 2500,
"max_content_length": 8192,
"min_pow_difficulty": 0,
"auth_required": false,
"payment_required": true,
"restricted_writes": false,
"created_at_lower_limit": 1577836800,
"created_at_upper_limit": 9999999999
}
}
Rule 4.5: Policy and Community Fields
| Field | Type | Description |
|---|---|---|
relay_countries |
array of strings | ISO 3166-1 alpha-2 country codes indicating the relay’s jurisdiction or audience |
language_tags |
array of strings | BCP-47 language tags for the relay’s primary languages, in preference order |
tags |
array of strings | Arbitrary descriptive labels (e.g. "bitcoin-only", "sfw-only") |
posting_policy |
string | URL to a human-readable document describing the relay’s posting rules |
privacy_policy |
string | URL to the relay’s privacy policy |
terms_of_service |
string | URL to the relay’s terms of service |
Rule 4.6: Payment Fields
| Field | Type | Description |
|---|---|---|
payments_url |
string | URL to a page with payment or subscription details |
fees |
object | Structured fee schedule (see Rule 4.7) |
Rule 4.7: Fee Schedule
The fees object groups fees into three categories:
{
"fees": {
"admission": [
{ "amount": 1000000, "unit": "msats" }
],
"subscription": [
{ "amount": 5000, "unit": "msats", "period": 2592000 }
],
"publication": [
{ "amount": 100, "unit": "msats", "kinds": [1, 6, 7] }
]
}
}
Each fee entry contains:
| Field | Type | Description |
|---|---|---|
amount |
integer | The cost |
unit |
string | The currency denomination (e.g. "msats") |
period |
integer | For subscription fees, the billing period in seconds |
kinds |
array of integers | For publication fees, the event kinds this fee applies to; absent means all kinds |
Rule 4.8: Retention Fields
The retention array describes how long or how many events the relay stores, optionally scoped to specific event kinds:
{
"retention": [
{ "kinds": [0, 1, 3], "time": 86400 },
{ "kinds": [[10000, 19999]], "count": 1000 },
{ "time": 3600, "count": 10000 }
]
}
Each entry contains:
| Field | Type | Description |
|---|---|---|
kinds |
array | Event kinds this rule applies to; entries may be integers or two-element [start, end] range arrays; absent means the rule applies globally |
time |
integer or null | Seconds to retain events; null means retain forever; 0 means events of this kind are not stored |
count |
integer or null | Maximum number of events retained |
5. Failure Handling
Rule 5.1: Failed Fetch
If the HTTP request fails for any reason — network error, connection timeout, non-JSON response body, relay not implementing this endpoint — the client must treat the result as “no document available.” This must not be treated as a fatal error or cause the relay connection to be closed.
Rule 5.2: Missing Fields
Clients must handle any field being absent. The correct behavior when a field is missing is to proceed as if that information is unknown, not to assume a default value or abort the operation.
Rule 5.3: Partial Documents
A relay may return a document containing only a subset of known fields. Clients must process whatever fields are present and ignore the rest.
Optional Features
The following behaviors are implemented by some clients and relays but are not required.
Optional: Fetch Triggering Strategies
Optional Rule 6.1: Automatic Fetch on Connection
A client implementation may automatically initiate an information document fetch whenever a new relay connection is opened, without requiring any explicit caller action. This ensures metadata is available before it is first needed.
on_relay_connected(relay_url):
fetch_information_document(relay_url)
// continues in background; does not block connection use
Optional Rule 6.2: Lazy Fetch on Demand
A client implementation may defer the fetch until something explicitly requests the document. Establishing a connection does not trigger a request.
get_relay_information(relay_url):
if not already_fetching(relay_url):
start_fetch(relay_url)
return await fetch_result(relay_url)
Optional: Caching
Optional Rule 7.1: Per-Relay Lifetime Cache
A client may cache the fetched document for the lifetime of the relay object or session. When multiple parts of the application request the document for the same relay URL simultaneously, only one HTTP request is issued; all requesters receive the same result. Late requesters receive the cached result without triggering a new request.
fetch_information_document(url):
if cache.has(url):
return cache.get(url) // immediate, no network request
result = await http_get(url)
cache.set(url, result)
return result
Optional Rule 7.2: Application-Wide Registry
A client may store fetched documents in a single shared registry keyed by relay URL. Any part of the application can retrieve a document by URL without knowing whether it has already been fetched.
registry = Map<url, RelayDocument>
load_document(url):
if registry.has(url):
return // already loaded
doc = await fetch_information_document(url)
registry.set(url, doc)
Optional: Timeouts
Optional Rule 8.1: Automatic Timeout
A client may apply an automatic timeout to the fetch request. If the relay does not respond within the timeout period, the fetch is treated as a failure per Rule 5.1.
Recommended timeout range: 7–10 seconds.
Optional Rule 8.2: Caller-Supplied Deadline
A client may accept a caller-provided deadline or timeout and honor it in preference to any automatic timeout. The automatic timeout applies only when the caller provides none.
fetch_information_document(url, deadline=null):
effective_deadline = deadline ?? now() + DEFAULT_TIMEOUT
return http_get_with_deadline(url, effective_deadline)
Optional: URL Canonicalization
Optional Rule 9.1: Full Canonicalization
Beyond the protocol conversion required by Rule 2.3, a client may apply additional normalization to relay URLs before use:
- Lowercase the hostname
- Strip trailing slashes from the path
- Infer the protocol when absent: loopback addresses (
localhost,127.0.0.1) usehttp://; all others usehttps:// - Accept bare hostnames without a protocol prefix as valid input
Normalization table:
| Input | Normalized |
|---|---|
WSS://Relay.Example.COM/ |
wss://relay.example.com |
https://relay.example.com |
wss://relay.example.com |
relay.example.com |
wss://relay.example.com |
localhost:8080 |
ws://localhost:8080 |
Normalization must be idempotent — applying it multiple times must produce the same result.
normalize(normalize(normalize("WSS://Relay.Example.COM/")))
// => "wss://relay.example.com"
Optional: Request Optimization
Optional Rule 10.1: Batched Fetching
Rather than issuing one HTTP request per relay, a client may accumulate multiple relay URLs over a short time window and dispatch them together in a single batch request to a backend service. This reduces request count when many relay connections are opened in quick succession, at the cost of a small additional latency for each individual relay.
pending_urls = []
load_document(url):
pending_urls.push(url)
schedule_batch_flush() // debounced, fires after window closes
flush_batch():
urls = pending_urls.splice(0)
results = await batch_fetch_service.post("/relay/info", { urls })
for { url, document } in results:
registry.set(url, document)
Optional Rule 10.2: Intermediary Fetching
A client may route requests through an intermediary service rather than fetching relay documents directly. The client sends a list of relay URLs to the intermediary, which retrieves the documents on the client’s behalf and returns them in a single response.
This shifts the fetch work server-side and can improve reliability in client environments where direct outbound HTTP connections are constrained.
Optional: Relay-Side Live Field Values
Optional Rule 11.1: Runtime Authentication State
A relay may determine the value of limitation.auth_required at request time by reading from its live configuration rather than from a value fixed at startup. This ensures the field accurately reflects the relay’s current state if authentication requirements are changed at runtime without a restart.
Optional Rule 11.2: Build-Time Version Injection
A relay may populate the version field from build-time metadata (such as a version string injected at compile time) and apply this value in preference to any version string stored in a configuration file. This guarantees the advertised version always matches the running binary.
Optional Rule 11.3: Capability-Driven supported_nips
A relay may derive the contents of supported_nips by introspecting which protocol handlers are actually registered at runtime, rather than maintaining a manually curated static list. This prevents the advertised capabilities from drifting out of sync with the implementation.
build_supported_nips(relay):
nips = [1, 11] // always supported
if relay.delete_handler is registered:
nips.add(9)
if relay.count_handler is registered:
nips.add(45)
if relay.negentropy_enabled:
nips.add(77)
return nips
Optional: Multiple Relay Identities
Optional Rule 12.1: Per-Path Relay Documents
A single server process may host multiple distinct relay identities, each mounted at a different URL path and each serving its own independent information document. Each identity has its own name, pubkey, description, and other fields, and is independently addressable as a relay.
Example:
GET / Accept: application/nostr+json → outbox relay document
GET /inbox Accept: application/nostr+json → inbox relay document
GET /private Accept: application/nostr+json → private relay document
Looking for comments…
Searching Nostr relays. This may take a moment the first time this article is opened.
Looking for comments…
Searching Nostr relays. This may take a moment the first time this article is opened.