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

NIP-XX

Ply

draft optional

This NIP defines a Nostr event kind for representing a single half-move (ply) in a turn-based, two-player abstract strategy board game of the chess family. The event carries the half-move as an opaque string whose encoding is defined by the rule system, so the NIP remains agnostic about move notation, board geometry, and rule-system semantics.

Abstract

Defines kind: 3423, a regular event representing one half-move played in a game. The event’s content field carries the half-move as a string, in an encoding defined by the rule system for the session’s game and variant — Standard Algebraic Notation (SAN) for classical chess, or any other encoding the rule system specifies (including a structured array for exotic variants). Move legality, capture identification, recognition of compound or special moves, promotion logic, and all other rule-system semantics are determined by the client’s rule engine from the current position and the rules of the variant being played.

Motivation

A turn-based game on Nostr requires a compact, unambiguous representation of each half-move. The encoding philosophy of this NIP is to carry the played move as an opaque string and to defer its notation and all rule-system semantics to the rule engine. This keeps event sizes small, leaves Nostr relays free of any game-specific logic, and lets each game choose the move notation best suited to it — a standard notation such as SAN for classical chess (interoperable with existing tooling), or a bespoke encoding for an exotic variant.

The move is carried in the content field rather than as individual tags because the move payload is not meant to be indexed or filtered by relays: nobody queries plies by move detail across sessions. Tags are reserved for indexable, contextual metadata (session reference, opponent reference, step number, flags).

Among that metadata, the opponent reference — a p tag naming the player whose turn it now is — lets a player discover that a move was made in one of their games with a single subscription keyed on their own pubkey, {"kinds":[3423],"#p":["<player>"]}, rather than opening or maintaining a per-session subscription for every game they are in. This is the same “addressed to me” pattern Nostr uses for mentions and direct messages, and it keeps a client’s live “your turn” tracking at one fixed-size subscription regardless of how many games the player has.

Specification

Event kind

kind: 3423

Ply events are regular events per NIP-01: immutable once signed and broadcast. A new event is created for every half-move.

Tags

Reference tags

Tag Cardinality Value Description
e game_session exactly one event id, optional relay hint, marker reference to the Game Session (kind 3422) for this session
p opponent exactly one opponent pubkey, optional relay hint, marker reference to the opponent (the other player), for turn notification and discovery
step exactly one positive integer (decimal, no leading zero) the signer’s own move ordinal, monotonically increasing from 1 per signer

The e tag MUST carry the marker game_session as its fourth element:

["e", "<game_session_event_id>", "<relay_hint>", "game_session"]

