NIP-XX
Mood
draft optional
Abstract
This NIP defines an event kind for a reader to label a half-move and the position it produces with one mood — a short token expressing how that move feels. The label is keyed not to a game event but to the (move, resulting position) pair itself: the event’s addressable identifier (d tag) is a fixed-length digest of the pair, and the pair is carried in clear in dedicated tags. The same move yielding the same position — in any game, by any players, reached by any move order — is therefore one coordinate, so a reader’s mood follows the move across games instead of being pinned to one Ply. The mood vocabulary is game-defined, but the identity is built from universal, game-agnostic encodings (this suite: the protocol’s [source, destination, actor] move and a canonical position string), so the same move and position cumulate even across different game labels (e.g. a game labelled chess and a multi-variant sanki game using the western style).
Motivation
A mood is a lightweight, self-curated aesthetic annotation — “this move is tactical / romantic / …” rather than “good / blunder”. Three properties shape the model:
- A mood belongs to a move-and-its-result, not to a game. The aesthetic character of a move is a property of the move and the position it produces, independent of which game it occurred in. Keying the mood to the (move, resulting position) pair makes it transposition-stable and cross-game: a reader who labels a move once sees that label wherever the same move recurs.
- The identity is game-agnostic. Because the move and position encodings are universal (not chosen per game), the same (move, resulting position) carries the same coordinate regardless of the
gamelabel — so a reader’s moods in a purechessgame and in asankigame played in the western style cumulate into one slot. Thegameandvarianttags are descriptive, not part of the identity. - A reader holds at most one mood per coordinate, and changes their mind over time. This is a single mutable value, replaced on each change and removable — exactly the shape of an addressable (parameterized replaceable) event, whose
(kind, pubkey, d)coordinate a relay reduces to its latest occurrence. No client-side log folding, tombstone reconciliation, or unbounded event history is required: the relay holds the current value.
Publishing moods as signed, addressable events serves the same ends as the puzzle family’s Composition (kind 3440), the suite’s other open, position-carrying artifact: free, decentralized, and pluralistic (any reader publishes their own; consumers scope to the authors they care about).
Cross-reader aggregation (how a circle or the network feels about a move), client-side recall caching, and difficulty/curation layers are consumer-side or off-protocol enrichments, deliberately out of scope (see Scope boundary).
Specification
Event kind
kind: 30422
Mood events are addressable (parameterized replaceable) events per NIP-01: a relay retains only the latest event for a given (kind, pubkey, d) triple (latest created_at, lowest event id on a tie). The d tag is the (move, resulting position) pair, so each reader has one current mood per move-and-result; re-publishing for the same d replaces rather than duplicates.
Signer — any reader
The event is signed by any reader — a spectator or a player — labelling a move for themselves. It involves no arbiter or timestamper, has no effect on ratings or on any session’s lifecycle, and references suite events only optionally, for provenance. A reader labels their own moods; the protocol places no permission model on which moves may be labelled.
The d identifier
The d tag is the lowercase-hex SHA-256 digest of a preimage composed of two parts joined by |:
preimage = <move> "|" <resulting-position>
d = sha256(preimage) (UTF-8 bytes; 64 lowercase hex characters)
<move>— the half-move in the protocol’s move encoding. (This suite: the[source, destination, actor]JSON triple, serialized compactly.)<resulting-position>— the canonical position string of the position after the move is applied. (This suite: a canonical FEEN string.)
The digest keeps the identity’s defining property — d is a pure function of the pair, computable locally by anyone who knows the move and the position, with no lookup — while making it bounded: 64 characters for every game, however large its position encoding. An unbounded clear-text d would make the addressable identity hostage to relay-side limits on indexed tag values (a relay that caps or truncates them silently breaks replacement, #d filters, and NIP-09 coordinate deletion), and a fixed-length hex d keeps the NIP-01 coordinate kind:pubkey:d short and exactly three colon-separated segments. The delimiter | MUST NOT appear in either component (it appears in neither a compact move triple nor a canonical FEEN string), which makes the preimage — and therefore the digest — injective over pairs.
The clear-text pair travels with the event: the move and position tags (§Tags) carry the two preimage components verbatim. A conforming Mood’s d MUST equal the digest of the preimage rebuilt from its move and position tags; an event violating this is non-conforming and MUST be ignored. The check is stateless, so the digest costs nothing in auditability: any consumer can verify the identity from the event alone.
The key carries no version field: a breaking change to this encoding is published under a new kind number rather than an in-field version — per-event version fields fragment addressable identities. The encodings it composes are themselves externally versioned (FEEN v1.0.0, the move triple).
Two design points:
- The position is the one the move produces (after), not the one it is played from. This matches where a mood is shown — on the resulting board, on the piece that just moved — and is collision-free: a conforming position notation records captured pieces (so nothing a move removes is lost from the resulting position), so distinct moves never collapse to one key.
- When an application’s encoding is game-agnostic (as this suite’s is), identical (move, resulting position) pairs share a coordinate across game labels. Cumulation extends only as far as the encodings are shared: parties that encode positions or moves differently simply do not cumulate (and clients that do not recognize an encoding ignore it).
This NIP imposes no particular encoding on either component: a producer uses whatever encoding its intended consumers share, and a consumer that cannot parse a mood’s move/position tags in an encoding it recognizes simply ignores the event — liberal in what it accepts, strict in what it renders, exactly as for an unrecognized content. The game’s supporting document records the canonical encoding an application uses (this suite: the move triple and a canonical FEEN of the resulting position) so that its own moods address one another.
Tags
| Tag | Cardinality | Value | Description |
|---|---|---|---|
d |
exactly one | sha256(<move> "|" <resulting-position>), 64 lowercase hex (above) |
the addressable identifier |
move |
exactly one | the <move> preimage component, verbatim |
the half-move in clear; with position, MUST rebuild the d digest |
position |
exactly one | the <resulting-position> preimage component, verbatim |
the resulting position in clear; with move, MUST rebuild the d digest |
game |
exactly one | identifier matching ^[a-z][a-z0-9]{0,31}$ |
descriptive: the game context in which the mood was set, and the selector for the content vocabulary (reference: sanki). Not part of the identity. |
variant |
0 to n | identifier matching ^[a-z][a-z0-9]{0,31}$ |
descriptive: the distinct variant(s) present in the position, an unordered set, for discovery; recoverable from the position in any case. Not part of the identity. |
e ply |
0 or 1 | event id, optional relay hint, marker ply |
OPTIONAL reference to a Ply (kind 3423) that played this move, for navigation to the move in a game where it occurred; absent when the move is labelled outside any specific game (e.g. from analysis). |
e game_session |
0 or 1 | event id, optional relay hint, marker game_session |
OPTIONAL reference to the Game Session (kind 3422) the mood was set in, so a feed can resolve the game without first resolving the ply. Set together with ply when both are known. |
alt |
0 or 1 | human-readable string | NIP-31 fallback description (e.g. "Sanki mood: tactical."). |
nonce |
exactly one | nonce + difficulty | NIP-13 proof of work |
The nonce tag follows NIP-13. Because any pubkey may emit kind:30422 at any coordinate, the tag raises the per-event cost of mood spam (bulk publication across many fabricated coordinates). Conformance is intrinsic — the event id satisfies NIP-13 against the tag’s committed difficulty — and relays additionally enforce their advertised minimum at ingestion. Unlike a Ply, a mood is not clock-timed, so the difficulty need not be kept low; see §Security considerations.
Content
The content field is a single mood label — a short token from the vocabulary the game defines. It MUST be non-empty (the absence of a mood is the absence of an event, not an empty one). A reader holds at most one mood per coordinate, so content carries exactly one label.
A consumer ignores a mood whose content is not in the vocabulary it recognizes — liberal in what it accepts, strict in what it renders: any pubkey may emit kind:30422, but a conforming client only surfaces labels it knows.
Setting, changing, and removing a mood
- Set or change — publish a
kind:30422event with the chosencontentat the coordinate. Because the event is addressable, a change reuses the samedand the relay replaces the previous label; no prior event survives to resurface. -
Remove — publish a NIP-09 deletion (kind
5) targeting the coordinate:["a", "30422:<reader_pubkey>:<d>"]This requests deletion of the addressable event at that coordinate (all occurrences up to the deletion’s
created_at). Removal is therefore a single event, with no replacement label to maintain. Re-setting a mood afterwards works normally: the new event’s latercreated_atis outside the earlier deletion’s range.
NIP-09 deletions are best-effort: a relay MAY ignore them. The failure mode is mild and bounded — because the event is replaceable, a relay that ignores a removal still serves only the reader’s last set label, never an older one. No superseded mood resurfaces.
Aggregation
A relay holds the authoritative current state, so a reader’s mood for a coordinate is read directly:
A reader’s mood for a move-and-result is the
contentof their latest non-deletedkind:30422event at(30422, reader, d), or none if no such event exists.
Concretely, a consumer:
- Computes the
dof each move it wants to display (the digest of the preimage built from the universal encodings: move, resulting position). - Queries
{ kinds: [30422], authors: [<reader(s)>], "#d": [<d>, …] }. - Renders each returned event’s
contentif it is in the recognized vocabulary, reconciled against any visible kind5coordinate deletion.
Querying a single #d across authors yields at most one mood per author — the basis for cross-reader sentiment on a move (a consumer-side enrichment).
Scope boundary
This NIP defines only the mood artifact and its addressing. The following belong to consuming applications, the game’s supporting document, or off-protocol services: the mood vocabulary and the exact versioned encoding of <move> and <resulting-position> (game-defined); client-side recall caches and incremental indexing; cross-reader aggregation and its presentation; and difficulty, curation, or recommendation layers over the raw mood stream.
Examples
Each conforming mood also carries a nonce tag (NIP-13 proof of work, §Tags); it is shown in the first example with an illustrative placeholder value and omitted from the others for brevity.
Example — labelling a move tactical (sanki)
{
"kind": 30422,
"pubkey": "<reader-pubkey>",
"created_at": 1718900000,
"tags": [
["d", "<64-hex sha256 of the preimage below>"],
["move", "[\"e2\",\"e4\",null]"],
["position", "<canonical FEEN of the resulting position>"],
["game", "sanki"],
["variant", "chess"],
["e", "<a-ply-that-played-this-move>", "wss://relay.example.com", "ply"],
["e", "<that-ply's-game-session>", "wss://relay.example.com", "game_session"],
["alt", "Sanki mood: tactical."],
["nonce", "8127", "16"]
],
"content": "tactical"
}
Reaching the same move and resulting position in another game — even one labelled chess rather than sanki — a client computes the same d and the relay returns this event, so the reader’s tactical mood is shown again, without any per-game re-labelling.
Example — changing one’s mind later
Identical d, a later created_at, content "classical": the relay replaces the previous event, and the reader’s mood for that move-and-result becomes classical.
Example — removing the mood (NIP-09 on the coordinate)
{
"kind": 5,
"pubkey": "<reader-pubkey>",
"created_at": 1718903600,
"tags": [
["a", "30422:<reader-pubkey>:<64-hex sha256 of the move|position preimage>"],
["k", "30422"]
],
"content": ""
}
After this, the reader has no mood for that move-and-result.
Client guidelines
- Compute
dlocally. A client derives each move’sdby hashing the preimage built from the move and the canonical resulting position, per the encoding its application uses. The same routine is used on write (labelling) and on read (recall), so the two agree by construction. - Verify the identity; ignore what you can’t parse. A client checks that a mood’s
dequals the digest of itsmoveandpositiontags (a stateless check) and recognizes only the encoding(s) its application uses; a mood failing the check, or whose pair does not parse in a recognized encoding, is ignored, never rendered — the protocol places no encoding restriction, so robustness is the consumer’s job. - Recall by coordinate. To show a reader’s moods over a game, compute the
dof each half-move and query the reader’skind:30422events by#d. A local cache MAY accelerate this, but is not required for correctness — the relay holds the current value. - Trust scope. When aggregating across authors, scope to authors the user trusts (typically themselves and their NIP-02 follows), exactly as the suite scopes other open-authorship kinds; any pubkey may emit
kind:30422. - Privacy disclosure. A mood permanently links the reader’s pubkey to an aesthetic opinion about a move — public and, like every Nostr event, effectively permanent; a client SHOULD make this explicit before the first publication.
- Mine the proof of work before publishing. Before broadcasting a mood, compute its
nonce(NIP-13) to the relay-advertised difficulty (§Tags). A mood is not clock-timed, so a brief mining delay is harmless; a client MAY mine eagerly.
Security considerations
- Key canonicality is load-bearing within an application. Addressing relies on a byte-identical preimage across the clients that share an encoding — the digest faithfully amplifies any divergence. Pin the canonical position/move forms in the
game’s supporting document (this suite: the protocol move and a canonical FEEN of the resulting position); a divergent preimage silently fails to match (moods scatter across near-duplicate coordinates). Publish any breaking change to that encoding under a new kind number rather than mutating this one — per-event version fields would fragment addressable identities. A mood in an unrecognized or malformed encoding is otherwise harmless: consumers verify the digest, fail to parse the pair, and ignore it. - Public taste profile. Moods are ordinary public events; a reader’s
kind:30422stream is an enumerable profile of their aesthetic opinions, linkable to their pubkey. Mitigation is dedicated keypairs and user awareness, not the protocol. - Spam. Any pubkey may emit
kind:30422; consumers MUST scope to trusted authors when aggregating, and relays MAY apply their own admission policy. - Proof of work. The required
noncetag follows NIP-13 (§Tags) and raises the per-event cost of bulk mood spam — publication across many fabricated coordinates. Relays SHOULD enforce a minimum difficulty appropriate to their policy; thenoncevalue in the example is an illustrative placeholder, computed by brute force until the event ID achieves the target leading-zero-bit difficulty. Unlike a Ply, a mood is not clock-timed: no game clock is charged for the mining time, so a relay MAY require a higher difficulty than it does for Plies without taxing honest play. PoW raises the per-event cost of spam but does not replace author-scoping (above) when aggregating across readers. - Best-effort removal. A relay ignoring a NIP-09 coordinate deletion still serves only the reader’s last set label, never a resurrected older one — the addressable model bounds the failure to “the most recent mood lingers”.
References
- NIP-01 — addressable (parameterized replaceable) events and the
(kind, pubkey, d)coordinate. - NIP-09 — event deletion request; the
a-tag coordinate deletion used to remove a mood. - NIP-13 — Proof of Work (the
noncetag). - Ply (kind
3423) — the optional provenance target (ply) and the source of the move encoding reused ind. - Composition (kind
3440) — the puzzle family’s position-carrying sibling: open authorship consumed through reader-side author scoping, as here.
