Edits across all 13 EGGs to remove implementer guesswork. After this an
implementer can build a listener and speaker without reading our source.
README:
- Conventions section: hex casing rule (lowercase, 64 chars, no 0x),
NIP-01 foundations, `a`-tag form, created_at tie-break, JSON / time.
- Joining sequence: numbered end-to-end walkthrough from `kind:30312`
to first audio frame, referencing each EGG.
EGG-01 (room event):
- `relays` tag is one tag with multiple values (not multi-tag).
- `service` URL trailing-slash normalization.
- `private` status: gate is implementation-defined, not "render as open".
- `d`-tag charset locked to [A-Za-z0-9._-] so it interpolates safely
into the moq-auth namespace.
- Relay-discovery rule: publish to `relays` tag ∪ NIP-65 outbox.
- Tie-break on identical created_at via smallest event id.
EGG-02 (auth):
- JWT signing pinned: ES256 over P-256, JWKS at /.well-known/jwks.json,
5-minute relay cache.
- NIP-98 tags pinned: u / method / payload, base64 RFC 4648 standard
(not base64url).
- Error taxonomy: full HTTP status + `error` slug table for /auth, plus
WebTransport CONNECT 200/401/403/404 table.
EGG-03 (audio):
- Pin moq-lite Lite-03 to kixelated/moq-rs `rs/moq-lite/src/lite/`.
- One Opus packet per moq Frame (no container, no timestamp).
- Pubkey hex casing reaffirmed at the suffix / broadcast slots.
- AnnouncePlease prefix="" for speaker discovery.
- Mute = stop publishing (not silence frames, not Announce Ended).
- Mid-stream join: discard pre-skip per RFC 7845.
EGG-04 (presence):
- Heartbeat jitter ±5 s required (anti-thundering-herd).
- "0"/"1" are strings, not booleans.
- Single-room rule via replaceable-event semantics.
EGG-05 (chat): 8 KB suggested, 64 KB hard cap, 3 msg/s render rate.
EGG-06 (reactions): 30s window measured against created_at, drop-on-arrival
if already stale.
EGG-07 (moderation): replay protection — dedupe kicks by id within 120 s.
EGG-08 (scheduling): planned rooms MUST 403 at the auth sidecar.
EGG-10 (theming): bg image caps (1 MB / 4096 px soft, 8 MB / 8192 px hard).
Deferred (per review): EGG-00 (Conventions as a standalone spec), EGG-13
(capability advertisement), EGG-14 (discovery), test vectors corpus.
The "Conventions" section in README covers EGG-00's most urgent content
inline.
https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
4.4 KiB
EGG-04: Presence (kind:10312)
status: draft
requires: EGG-01
category: required
Summary
Each participant in a room periodically broadcasts a kind:10312 event
declaring their state: which room they are in, whether their hand is raised,
whether they are muted, whether they are actively pushing audio, and whether
they currently hold a stage seat.
Presence is the source of truth for the audience-side participant grid, the hand-raise queue, the listener counter, and the home-screen "live" bubble.
Wire format
kind:10312 is a NIP-01 replaceable event with a fixed d-tag (one
presence-state per pubkey, regardless of how many rooms a user joins; see
"Behavior" below).
{
"kind": 10312,
"pubkey": "<participant pubkey hex>",
"tags": [
["a", "30312:<host pubkey hex>:<room d>", "<relay hint>"],
["hand", "0" | "1"],
["muted", "0" | "1"],
["publishing", "0" | "1"],
["onstage", "0" | "1"],
["alt", "Room Presence tag"]
],
"content": "",
...
}
The a tag is REQUIRED and points at the room defined in EGG-01.
Behavior
- A participant MUST emit a
kind:10312event:- on entering the room,
- every 30 s ± 5 s of jitter while in the room,
- immediately when any flag (
hand,muted,publishing,onstage) changes, - one final time on leaving, with
publishing="0"andonstage="0". The ±5 s jitter is REQUIRED. Synchronised heartbeats from a thousand-listener room would otherwise trigger relay bursts every 30 s.
- Receivers MUST treat a participant as departed if no presence has been
observed in
> 6 minutes. (One missed heartbeat plus a 5 minute tolerance window.) - The
handflag is the participant's request-to-speak. Hosts and admins read it to populate a queue (EGG-07). - The
mutedflag is set ONLY by speakers who are also publishing. Pure audience members SHOULD omit it. - The
publishingflag means "I currently have an openaudio/databroadcast on the relay" (EGG-03). It MUST be0whenever the speaker has stopped publishing, regardless of mute state. - The
onstageflag means "I currently hold a speaker slot in the room'sp-tag list (EGG-01) AND I have not voluntarily stepped off". A speaker who steps off the stage without losing the role tag MUST emitonstage="0". - The
d-tag MUST be the FIXED stringnests-room-presence(chosen so that one presence event represents a participant's CURRENT room across all clients). A participant MUST NOT be in two rooms simultaneously. - Every flag MUST be present in every emitted event. Receivers MAY assume
omitted flags default to
0, but emitters that omit are non-conformant. - Hosts and clients displaying the participant grid MUST hide presences
from
kind:10312events whosecreated_atis older than the staleness window in rule (2). - The flag values
"0"and"1"are strings, not JSON booleans — they appear inside a Nostr tag array which is[string, string, ...]by NIP-01. Parsers MUST accept strings only; integers, booleans, or any other JSON type for these positions MUST be treated as malformed. - Single-room rule. Because the
d-tag is fixed (rule 7), a participant has exactly one current presence event, scoped to one room. Receivers MUST keep only the most recentkind:10312per pubkey (replaceable-event semantics) and apply NIP-01 tie-break on identicalcreated_at. A buggy peer that publishes presences for two rooms in rapid succession is presumed to be in whichever room the most recent presence references.
Example
{
"kind": 10312,
"pubkey": "def...co",
"created_at": 1714003231,
"tags": [
["a", "30312:abchost:office-hours-2026-04", "wss://relay.example"],
["hand", "0"],
["muted", "0"],
["publishing", "1"],
["onstage", "1"],
["alt", "Room Presence tag"]
],
"content": "",
"id": "...",
"sig": "..."
}
Compatibility
EGG-07 (moderation) reads hand to drive the host's promote queue.
EGG-03 (audio plane) is the source of truth for whether a speaker IS
broadcasting; presence's publishing flag is a Nostr-side reflection that
clients use as a hint when they cannot subscribe to the audio plane (e.g.
listeners on weak networks rendering the home bubble).
The kind:10312 shape here intentionally mirrors kind:10311 from
NIP-53 streaming so that a generic Nostr client can render audience
indicators for both protocols without code-paths.