Sashité for Developers
  1. Sashité for Developers
  2. Nostr
  3. NIPs
  4. Mood

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:

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)

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:

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

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 content of their latest non-deleted kind:30422 event at (30422, reader, d), or none if no such event exists.

Concretely, a consumer:

  1. Computes the d of each move it wants to display (the digest of the preimage built from the universal encodings: move, resulting position).
  2. Queries { kinds: [30422], authors: [<reader(s)>], "#d": [<d>, …] }.
  3. Renders each returned event’s content if it is in the recognized vocabulary, reconciled against any visible kind 5 coordinate 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

Security considerations

References