The relay hint (third element) is OPTIONAL but SHOULD be present. This reference identifies the Game Session (kind 3422) for the session this Ply belongs to. It is used for session-scoped discovery (e.g., retrieving all Plies for a given session via a standard #e filter on the Game Session’s event ID) and for cross-event validation by the arbiter and verifiers.

The p tag MUST carry the marker opponent as its fourth element:

["p", "<opponent_pubkey>", "<relay_hint>", "opponent"]

It names the opponent — the player of the referenced Game Session who is not the signer, i.e. the player whose turn it now is. The game is two-player, so there is exactly one such opponent; the signer does NOT tag themselves. The relay hint (third element) is OPTIONAL but SHOULD be present (the opponent’s read relay). This reference exists for turn notification and discovery: it lets a client find every move played against a given player with the standard filter {"kinds":[3423],"#p":["<player>"]} — a single, fixed-size subscription independent of the number of sessions — without enumerating sessions or holding a per-session subscription. It does not replace the e game_session tag: the e reference remains the basis for session-scoped retrieval and canonical assembly, while the p reference is a discovery aid layered on top. The marker name is documentary; the #p filter matches on the pubkey value regardless of the marker.

Optional flag tags

Tag Cardinality Value Description
draw zero or one (none beyond the tag name) optional flag attached to the move; the protocol attaches no meaning to it

The draw tag, when present, carries no value beyond its name. This NIP attaches no semantics to it: whether the game has a draw concept, what the flag signifies, and how (if at all) it is accepted or declined are determined by the rule system and the designated arbiter, and are out of scope for this NIP. (For the reference sanki rule system, its supporting document Statuses — Sanki interprets it as an implicit draw offer, accepted by the opponent’s own flagged reply.)

Proof-of-work tag

Tag Cardinality Value Description
nonce exactly one nonce + difficulty NIP-13 proof of work

The nonce tag follows NIP-13. It raises the per-Ply cost of spam — in particular premove flooding (publishing many candidate Plies for a future slot). Conformance is intrinsic — the event id satisfies NIP-13 against the tag’s committed difficulty — and relays additionally enforce their advertised minimum at ingestion. Because a Ply is clock-timed, that difficulty SHOULD be kept low; see §Security considerations.

Content

The content field carries the played half-move, encoded as a non-empty string. This NIP does not prescribe the encoding: it is defined by the rule system for the session’s (game, variant) (declared in the founding events) and validated by the designated arbiter. This keeps the event rule-agnostic at the transport layer — relays and generic clients carry the move opaquely; only a rule engine interprets it.

For classical two-player chess, the natural encoding is Standard Algebraic Notation (SAN) — the per-move notation used by PGN — e.g., "e4", "Nf3", "O-O", "exd5", "e8=Q". Using SAN makes a completed session trivially exportable to a NIP-64 chess record and interoperable with existing PGN tooling.

Other encodings are equally valid when the rule system defines them — for example a coordinate notation ("e2e4"), or a structured [source, destination, actor] array for variants that benefit from rule-light board reconstruction (the reference sanki rule system specifies one in its supporting document, Move Encoding — Sanki).

The only transport-level constraints this NIP places on content are:

Move legality, interpretation of the notation, capture identification, special-move recognition, and all other rule-system semantics are determined by the rule engine from the content and the current position, per the variant. The arbiter validates that content conforms to the encoding it has published for the (game, variant).

Semantic constraints

A conforming Ply event MUST satisfy all of the following:

  1. The e tag is present exactly once, carries the marker game_session as its fourth element, and references a retrievable, signature-valid, conforming Game Session event (kind 3422, per its §Semantic constraints) that is the canonical Game Session for its founding (kind 3422 §Idempotence and race resolution; Canonical Timing §Meta-resolution). A Ply referencing a non-canonical duplicate is not part of any canonical history.
  2. The p tag is present exactly once, carries the marker opponent as its fourth element, and its pubkey is a valid 32-byte lowercase hex key, distinct from the signer’s pubkey, and equal to the player of the referenced Game Session who is not the signer (the opponent).
  3. The step tag is present exactly once and contains a positive integer (decimal, no leading zero).
  4. The draw tag, when present, appears at most once and carries no value beyond the tag name.
  5. The content field is a non-empty string of at most 256 characters. Its encoding and validity are defined by the rule system for the session’s (game, variant) and verified by the arbiter; they are not constrained by this NIP.
  6. Exactly one nonce tag is present, carries its committed difficulty as its third element, and the event id satisfies NIP-13 against that committed difficulty — never against a relay’s advertised minimum (an ingestion policy, not conformance).

Constraints 3, 4, 6, the structural part of 5 (non-empty string, length), and the structural part of 2 (one p tag, valid hex key, distinct from the signer) are checkable from the event alone. Constraint 1, and the remainder of constraint 2 (that the tagged key is the referenced Game Session’s non-signer player), additionally require cross-event validation against the Game Session. The semantic validity of content depends on the rule system and the current position, and is enforced by the client and the arbiter at the validation step (see §Validation responsibility).

Inferred from position state

The Ply event carries only the played move; everything else is determined by the client’s rule engine from the current position and the variant’s rules. For example, in chess-family variants the rule engine infers whether the move is a capture (and the captured piece), whether it triggers a composite displacement (such as castling) or a special capture (such as en passant), and whether and how a piece is transformed (such as promotion). The protocol takes no position on any of this.

Time accounting

The created_at field of a Ply event is the moving player’s self-claim of when they signed the event. In attested mode it is informational only and MUST NOT be used for time-control accounting, race resolution, or any other timing-dependent decision. In self-timed mode it IS the Ply’s canonical timing, made non-backdatable by the designated timing relay’s strict ingestion window. See Canonical Timing.

Time accounting anchors at t₀, the canonical session-start moment (kind 3422 §Canonical session start: the later of the Game Session’s start_at tag, when present, and its canonical timing). No Ply is valid before t₀; Plies whose canonical timing precedes t₀ MUST be treated as invalid by verifiers.

Per-ply timing is the Ply’s canonical timing (Canonical Timing §Canonical timing of an event). In attested mode it is the created_at of an Event Timestamp Attestation (kind 3410, see the Event Timestamp Attestation NIP) signed by the designated timestamper, which references the Ply via the required attests-marked e tag; attestations from any other signer are ignored, and the canonical attestation per Ply is the designated timestamper’s smallest-created_at one, smallest id on a tie (Event Timestamp Attestation NIP §Same-signer equivocation). In self-timed mode it is the Ply’s own created_at, made non-backdatable by the designated timing relay. Time-control accounting is computed from the sequence of per-Ply canonical timings, anchored at t₀ — rule-system-defined; the reference sanki rule system specifies the precise arithmetic in its supporting document (Time Accounting — Sanki).

In attested mode, the structure of a per-Ply Event Timestamp Attestation is:

["e", "<ply_event_id>", "<relay_hint>", "attests"]
["e", "<game_session_event_id>", "<relay_hint>", "<app>:session"]

The second e tag references the Game Session (kind 3422) and carries an application-namespaced discovery marker <app>:session, per the Event Timestamp Attestation NIP §Optional tags (the reference timestamper uses sashite:session). In this suite’s attested mode the designated timestamper MUST include this session-scoped e tag on every per-Ply, per-Request, and per-Adjudication attestation — it is what makes the discovery below sound, and a writer/reader asymmetry otherwise (an attestation without it would be authoritative yet invisible to session-scoped queries). The marker name is documentary: a client finds all of a session’s attestations via the standard Nostr filter {"kinds": [3410], "#e": ["<game_session_event_id>"]}, which matches on the Game Session’s event id regardless of marker. The Session Start Attestation is discoverable separately via its attests reference to the Game Session event.

NIP-26-delegated signatures from the timestamper are NOT accepted as authoritative; see kind 3420 §Security considerations.

Race resolution

When multiple Plies are signed by the same player for the same step value, each candidate’s canonical timing is determined per Canonical Timing §Canonical timing of an event, with the smallest event ID as tiebreaker. By default the smallest-timed candidate is canonical (Canonical Timing §Meta-resolution); where the rule system admits several legitimate candidates per slot, it MAY define the selection among them, consuming this canonical timing purely as the ordering. A Ply with no canonical timing is pending and MUST NOT be considered.

Multiple Plies for the same step

If more than one Ply is signed by the same player for the same (session, signer, step), that slot has several candidates. This NIP gives each candidate a deterministic canonical timing (§Race resolution) and otherwise takes no position: whether several contents per slot are permitted, and how the canonical one is chosen among them, is a rule-system / arbiter concern, out of scope for the protocol. In particular this NIP does not treat differing-content candidates as a violation. The reference deployment’s treatment for the sanki rule system — identical re-submissions are idempotent retries, and among differing candidates the canonical move is the first legal one, an illegal candidate (premove or live move) being skipped rather than sanctioned — is described in the rule system’s supporting document (Move Encoding — Sanki §Slot candidates and selection).

Step semantics and play order (convention)

The step value is the signer’s own move ordinal: a player’s first Ply carries step 1, their second carries step 2, and so on — independently for each player. A Ply therefore occupies the slot (session, signer, step). Because the signer is part of the slot, each player owns their own step sequence: no Ply can occupy another player’s slot.

The interleaving of the players’ sequences into a single play order is defined by the rule system. In this suite, play strictly alternates and, within a given step value, the player in seat first moves before the player in seat second: the global half-move order is (step 1, first), (step 1, second), (step 2, first), (step 2, second), … — matching the conventional move numbering of chess scoresheets (rule-system-defined; Move Encoding — Sanki §Within-step ordering for the reference rule system).

The interleaving is a rule-system convention, not a protocol rule. A rule system MAY define any other interleaving — including turn structures where a player moves more than once in a row; the rule system, not this NIP, determines where each (signer, step) slot falls in the play order. The arbiter’s natural-state computation (see kind 3425) consumes Plies in the rule system’s play order.

Validation responsibility

Validation of the nonce (constraint 6: event id versus its committed difficulty) is checkable from the event alone; a relay additionally enforces its advertised NIP-13 minimum at ingestion — a relay policy, distinct from conformance.

Move legality (rule-system conformance, position consistency, turn order, draw and termination conditions) is the responsibility of the client. This NIP makes no claim about server-side or relay-side validation. Relays accepting kind: 3423 events MAY perform syntactic tag validation per this specification but are NOT required to validate move legality. A relay that rejects events solely on grounds of move legality should be documented as a specialized relay outside the scope of this NIP.

Examples

The examples below use Standard Algebraic Notation (SAN) for classical chess. The same event structure carries any other encoding the rule system defines; only the content string changes. Each conforming Ply also carries a nonce tag (NIP-13 proof of work, §Proof-of-work tag); it is shown in Example 1 with an illustrative placeholder value and a low target difficulty, and omitted from the other examples for brevity.

Example 1 — A simple move

A pawn advance, e4.

{
 "kind": 3423,
 "pubkey": "<moving player's pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["e", "<game_session_event_id>", "wss://relay.example.com", "game_session"],
 ["p", "<opponent_pubkey>", "wss://relay.example.com", "opponent"],
 ["step", "1"],
 ["nonce", "8127", "8"]
 ],
 "content": "e4",
 "id": "...",
 "sig": "..."
}

