The deployed nostrnests reference (NestsUI v2 + moq-auth + moq-rs)
emits a different on-the-wire schema than the previous EGG-01 / EGG-09
drafts and Quartz writers. Verified by reading the production
NestsUI bundle. The deployed schema is now canonical:
kind:30312 (room event)
- relay URL → ["streaming", url] (was ["endpoint", url])
- auth URL → ["auth", url] (was ["service", url])
- live status → ["status", "live"] (was "open")
- ended status → ["status", "ended"] (was "closed")
- room name → ["title", name] (was ["room", name])
kind:10112 (user MoQ-server list)
- 3-element entries: ["server", relay, auth]
- first-element name "relay" accepted as a synonym (legacy)
- 2-element entries: derive auth host by replacing leading "moq."
with "moq-auth.", or prepending "moq-auth." otherwise
Implementation
- ServiceUrlTag.TAG_NAME = "auth", LEGACY_TAG_NAME = "service"
- EndpointUrlTag.TAG_NAME = "streaming", LEGACY_TAG_NAME = "endpoint"
- StatusTag enum: PLANNED/LIVE/PRIVATE/ENDED with code "live"/"ended";
legacy "open"/"closed" still parsed on read
- NestsServersEvent: emit/read 3-element [server, relay, auth] tags
with the legacy-shape tolerances above; expose a NestsServer pair
- Account.nestsServers.flow now produces List<NestsServer> pairs
- NestsServersScreen: rewritten edit-field asks for both URLs,
recommended row shows the nostrnests pair, list rows show both URLs
- NestsScreen first-time setup writes the nostrnests pair, not a
single URL; gate the create FAB on both URLs being parseable
- CreateNestViewModel: drop the resolveServerPair hardcoded mapping —
the saved pair is now authoritative; defaults stay correct for an
empty kind-10112 list
Specs
- EGG-01 rewritten to canonical streaming/auth/live/ended with a
legacy-spelling table; example uses the real nostrnests pair
- EGG-02 references "auth" tag throughout; error taxonomy says "ended"
- EGG-09 rewritten to 3-element server tag with derivation fallback
- New nestsClient/specs/nip-53-proposed.md — a single self-contained
proposed update to upstream NIP-53 covering kind 30312 + kind 10112
with the deployed schema and the JWT-mint flow
All existing unit tests adjusted; quartz:jvmTest, amethyst:testPlayDebugUnitTest,
and nestsClient:jvmTest pass.
EGGs — Extensible Gossip Guidelines
Wire-protocol specs for nostrnests-style audio rooms. Each EGG defines one self-contained capability that a client or relay can implement. Two compliant peers that implement the same set of EGGs round-trip without further coordination.
Specs intentionally mirror the Nostr NIP and Blossom BUD formats: ASCII Markdown, RFC-2119 keywords (MUST / SHOULD / MAY), one capability per file.
Index
| # | File | Topic | Category |
|---|---|---|---|
| 01 | EGG-01 | Room event (kind:30312) |
required |
| 02 | EGG-02 | Auth & WebTransport handshake | required |
| 03 | EGG-03 | Audio plane (moq-lite) | required |
| 04 | EGG-04 | Presence (kind:10312) |
required |
| 05 | EGG-05 | In-room chat (kind:1311) |
optional |
| 06 | EGG-06 | Reactions (kind:7) |
optional |
| 07 | EGG-07 | Roles & moderation (kind:4312) |
optional |
| 08 | EGG-08 | Scheduling (status=planned) |
optional |
| 09 | EGG-09 | User server list (kind:10112) |
optional |
| 10 | EGG-10 | Theming (c/f/bg) |
decorative |
| 11 | EGG-11 | Recording (recording tag) |
decorative |
| 12 | EGG-12 | Catalog track (catalog.json) |
optional |
Conformance levels
A peer claims Listener compliance when it implements EGG-01, EGG-02, EGG-03 and at least the read side of EGG-04.
A peer claims Speaker compliance when it implements all of the above plus the publish side of EGG-03 and the write side of EGG-04.
A peer claims Host compliance when it implements Speaker compliance plus EGG-07 and the write side of EGG-08 (if scheduled rooms are exposed in its UI).
EGG-05 through EGG-12 are independently optional. Lacking any of them MUST NOT break interop on the EGGs a peer does implement.
Versioning
Each spec carries a status line at the top: draft (subject to change),
accepted (frozen except for clarifications), or replaced-by: EGG-XX
(superseded). Breaking changes ship as a new EGG number; existing numbers are
never re-purposed.
Naming
EGG = Extensible Gossip Guideline. The acronym is intentional: nostrnests serves nests; nests hold eggs.
Conventions
Rules in this section apply across every EGG. They are normative.
- Hex strings. Every pubkey, event id, signature, and other hex value
referenced in any EGG MUST be lowercase, exactly the spec's expected
length (64 chars for pubkey/event id, 128 chars for sig), with NO
0xprefix and NO whitespace. Receivers MUST reject mixed-case or padded hex rather than silently lowercasing — an inconsistent hex on the wire is a bug at the source. - Nostr foundations. Unless an EGG says otherwise, every event referenced
here is a NIP-01 event (id = sha256 of the canonical serialization;
schnorr signature over
id). Receivers MUST verifyidandsigbefore acting on the event. Replaceable / addressable / ephemeral semantics follow NIP-01. a-tag form. Every["a", ...]tag in these specs uses the addressable form<kind>:<author pubkey hex>:<d-tag>. The optional 3rd element is a relay hint URL.- Created-at tie-break. When two events share the same
(kind, pubkey, d)AND the samecreated_at, receivers MUST keep the one with the lexicographically SMALLEST event id and discard the other (matches NIP-01 replaceable-event tie-break behavior). - JSON. All HTTP bodies and event content are UTF-8. JSON producers MUST NOT include a BOM. JSON parsers MUST tolerate trailing whitespace.
- Time. All timestamps are unsigned unix seconds (base-10 ASCII when carried as a tag value, integer when carried in JSON). Sub-second precision is out of scope.
Joining sequence
A normative end-to-end walkthrough for "I have a kind:30312 event in hand,
get me to first audio frame". Every step references the EGG that defines it.
┌──────────────────────────────────────────────────────────────────────┐
│ 1. Parse room event (EGG-01) │
│ - Verify NIP-01 id + sig │
│ - Read `service`, `endpoint`, `relays`, `status`, `p` tags │
│ - Reject if status != "open" / "private" │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 2. Subscribe to chat / presence relays (EGG-04, EGG-05, EGG-06) │
│ Filter: { "#a": ["30312:<host>:<d>"], "kinds":[1311,7,4312] } │
│ Filter: { "#a": ["30312:<host>:<d>"], "kinds":[10312] } │
│ Relays = `relays` tag ∪ host's NIP-65 outbox │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 3. Mint moq-auth JWT (EGG-02) │
│ POST <service>/auth │
│ Authorization: Nostr <base64(NIP-98 event)> │
│ body: { "namespace": "nests/30312:<host>:<d>", │
│ "publish": <true if speaker, else false> } │
│ ← 200 { "token": "<es256 jwt>" } │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 4. Open WebTransport session (EGG-02) │
│ CONNECT :path = /<namespace>?jwt=<token> │
│ :authority = host:port from `endpoint` │
│ Run moq-lite Setup handshake on the bidi control stream │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 5. Discover speakers (EGG-03) │
│ Send moq-lite AnnouncePlease prefix="" │
│ Each Announce { suffix=<speaker pubkey hex>, status=Active } │
│ is one live speaker. │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 6. Subscribe per speaker (EGG-03) │
│ Subscribe { broadcast=<pubkey hex>, track="audio/data" } │
│ Each Group → unidirectional QUIC stream │
│ Each Frame → exactly one 20 ms Opus packet │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 7. Publish own presence (EGG-04) │
│ kind:10312 with `a` = "30312:<host>:<d>", flags, fixed d-tag │
│ Re-publish every 30 s (with ±5 s jitter) until leaving │
└──────────────────────────────────────────────────────────────────────┘
│
▼
(speaker only)
┌──────────────────────────────────────────────────────────────────────┐
│ 8. Speaker — publish own audio (EGG-03) │
│ Send moq-lite Announce { suffix=<own pubkey hex>, status=Active } │
│ Open one unidirectional QUIC stream per Group, write Frames │
└──────────────────────────────────────────────────────────────────────┘
Token lifetime is 600 s. Re-mint a fresh token (steps 3 + 4) before the
old one expires. Relays MUST close sessions within 30 s past exp.
Hosting a new room
A normative walkthrough for "I am a host; create a fresh room and start
broadcasting". Symmetrical to the joining sequence above but starts one
step earlier — the host has to bring the kind:30312 into existence
before anyone (including themselves) can authenticate against it.
┌──────────────────────────────────────────────────────────────────────┐
│ 1. Pick service + endpoint (EGG-01, EGG-09) │
│ Default: FIRST entry of host's own kind:10112 (EGG-09) │
│ Both MUST be https://; http:// is rejected (EGG-09 rule 1) │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 2. Compose kind:30312 (EGG-01) │
│ - status = "open" (or "planned" + starts tag, EGG-08) │
│ - d = fresh room id, charset [A-Za-z0-9._-] (EGG-01 rule 9) │
│ - first p-tag = ["p", <own pubkey>, <relay hint>, "host"] │
│ - relays / image / summary / theme tags as desired │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 3. Sign + publish to relays (EGG-01 rule 10) │
│ Target set = `relays` tag ∪ host's NIP-65 outbox │
│ The auth sidecar's inbound relay pool MUST overlap this set, │
│ otherwise step 4 will 410 `unknown_room` indefinitely. │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 4. Mint publish JWT (EGG-02) │
│ POST <service>/auth, body = {namespace, "publish": true} │
│ NIP-98 Authorization signed by host pubkey. │
│ On 410 `unknown_room`: relay-propagation lag — retry up to 3 │
│ times with backoff (1s / 2s / 4s) before surfacing. │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 5. Open WebTransport + run moq-lite Setup (EGG-02 step 3) │
│ Same as listener flow. │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 6. Announce own broadcast (EGG-03) │
│ Announce { suffix=<own pubkey hex>, status=Active, hops=0 } │
│ Then AnnouncePlease prefix="" to also receive other speakers. │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 7. Publish own presence (EGG-04) │
│ publishing="1", onstage="1", muted="0"; heartbeat per EGG-04. │
└──────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ 8. Stream Opus (EGG-03) │
│ One unidirectional QUIC stream per Group, one Opus packet per │
│ Frame, 20 ms cadence. │
└──────────────────────────────────────────────────────────────────────┘
Ongoing host duties:
- Add a speaker: re-publish kind:30312 with the new
["p", _, _, "speaker"]tag (EGG-07). - Promote to admin / demote / remove: re-publish kind:30312 with updated role markers (EGG-07).
- Kick: sign and broadcast a kind:4312 ephemeral (EGG-07).
- Edit metadata (rename, image, theme): re-publish kind:30312 with
higher
created_at(EGG-01 rule 1). - Close the room: re-publish kind:30312 with
status="closed"(EGG-01 rule 6). The audio plane SHOULD be torn down server-side. - Publish recording after close: re-publish closed event with
["recording", url]tag (EGG-11).
Filing changes
Edit a single EGG per pull request. Include a wire-format example showing the delta. If a change crosses two specs, file two PRs and reference each from the other.