{"id":"18ddbd1bc6ab6b3370623ba3c6b0a5c04278f98fef6feb27e0e5c6fbe5e4fbed","pubkey":"7ed7d5c3abf06fa1c00f71f879856769f46ac92354c129b3ed5562506927e200","created_at":1777737358,"kind":30817,"tags":[["d","relay-information-document-specification"],["title","Relay Information Document Specification"],["client","nostrhub.io"]],"content":"# Nostr Relay Information Document Specification\n\n## 1. Overview\n\nA 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.\n\nThe 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.\n\n## 2. Retrieving the Document\n\n### Rule 2.1: HTTP Request\n\nThe client retrieves the document by sending an HTTP `GET` request to the relay's base URL with the following header:\n\n```\nAccept: application/nostr+json\n```\n\n**Example:**\n\n```\nGET / HTTP/1.1\nHost: relay.example.com\nAccept: application/nostr+json\n```\n\n### Rule 2.2: Accept Header Matching\n\nThe 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.\n\n```\nrequest.header(\"Accept\") == \"application/nostr+json\"\n```\n\nRequests 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.\n\n### Rule 2.3: URL Protocol Conversion\n\nRelay URLs use the WebSocket protocol (`wss://` or `ws://`). Before making the HTTP request, the client converts the URL to its HTTP equivalent:\n\n```\nwss://  →  https://\nws://   →  http://\n```\n\nThe host, path, port, and query string are left unchanged.\n\n**Example:**\n\n```\nwss://relay.example.com      →  https://relay.example.com\nws://localhost:8080/nostr    →  http://localhost:8080/nostr\n```\n\n### Rule 2.4: Multiplexing with WebSocket\n\nThe relay serves both its WebSocket endpoint and the information document from the same base URL. Dispatch is determined by request headers:\n\n```\non GET /:\n  if Upgrade == \"websocket\":\n    → handle WebSocket connection\n  else if Accept == \"application/nostr+json\":\n    → serve information document\n  else:\n    → serve fallback response (landing page, redirect, etc.)\n```\n\n## 3. Response Format\n\n### Rule 3.1: Status and Headers\n\nA successful response must include:\n\n```\nHTTP/1.1 200 OK\nContent-Type: application/nostr+json\nAccess-Control-Allow-Origin: *\n```\n\nThe `Access-Control-Allow-Origin: *` header is required so that browser-based clients can fetch the document from any origin without CORS restrictions.\n\n### Rule 3.2: Body\n\nThe response body is a single JSON object. All fields are optional. Clients must not treat a missing or null field as an error.\n\n**Example response:**\n\n```json\n{\n  \"name\": \"My Relay\",\n  \"description\": \"A public Nostr relay.\",\n  \"pubkey\": \"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d\",\n  \"contact\": \"mailto:admin@example.com\",\n  \"supported_nips\": [1, 2, 9, 11, 40],\n  \"software\": \"https://github.com/example/myrelay\",\n  \"version\": \"1.4.2\",\n  \"limitation\": {\n    \"max_message_length\": 131072,\n    \"max_subscriptions\": 20,\n    \"auth_required\": false,\n    \"payment_required\": false\n  }\n}\n```\n\n### Rule 3.3: Unknown Fields\n\nClients 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.\n\n## 4. Document Fields\n\n### Rule 4.1: Identity Fields\n\nThese fields describe the relay and its operator.\n\n|Field|Type|Description|\n|---|---|---|\n|`name`|string|Human-readable name for the relay|\n|`description`|string|Description of the relay's purpose or audience|\n|`pubkey`|string|Relay operator's public key, lowercase hex-encoded|\n|`contact`|string|Contact address for the operator (e.g. `mailto:` or `https:`)|\n|`icon`|string|URL to a square image representing the relay|\n|`banner`|string|URL to a banner image for the relay|\n\nThe `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.\n\n### Rule 4.2: Software Fields\n\nThese fields identify the relay implementation.\n\n|Field|Type|Description|\n|---|---|---|\n|`software`|string|URL or identifier for the relay software|\n|`version`|string|Version string of the relay software|\n\n### Rule 4.3: Capability Fields\n\nThese fields declare what protocol features the relay supports.\n\n|Field|Type|Description|\n|---|---|---|\n|`supported_nips`|array of numbers|Protocol feature numbers this relay implements|\n\nClients should consult `supported_nips` before using protocol features that require explicit relay support.\n\nBecause 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.\n\n### Rule 4.4: Limitation Fields\n\nThe optional `limitation` object describes constraints the relay enforces on clients. All subfields are optional.\n\n**Access control:**\n\n|Field|Type|Description|\n|---|---|---|\n|`auth_required`|boolean|Whether authentication is required before any action|\n|`payment_required`|boolean|Whether payment is required before any action|\n|`restricted_writes`|boolean|Whether some write condition must be fulfilled|\n\n**Size and throughput limits:**\n\n|Field|Type|Description|\n|---|---|---|\n|`max_message_length`|integer|Maximum byte length of any incoming WebSocket message|\n|`max_subscriptions`|integer|Maximum concurrent subscriptions per connection|\n|`max_filters`|integer|Maximum filters per subscription request|\n|`max_limit`|integer|Maximum value of the `limit` filter field|\n|`max_subid_length`|integer|Maximum character length of a subscription ID|\n|`min_prefix`|integer|Minimum hex prefix length for ID and pubkey filters|\n|`max_event_tags`|integer|Maximum number of tags on a single event|\n|`max_content_length`|integer|Maximum character length of an event's `content` field|\n\n**Proof-of-work:**\n\n|Field|Type|Description|\n|---|---|---|\n|`min_pow_difficulty`|integer|Minimum proof-of-work difficulty required on incoming events|\n\n**Event validity bounds:**\n\n|Field|Type|Description|\n|---|---|---|\n|`created_at_lower_limit`|integer|Earliest Unix timestamp accepted in `created_at`|\n|`created_at_upper_limit`|integer|Latest Unix timestamp accepted in `created_at`|\n\n**Example:**\n\n```json\n{\n  \"limitation\": {\n    \"max_message_length\": 65536,\n    \"max_subscriptions\": 10,\n    \"max_filters\": 5,\n    \"max_limit\": 1000,\n    \"max_subid_length\": 128,\n    \"min_prefix\": 4,\n    \"max_event_tags\": 2500,\n    \"max_content_length\": 8192,\n    \"min_pow_difficulty\": 0,\n    \"auth_required\": false,\n    \"payment_required\": true,\n    \"restricted_writes\": false,\n    \"created_at_lower_limit\": 1577836800,\n    \"created_at_upper_limit\": 9999999999\n  }\n}\n```\n\n### Rule 4.5: Policy and Community Fields\n\n|Field|Type|Description|\n|---|---|---|\n|`relay_countries`|array of strings|ISO 3166-1 alpha-2 country codes indicating the relay's jurisdiction or audience|\n|`language_tags`|array of strings|BCP-47 language tags for the relay's primary languages, in preference order|\n|`tags`|array of strings|Arbitrary descriptive labels (e.g. `\"bitcoin-only\"`, `\"sfw-only\"`)|\n|`posting_policy`|string|URL to a human-readable document describing the relay's posting rules|\n|`privacy_policy`|string|URL to the relay's privacy policy|\n|`terms_of_service`|string|URL to the relay's terms of service|\n\n### Rule 4.6: Payment Fields\n\n|Field|Type|Description|\n|---|---|---|\n|`payments_url`|string|URL to a page with payment or subscription details|\n|`fees`|object|Structured fee schedule (see Rule 4.7)|\n\n### Rule 4.7: Fee Schedule\n\nThe `fees` object groups fees into three categories:\n\n```json\n{\n  \"fees\": {\n    \"admission\": [\n      { \"amount\": 1000000, \"unit\": \"msats\" }\n    ],\n    \"subscription\": [\n      { \"amount\": 5000, \"unit\": \"msats\", \"period\": 2592000 }\n    ],\n    \"publication\": [\n      { \"amount\": 100, \"unit\": \"msats\", \"kinds\": [1, 6, 7] }\n    ]\n  }\n}\n```\n\nEach fee entry contains:\n\n|Field|Type|Description|\n|---|---|---|\n|`amount`|integer|The cost|\n|`unit`|string|The currency denomination (e.g. `\"msats\"`)|\n|`period`|integer|For subscription fees, the billing period in seconds|\n|`kinds`|array of integers|For publication fees, the event kinds this fee applies to; absent means all kinds|\n\n### Rule 4.8: Retention Fields\n\nThe `retention` array describes how long or how many events the relay stores, optionally scoped to specific event kinds:\n\n```json\n{\n  \"retention\": [\n    { \"kinds\": [0, 1, 3], \"time\": 86400 },\n    { \"kinds\": [[10000, 19999]], \"count\": 1000 },\n    { \"time\": 3600, \"count\": 10000 }\n  ]\n}\n```\n\nEach entry contains:\n\n|Field|Type|Description|\n|---|---|---|\n|`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|\n|`time`|integer or null|Seconds to retain events; `null` means retain forever; `0` means events of this kind are not stored|\n|`count`|integer or null|Maximum number of events retained|\n\n## 5. Failure Handling\n\n### Rule 5.1: Failed Fetch\n\nIf 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.\n\n### Rule 5.2: Missing Fields\n\nClients 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.\n\n### Rule 5.3: Partial Documents\n\nA relay may return a document containing only a subset of known fields. Clients must process whatever fields are present and ignore the rest.\n\n---\n\n## Optional Features\n\nThe following behaviors are implemented by some clients and relays but are not required.\n\n## Optional: Fetch Triggering Strategies\n\n### Optional Rule 6.1: Automatic Fetch on Connection\n\nA 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.\n\n```\non_relay_connected(relay_url):\n  fetch_information_document(relay_url)\n  // continues in background; does not block connection use\n```\n\n### Optional Rule 6.2: Lazy Fetch on Demand\n\nA client implementation may defer the fetch until something explicitly requests the document. Establishing a connection does not trigger a request.\n\n```\nget_relay_information(relay_url):\n  if not already_fetching(relay_url):\n    start_fetch(relay_url)\n  return await fetch_result(relay_url)\n```\n\n## Optional: Caching\n\n### Optional Rule 7.1: Per-Relay Lifetime Cache\n\nA 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.\n\n```\nfetch_information_document(url):\n  if cache.has(url):\n    return cache.get(url)  // immediate, no network request\n\n  result = await http_get(url)\n  cache.set(url, result)\n  return result\n```\n\n### Optional Rule 7.2: Application-Wide Registry\n\nA 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.\n\n```\nregistry = Map<url, RelayDocument>\n\nload_document(url):\n  if registry.has(url):\n    return  // already loaded\n\n  doc = await fetch_information_document(url)\n  registry.set(url, doc)\n```\n\n## Optional: Timeouts\n\n### Optional Rule 8.1: Automatic Timeout\n\nA 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.\n\nRecommended timeout range: 7–10 seconds.\n\n### Optional Rule 8.2: Caller-Supplied Deadline\n\nA 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.\n\n```\nfetch_information_document(url, deadline=null):\n  effective_deadline = deadline ?? now() + DEFAULT_TIMEOUT\n  return http_get_with_deadline(url, effective_deadline)\n```\n\n## Optional: URL Canonicalization\n\n### Optional Rule 9.1: Full Canonicalization\n\nBeyond the protocol conversion required by Rule 2.3, a client may apply additional normalization to relay URLs before use:\n\n- Lowercase the hostname\n- Strip trailing slashes from the path\n- Infer the protocol when absent: loopback addresses (`localhost`, `127.0.0.1`) use `http://`; all others use `https://`\n- Accept bare hostnames without a protocol prefix as valid input\n\n**Normalization table:**\n\n|Input|Normalized|\n|---|---|\n|`WSS://Relay.Example.COM/`|`wss://relay.example.com`|\n|`https://relay.example.com`|`wss://relay.example.com`|\n|`relay.example.com`|`wss://relay.example.com`|\n|`localhost:8080`|`ws://localhost:8080`|\n\nNormalization must be idempotent — applying it multiple times must produce the same result.\n\n```\nnormalize(normalize(normalize(\"WSS://Relay.Example.COM/\")))\n// => \"wss://relay.example.com\"\n```\n\n## Optional: Request Optimization\n\n### Optional Rule 10.1: Batched Fetching\n\nRather 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.\n\n```\npending_urls = []\n\nload_document(url):\n  pending_urls.push(url)\n  schedule_batch_flush()  // debounced, fires after window closes\n\nflush_batch():\n  urls = pending_urls.splice(0)\n  results = await batch_fetch_service.post(\"/relay/info\", { urls })\n  for { url, document } in results:\n    registry.set(url, document)\n```\n\n### Optional Rule 10.2: Intermediary Fetching\n\nA 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.\n\nThis shifts the fetch work server-side and can improve reliability in client environments where direct outbound HTTP connections are constrained.\n\n## Optional: Relay-Side Live Field Values\n\n### Optional Rule 11.1: Runtime Authentication State\n\nA 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.\n\n### Optional Rule 11.2: Build-Time Version Injection\n\nA 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.\n\n### Optional Rule 11.3: Capability-Driven supported_nips\n\nA 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.\n\n```\nbuild_supported_nips(relay):\n  nips = [1, 11]  // always supported\n  if relay.delete_handler is registered:\n    nips.add(9)\n  if relay.count_handler is registered:\n    nips.add(45)\n  if relay.negentropy_enabled:\n    nips.add(77)\n  return nips\n```\n\n## Optional: Multiple Relay Identities\n\n### Optional Rule 12.1: Per-Path Relay Documents\n\nA 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.\n\n**Example:**\n\n```\nGET /          Accept: application/nostr+json  →  outbox relay document\nGET /inbox     Accept: application/nostr+json  →  inbox relay document\nGET /private   Accept: application/nostr+json  →  private relay document\n```","sig":"63920ebbe0c44f485c79407630414a62859ceb31815548ddc1ee6c3e3df6a4bfc40cee8c14a9c08fc499fda00c5f67c2a0e0305b701ca477a8a67ac4605923f5"}