Example 2 — A capture

A pawn on the e-file captures on d5, exd5. The captured piece is not encoded: the rule engine determines it from the destination square’s occupant.

{
 "kind": 3423,
 "pubkey": "<moving player's pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["e", "<game_session_event_id>", "wss://relay.example.com", "game_session"],
 ["p", "<opponent_pubkey>", "wss://relay.example.com", "opponent"],
 ["step", "5"]
 ],
 "content": "exd5",
 "id": "...",
 "sig": "..."
}

Example 3 — Castling (a compound move)

Kingside castling, O-O. The rule engine computes the associated rook movement from the current position.

{
 "kind": 3423,
 "pubkey": "<moving player's pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["e", "<game_session_event_id>", "wss://relay.example.com", "game_session"],
 ["p", "<opponent_pubkey>", "wss://relay.example.com", "opponent"],
 ["step", "7"]
 ],
 "content": "O-O",
 "id": "...",
 "sig": "..."
}

Example 4 — Promotion (a transformation choice)

A pawn reaches the last rank and the moving player promotes to a queen, e8=Q. The chosen target type is part of the SAN string.

{
 "kind": 3423,
 "pubkey": "<moving player's pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["e", "<game_session_event_id>", "wss://relay.example.com", "game_session"],
 ["p", "<opponent_pubkey>", "wss://relay.example.com", "opponent"],
 ["step", "40"]
 ],
 "content": "e8=Q",
 "id": "...",
 "sig": "..."
}

