Captures the gap between NIP-53's room discovery and what audio-capable
clients/servers must actually agree on to interop. NIP-53 defines the
30312 event but leaves the HTTP control plane + MoQ namespacing +
audio codec params entirely to individual implementations. With Nests
going generic-server, this is the moment to standardize.
What the draft covers:
1. HTTP control plane:
- Path convention: GET <service>/<d-tag>
- NIP-98 `Authorization: Nostr <base64>` header required
- JSON response shape (endpoint, token, transport, codec,
sample_rate, frame_duration_ms, moq_version)
- Canonical error-status map (401/403/404/410/503)
2. WebTransport + MoQ handshake requirements (Extended CONNECT,
required HTTP/3 settings, Bearer token passing).
3. MoQ track naming — vendor-neutral one-element namespace
`[<d-tag>]` with track-name = speaker-pubkey-hex. Explicit
rejection of the `["nests", <d-tag>]` prefix for new deployments.
4. Audio object format: raw Opus packets (no Ogg, no TOC), 48 kHz
mono, 20 ms default, mono PCM 16-bit decode target. Both
OBJECT_DATAGRAM and STREAM_HEADER_SUBGROUP accepted; listeners
MUST handle both.
5. Per-track access control: server MUST verify publishing pubkey is
a host/speaker in the current 30312 event (which is replaceable —
revocation cascade is spec'd).
6. Leave procedure (UNSUBSCRIBE, UNANNOUNCE, final 10312 presence,
WT_CLOSE_SESSION capsule).
7. Presence extension: the `["muted", "1"|"0"]` tag we already ship
in nestsClient's MeetingRoomPresenceEvent overload, promoted from
Amethyst-specific to NIP-defined.
8. Server + client requirements summaries.
9. Known divergences from current nostrnests/nests servers (two-
element `["nests", <d-tag>]` namespace + `/api/v1/nests/<d-tag>`
path) with a transition strategy via a `"nip_xx": true` flag in
room-info responses.
10. Security considerations (bearer-token handling, NIP-98 `u` tag
binding, audio-replay attack surface, server-impersonation VU
meter recommendation).
Deliberately out of scope: E2E-signed audio objects (future NIP),
federation between audio servers, room recording/transcription.
Intended workflow: share with the Nests team while they're designing
the generic server, land changes against their feedback, then open a
PR on nostr-protocol/nips.
https://claude.ai/code/session_013nVLALALKaHVgHm9u5Cg8D
13 KiB
NIP-XX — Interactive Audio Rooms Join Protocol
draft optional
Abstract
This NIP specifies the client/server control plane and real-time audio
transport for joining a NIP-53 Meeting Space (kind 30312) as a listener
or speaker. It closes the gap between NIP-53's room discovery (which
defines only what a room is) and what an audio-capable client must do
to actually hear and speak in it.
A standard, vendor-neutral profile lets multiple server implementations interoperate with multiple clients. A client that follows this NIP will work against any server that advertises compliance, and vice versa.
Dependencies
- NIP-53: Live Activities
defines kind
30312(Meeting Space), kind10312(Room Presence), and kind1311(Live Activity Chat). - NIP-98: HTTP Auth
defines kind
27235and theAuthorization: Nostr <base64-event>header. - IETF
moq-transportand IETFwebtransport-http3provide the audio transport substrate.
Terminology
- Host — the pubkey that signed the kind
30312event. - Speaker — any pubkey listed in the
30312event'sptags with rolehostorspeaker. - Listener — any other participant (including un-tagged members of the audience).
- MoQ endpoint — the WebTransport URL a server returns from its room-info HTTP endpoint; where audio flows once the session is established.
Event kinds used
Kind 30312 (already defined by NIP-53)
Clients and servers compliant with this NIP MUST honour these tags:
| Tag | Meaning | Required |
|---|---|---|
d |
Room identifier. The "room id" everywhere else in this spec. | yes |
room |
Human-readable display name. | recommended |
summary |
Short description. | optional |
image |
Room cover URL. | optional |
status |
open | private | closed. |
yes |
service |
HTTPS base URL of a server implementing the HTTP control plane defined below. | yes |
streaming |
Present for non-audio streams (e.g. wss+livekit://…, HLS). MAY be omitted for pure audio rooms — in that case audio flows via the MoQ endpoint returned by the service URL. |
optional for audio rooms |
p |
["p", <pubkey>, <relay-hint>?, <role>?, <proof>?] — role is host or speaker. |
one host required |
A client MUST reject a room whose service URL is not HTTPS.
Kind 10312 (Room Presence) — extended
NIP-53 defines this event with an optional ["hand", "1"|"0"] tag. This NIP
adds a second optional tag:
| Tag | Meaning |
|---|---|
["muted", "1"] or ["muted", "0"] |
The participant's microphone is muted (1) or hot (0) at the time of publish. Absent means unspecified. |
Servers and peers MUST NOT rely on muted for authorization — it's a UI
signal, not an access control. The server still enforces who is allowed to
publish audio via the MoQ session.
Kind 1311 (Live Activity Chat)
Used per NIP-53 with no changes. Each room's chat is scoped by the a tag
pointing at the 30312 event.
HTTP control plane
Base URL
The service tag from the kind 30312 event is the base URL. All control-
plane requests are constructed as:
GET <service>/<room-d-tag> — room-info / join
where <room-d-tag> is the d tag value of the kind 30312 event,
URL-path-encoded.
The server MAY expose additional paths under the same base (e.g.
<service>/ for server metadata, <service>/.well-known/nostr-audio-rooms
for discovery). This NIP only specifies <service>/<room-d-tag>.
Authentication
Every request MUST carry a NIP-98 Authorization header:
Authorization: Nostr <base64(kind-27235-event)>
The kind 27235 event MUST have:
["u", "<fully-qualified-URL-being-requested>"]["method", "GET"](orPOST,DELETE, etc. matching the request verb)created_atwithin 60 s of the server's clock- A valid signature
The server SHOULD reject requests older than 60 s with 401 Unauthorized.
Servers MAY also reject requests whose signer isn't allowed in the room
(e.g. the room is private and the signer isn't on the allow-list).
Join / room-info response
GET <service>/<room-d-tag> with a valid NIP-98 header returns a JSON body
with Content-Type: application/json:
{
"endpoint": "https://relay.example.com:4443/moq",
"token": "eyJhbGciOi…",
"transport": "webtransport",
"codec": "opus",
"sample_rate": 48000,
"frame_duration_ms": 20,
"moq_version": "draft-17"
}
Fields:
| Field | Type | Required | Meaning |
|---|---|---|---|
endpoint |
string (https URL) | yes | WebTransport URL the client connects to. |
token |
string | yes | Opaque bearer token the client passes to the WebTransport layer (see below). |
transport |
string | no, defaults to "webtransport" |
Reserved for future transports. Clients MUST fail closed on unknown values. |
codec |
string | no, defaults to "opus" |
Audio codec name. Reserved for future codecs. Clients MUST fail closed on unknown values. |
sample_rate |
integer | no, defaults to 48000 |
Samples per second. |
frame_duration_ms |
integer | no, defaults to 20 |
Audio frame duration. |
moq_version |
string | no, defaults to server's preferred | Identifier of the MoQ-transport draft the server speaks (e.g. "draft-17"). Clients MUST include this version (and nothing else) in CLIENT_SETUP. |
Unknown fields MUST be ignored by the client to allow forward-compatible server extensions.
Error responses are standard HTTP status codes with an optional JSON body
{"error": "<short-code>", "reason": "<human-readable>"}:
| Status | When |
|---|---|
401 |
NIP-98 missing, expired, or signature invalid. |
403 |
Signer isn't allowed in this room (e.g. room closed, signer blocked). |
404 |
Room d tag unknown to this server. |
410 |
Room has ended. |
503 |
Server is healthy but audio backend is unavailable. |
Audio transport
WebTransport
Clients open a WebTransport session against the endpoint URL using the
Extended CONNECT handshake (RFC 9220
- the WebTransport-HTTP/3 draft). The HTTP CONNECT request MUST carry:
:method: CONNECT
:protocol: webtransport
:scheme: https
:authority: <host[:port] from endpoint>
:path: <path from endpoint>
Authorization: Bearer <token from room-info response>
Servers MUST:
- advertise
SETTINGS_ENABLE_CONNECT_PROTOCOL = 1(RFC 8441), - advertise
SETTINGS_ENABLE_WEBTRANSPORT = 1, - advertise
SETTINGS_H3_DATAGRAM = 1(RFC 9297).
MoQ session
After the WebTransport session is open, the client opens its first
bidirectional stream as the MoQ control stream and sends CLIENT_SETUP
advertising the moq_version from the room-info response. The server replies
with SERVER_SETUP selecting that version, or closes the session.
Track namespace + name
A speaker's audio track is published by the server and subscribed-to by clients under:
track_namespace = [ <room-d-tag> ]
track_name = <speaker-pubkey-hex> (64 lowercase hex chars)
The namespace is a one-element tuple containing the room's d tag. It is
intentionally vendor-neutral: there is no "nests" or server-brand prefix.
Rationale: the namespace is uniquely keyed by the NIP-53 room id and is
sufficient for a client to subscribe without knowing anything about the
server's brand. Multiple servers hosting rooms with the same d tag is
already not a concern — a d tag is unique under a host pubkey, and the
room-info response identifies exactly which server's MoQ endpoint the
client must connect to.
Audio objects
Each OBJECT on a speaker's track carries one Opus packet as its payload:
- Opus encapsulated in raw packet form (no Ogg / no TOC-prefix), as
produced by
libopus/ AndroidMediaCodec("audio/opus")encoder output. sample_ratefrom the room-info response (default 48 000 Hz).frame_duration_msfrom the room-info response (default 20 ms).- Mono (single channel), signed 16-bit PCM domain, VBR encoding.
Object delivery MAY use either:
- OBJECT_DATAGRAM (lowest latency, lossy) — recommended for live audio.
- STREAM_HEADER_SUBGROUP uni-streams (reliable) — MAY be used by servers that need delivery guarantees; clients MUST support receiving both.
group_id on each object is the speaker's monotonic group counter (one
group per speaker session). object_id is the zero-based Opus packet index
within the group. publisher_priority is 0x80 unless the server has
reason to vary it.
Server-to-client vs. client-to-server audio
A listener SUBSCRIBEs to each speaker's track namespace + track name. The server MUST accept SUBSCRIBEs from any authenticated client (subject to its access control).
A speaker ANNOUNCEs [ <room-d-tag> ] and publishes objects on track
name <speaker-pubkey-hex>. The server MUST verify that the announcing
pubkey is listed in the room's p tag set with role host or speaker
at the current moment (the room event is replaceable — the set can
change). On role revocation, the server MUST close the publisher's track.
Leaving
To leave, the client SHOULD:
- UNSUBSCRIBE every track it had open.
- If it was a speaker, send UNANNOUNCE +
SubscribeDonefor its own track. - Publish a final kind
10312presence event (optional, improves UX for other peers) with["muted","1"]and without["hand","1"]. - Close the WebTransport session with capsule type
0x2843(WT_CLOSE_SESSION, code0).
The server SHOULD treat 30 s without a kind 10312 refresh (per NIP-53) as
"left" for UI purposes.
Chat
Per NIP-53 kind 1311. No additions.
Server requirements summary
A server claiming this NIP MUST:
- Accept NIP-98-signed requests at
<service>/<d-tag>and return the JSON shape above on success. - Enforce the
statuson the kind30312event (reject join whenclosed). - Enforce
ptag role for publishers (speakers only). - Run a WebTransport / MoQ endpoint that speaks the
moq_versionadvertised in its room-info response. - Publish each speaker's Opus audio on track
[<d-tag>] / <speaker-pubkey-hex>and accept SUBSCRIBEs from any authenticated listener. - Not require any fields beyond those listed in this NIP for basic interop (but MAY include additional fields in its JSON response for server-specific features; clients ignore unknown fields).
Client requirements summary
A client claiming this NIP MUST:
- Resolve the
serviceURL via NIP-98 GET before opening a transport. - Establish WebTransport against the returned
endpointwith the returnedtokenas a Bearer header. - Run the MoQ handshake at the returned
moq_version. - SUBSCRIBE to one track per
host+speakerpubkey listed in the room event. - Decode Opus per the returned
sample_rate/frame_duration_ms. - Publish kind
10312presence no less frequently than every 30 s while joined, with["muted", …]reflecting the local user's microphone state and["hand", "1"]when requesting to speak. - Fail closed on unknown
transport, unknowncodec, or missingendpointfields.
Reference implementation
A compliant Kotlin/Android reference client is in development at
amethyst/nestsClient — see docs/plans/2026-04-22-pure-kotlin-quic-webtransport-plan.md
for the transport work-in-progress.
Known divergences from current nostrnests/nests servers
At the time of writing, existing nostrnests/nests deployments use:
- MoQ track namespace
[ "nests", <d-tag> ](two elements,"nests"prefix). This NIP specifies[ <d-tag> ](one element). Existing nests servers SHOULD accept both for a transition period; new deployments SHOULD use the one-element form. - A
/api/v1/nests/<d-tag>path convention. This NIP leaves the path entirely to theservicetag — servers are free to pick any path.
A "compliance" phase is proposed where a server can advertise
"nip_xx": true in its room-info JSON response to signal it implements
this NIP verbatim. Clients MAY use that flag to choose the vendor-neutral
namespace when present, and fall back to the legacy nests convention when
absent.
Security considerations
- The
tokenreturned by the server is a bearer token. Clients MUST NOT log it. Servers MUST scope it to a single room + pubkey and SHOULD expire it within the room's lifetime. - The server MUST validate the NIP-98 event's
utag matches the exact request URL, preventing token reuse across rooms. - Audio published by a pubkey
Pon this server can be replayed by a malicious peer claiming to bePon a different server — the listener has no signature on the Opus packets themselves. If end-to-end authenticity matters, a future NIP can extend this by signing each object with the speaker's Nostr key; this NIP leaves that out of scope. - A malicious server can serve silent audio while presence indicators show a speaker talking. Clients SHOULD expose signal-level metering (VU meter) so users can spot this.
Discussion
Send feedback to the author via Nostr or GitHub issues on the reference client repository.