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:
- it MUST be a non-empty string;
- its length MUST NOT exceed 256 characters (an anti-abuse bound; a single half-move in any reasonable notation is far shorter).
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:
- The
etag is present exactly once, carries the markergame_sessionas its fourth element, and references a retrievable, signature-valid, conforming Game Session event (kind3422, per its §Semantic constraints) that is the canonical Game Session for its founding (kind3422§Idempotence and race resolution; Canonical Timing §Meta-resolution). A Ply referencing a non-canonical duplicate is not part of any canonical history. - The
ptag is present exactly once, carries the markeropponentas 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). - The
steptag is present exactly once and contains a positive integer (decimal, no leading zero). - The
drawtag, when present, appears at most once and carries no value beyond the tag name. - The
contentfield 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. - Exactly one
noncetag 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:
- Implement a complete rule engine for each game variant supported.
- Interpret the
contentstring 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. - Validate received Ply events against the current game position before applying them to the local state.
- Order plies within a game using the
steptag 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). - 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. - On every published Ply, tag the opponent in a
popponenttag (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 withsinceto receive only new moves — and, on each received Ply, refresh the session named by itsegame_sessiontag and re-evaluate whose turn it is. This keeps live turn tracking at a single fixed-size subscription rather than one per game. Theptag 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
-
Signatures: Ply events are signed by the moving player. Receiving clients MUST verify signatures per NIP-01 before applying moves to local game state.
-
Opponent tag and metadata: the
popponenttag makes a player’s Plies discoverable directly by their pubkey (#p), in addition to per-session (#e). This exposes no association that is not already public — the Game Session (kind3422) already tags both players, so the player↔session link, and therefore the player’s Plies via that session, is already discoverable; the opponent tag only adds a direct index for efficient turn notification. Because the tag could otherwise be used to inject Plies into an arbitrary pubkey’s#pfeed, clients MUST verify against the referenced Game Session that the tagged key is the session’s non-signer player (constraint 2) and MUST ignore any Ply whoseptag does not name that player — such a Ply is non-conforming and never effective. -
Replay and reordering: A Ply event for a given game can in principle be replayed or arrive out of order. Clients SHOULD use the
steptag to detect duplicate or out-of-order plies for the same game. -
Race conditions: If two or more Ply events for the same
stepof the same session are signed by the same player, each candidate’s canonical timing is determined per Canonical Timing §Meta-resolution (smallest canonical timing, then smallest event ID). The canonical one — the smallest-timed by default, or the candidate the rule system’s selection retains where several legitimate candidates are admitted (§Multiple Plies for the same step) — is the only effective Ply; the rest MUST be ignored by clients and by any arbitration layer. They are not retracted from relays; they are simply not effective. -
Content interpretation: The
contentfield carries the played move in the rule system’s encoding for the session’s(game, variant); it is not free-form prose. Clients and the arbiter MUST interpret and validate it through the rule engine for that variant, and MUST reject a Ply whosecontentis empty, exceeds the length bound, or does not conform to that encoding. A generic client with no rule engine for the variant SHOULD treat the value as opaque and MUST NOT render it as arbitrary text. -
Proof of work: The
noncetag follows NIP-13 and protects against Ply spam — in particular premove flooding (publishing many candidate Plies for a future slot). Relays SHOULD enforce a minimum difficulty appropriate to their policy; the nonce value in this NIP’s example is an illustrative placeholder, computed by brute force until the event ID achieves the target leading-zero-bit difficulty. Unlike challenges, a Ply is clock-timed: the time spent mining the nonce delays publication and therefore the Ply’s canonical timing, and that latency is charged to the mover (rule-system accounting — Time Accounting — Sanki for the reference rule system). A difficulty high enough to matter on a fast time control would tax honest play, so deployments SHOULD keep it low — well below the per-move time budget. PoW raises the per-event cost of bulk spam but does not replace the rule system’s own bound on how many premoves a slot honours (forsanki, Move Encoding — Sanki §Bounding a slot’s candidates). -
Arbitration scope: This NIP makes no provision for game-outcome arbitration, draw acceptance, resignation, or termination. Those concerns are out of scope and require separate event kinds and protocols.
References
- NIP-01 — Basic protocol flow description
- NIP-13 — Proof of Work (the
noncetag) - NIP-64 — Chess (PGN); a completed session encoded with SAN content is exportable to this format
- Kind
3422— Game Session (establishes t₀) - Event Timestamp Attestation — primitive for per-Ply timestamping (attested mode)
- Canonical Timing — canonical timing, the pending state, meta-resolution, t₀