Example 5 — A move carrying the draw flag

A move that also carries the optional draw flag. The protocol attaches no meaning to the flag; its interpretation is rule-system/arbiter-defined (see §Optional flag tags).

{
 "kind": 3423,
 "pubkey": "<moving player's pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["e", "<game_session_event_id>", "wss://relay.example.com", "game_session"],
 ["p", "<opponent_pubkey>", "wss://relay.example.com", "opponent"],
 ["draw"],
 ["step", "10"]
 ],
 "content": "Nf3",
 "id": "...",
 "sig": "..."
}

Client guidelines

Clients implementing kind: 3423 SHOULD:

  1. Implement a complete rule engine for each game variant supported.
  2. Interpret the content string using the rule system’s encoding for the session’s (game, variant), and validate it (§Content, §Semantic constraints) before broadcasting locally-constructed moves or accepting received ones.
  3. Validate received Ply events against the current game position before applying them to the local state.
  4. Order plies within a game using the step tag and the rule system’s interleaving of the players’ sequences (§Step semantics and play order). When two events occupy the same (session, signer, step) slot, apply the race-resolution rule (§Race resolution).
  5. Before broadcasting a Ply, compute its nonce (NIP-13 proof of work) to the relay-advertised difficulty. Keep the difficulty low so mining does not eat into the mover’s clock (§Security considerations), and publish promptly once mined.
  6. On every published Ply, tag the opponent in a p opponent tag (the other player of the session). To track “your turn” live across all of a player’s games, subscribe to {"kinds":[3423],"#p":["<self>"]} — optionally bounded with since to receive only new moves — and, on each received Ply, refresh the session named by its e game_session tag and re-evaluate whose turn it is. This keeps live turn tracking at a single fixed-size subscription rather than one per game. The p tag is a discovery trigger, not a turn oracle: a received Ply means a move occurred in that session, not necessarily that it is now the recipient’s turn (e.g. with premoves or compound turn structures), so the recipient confirms the turn from the refreshed session state.

Security considerations

References