- Sashité for Developers
- Nostr
- NIPs
- Direct Challenge
NIP-XX
Direct Challenge
draft optional
This NIP defines a Nostr event kind for publicly proposing a session in a turn-based, two-player abstract strategy board game of the chess family. The Direct Challenge event names a specific opponent, names the rule system the session is played under, designates the session’s timing authority, and carries the proposed session terms. The same event, carrying two additional references, proposes the rematch of a concluded session.
Abstract
Defines kind: 3420, a regular event by which a player publicly proposes a session to a specific opponent. The event identifies the challenged player, designates the timing authority (an optional timestamper, or one or more timing relays), and carries the game context (shared), the rule system (shared), an optional proposed variant for each player, a time-control configuration, an optional seat assignment, and an acceptance deadline.
The challenged player accepts by publishing the session’s Game Session (kind 3422) referencing the challenge: acceptance and founding are one act, and the session is playable from that event. A challenge that receives no Game Session before its deadline expires without effect.
A Direct Challenge carrying a rematch_of reference proposes a rematch of a concluded session between its two players, with the seats swapped and every other term reused (§Rematch challenge).
Motivation
Initiating a session on Nostr requires a public, signed proposal that fixes session terms cryptographically. A Direct Challenge expresses one player’s offer to play another specific player under specific terms, witnessed by relays. The challenged player either founds the session (Game Session, kind 3422) or remains silent (letting the challenge expire). No “decline” event is defined; silence is the refusal.
This NIP carries only the structural and lifecycle aspects of the challenge. Game-specific semantics — what a given variant identifier means within a given game context, what the piece movements are, how time accounting elapses, how the session concludes — are determined by the rule-system document the challenge names (§Match-terms tags, the rules term). The suite designates no arbiter: every rule question of the session is a function of its public events and of that document, so any consumer can compute the same answer, and the players themselves found and conclude the session (kind 3422; kind 3425).
A Direct Challenge MAY leave either player’s variant — the challenged player’s, the challenger’s own, or both — or the seat assignment unfixed. In that case, the challenged player supplies the missing piece(s) in the Game Session. Omitting one’s own variant is a deliberate delegation: the challenger consents, by omission, to the challenged player choosing the variant the challenger will play.
Specification
Event kind
kind: 3420
Direct Challenge events are regular events per NIP-01: immutable once signed and broadcast.
Timing designation
This NIP requires exactly one timing designation, selecting the session’s timing mode per the Canonical Timing NIP §Timing modes and mode selection:
- Timestamper (marker
timestamper): a third party who provides signed receipt timing by publishing Event Timestamp Attestations (kind3410) for the session’s events. Carried, in attested mode, by exactly onetimestamper-markedptag. - Timing relay(s) (
timing_relaytags): the relay(s) whose acceptance establishes the session events’ canonical timing in self-timed mode, each conforming to the Self-Timed Timing Relay profile. Carried, in self-timed mode, as["timing_relay", "<wss://relay.example.com>"], one or more.
The designation fixes the timing mode (Canonical Timing §Canonical timing of an event):
- Attested mode — a timestamper IS designated (and no
timing_relaytag is carried). The canonical timing of the session’s events is the designated timestamper’s canonical attestationcreated_at; the events’ owncreated_atis ignored. - Self-timed mode (the default) — NO timestamper is designated, and one or more timing relays ARE. The canonical timing of each event is its own
created_at, once its acceptance by a designated timing relay is established — the relay’s strict ingestion window (it refuses any event whosecreated_atis in the past relative to its clock, beyond a small past tolerance) is what makes the self-claim trustworthy. The designated relays’ policies are verifiable via NIP-11 (Self-Timed Timing Relay §NIP-11 advertisement).
The self-timed guarantee is scoped to the designated relay(s): canonical timing is established for consumers who retrieve the session’s events from a designated relay (or a mirror they trust to be faithful to one), because acceptance itself is the timing bound. It leaves no in-event trace — an event re-encountered elsewhere, on an arbitrary relay or in an export, carries only its self-declared created_at, and nothing distinguishes an honestly-timed event from a backdated one accepted by a lax relay. A consumer who cannot establish acceptance by a designated relay MUST treat the event as pending (Canonical Timing §The pending state); the designation is what tells every verifier, forever, which relays to ask.
Designating a timestamper trades the self-timed baseline for signed, portable, third-party-verifiable timing — closing exactly the late-verifier gap above; whether a deployment offers it is a deployment choice, outside this generic NIP.
In attested mode, the timestamper publishes Event Timestamp Attestations (kind 3410, see the Event Timestamp Attestation NIP) for the session’s attested events: on this directed path, the Game Session (kind 3422), each Ply (kind 3423) and each Conclusion (kind 3425) — and, on the matchmaking path, the Pairing (kind 3419) before the Game Session. The Direct Challenge itself (kind 3420) is NOT attested. Attestations from any signer other than the designated timestamper are ignored. In self-timed mode there are no attestations; the same events acquire canonical timing from their own created_at once a designated timing relay accepts them. Kinds 3426 (Elo Rating Attestation) and 3427 (Glicko-2 Rating Attestation) are generic, suite-independent NIPs signed by a rating authority outside the session’s timing mode. The canonical timing is the authoritative source for race resolution and clock accounting; see Canonical Timing.
The timing designation propagates to the Game Session, which MUST mirror it exactly (the same timestamper, or the same timing_relay set).
Tags
Player tags
| Tag | Cardinality | Value | Description |
|---|---|---|---|
p |
one or two | pubkey, optional relay hint, role marker | exactly one opponent (the challenged player); and, in attested mode, exactly one timestamper |
timing_relay |
zero or more | relay URL (wss://…) |
in self-timed mode (the default), one or more designated timing relays; absent in attested mode |
The p tags MUST carry a role marker as the fourth element:
["p", "<challenged_pubkey>", "<relay_hint>", "opponent"]— the challenged player. Exactly one such tag is required.["p", "<timestamper_pubkey>", "<relay_hint>", "timestamper"]— the designated timestamper. Present (exactly one) puts the session in attested mode.["timing_relay", "<wss://relay.example.com>"]— a designated timing relay. One or more present puts the session in self-timed mode (the default). See §Timing designation.
The p tags serve a dual purpose: semantic (identifying the participants) and structural (notification routing via standard Nostr #p filters). The dual purpose is intentional — repeating identifiable pubkeys in p tags enables compatibility with the standard Nostr discovery mechanism without requiring chained subscription logic on the client side.
The relay hint (third element of a p tag) is OPTIONAL but SHOULD be present. When omitted, an empty string MAY be used in its position.
Constraints on the pubkeys:
- The
opponentpubkey MUST be distinct from the event’spubkey(the challenger). - When a
timestamperis designated (attested mode), its pubkey MUST be distinct from the challenger and the opponent.
Match-terms tags
| Tag | Cardinality | Value | Description |
|---|---|---|---|
game |
exactly one | game identifier | the broader game context shared by both players |
variant |
zero to two | pubkey + variant identifier | variant assigned to a player |
time_control |
one or more | duration + optional increment + optional plies | session time-accounting period |
seat |
zero or one | first or second |
seat declared by the challenger (the challenged player implicitly receives the other value) |
rules |
exactly one | digest + optional retrieval hint | the rule-system document the session is played under |
At least one time_control tag MUST be present. Multiple time_control tags MAY be present to encode multi-period time controls (e.g., classical chess time controls with extra time after a given move count).
A game identifier and a variant identifier are each a non-empty ASCII string starting with a lowercase letter, followed by zero or more lowercase letters or digits, with total length between 1 and 32 characters inclusive. Each MUST match the regular expression:
^[a-z][a-z0-9]{0,31}$
This NIP does not prescribe a vocabulary for these identifiers. The interpretation of a given game or variant identifier is the responsibility of the consuming application. Conceptually:
gameidentifies the broader game context. Examples:chess(a standalone chess application),sanki(a multi-game 8x8 platform),go,xiangqi,shogi.variantidentifies the specific variant played within that game. Examples:chess,ogi,xiongqi.
The relationship between game and variant is application-defined. A given application MAY (and typically does) constrain which variants are valid within each game it supports, including constraints such as a shared board dimension. The protocol itself does not validate this relationship; it is the responsibility of the consuming application.
The game tag is structured ["game", "<game_id>"] and applies to the entire session (singleton).
Each variant tag is structured ["variant", "<pubkey>", "<variant_id>"]:
- The second element is the pubkey of the player to whom this variant is assigned. It MUST be either the challenger’s pubkey or the challenged player’s pubkey.
- The third element is the variant identifier.
Each player’s variant is OPTIONAL in the Direct Challenge — the challenger’s own (the variant tag whose second element matches the event’s pubkey) as well as the challenged player’s. Any variant the Direct Challenge does not fix is supplied by the challenged player in the Game Session (kind 3422 §Variants): the (Direct Challenge, Game Session) pair always jointly fixes both variants. A challenger who omits their own variant thereby delegates its choice to the challenged player; a challenger who omits the challenged player’s variant leaves that player free to choose their own, as before.
When both players’ variants are specified and they differ, the session is a multi-variant session. Multi-variant sessions are valid at the protocol level; whether a rating filter binds across them depends on the declared pool scope (kind 3418 §Match-terms tags): excluded under a pervariant scope, rated together under a pergame scope that unifies the game’s variants.
A time_control tag describes one period of session-wide time accounting:
["time_control", "<duration>", "<increment>", "<plies>"]
| Element | Position | Type | Required | Description |
|---|---|---|---|---|
| duration | 2nd | non-negative integer (seconds) | yes | Time budget for the period |
| increment | 3rd | non-negative integer (seconds) | no | Fischer increment per ply within this period |
| plies | 4th | strictly positive integer | no | Move-count quota — period covers N plies, then repeats |
Trailing optional fields MAY be omitted. If increment is omitted, plies MUST also be omitted. Each present element is a bare decimal integer matching ^(0|[1-9][0-9]*)$ — no leading zeros — and an empty-string element is malformed: omit the element rather than leaving it empty. A duration of 0 is valid only in the three-element per-move form (duration: 0 + increment + plies, the last row below); in any other form the duration MUST be strictly positive. An omitted increment and an explicit increment of 0 are distinct configurations: no normalization is applied anywhere in the suite, so ["time_control", "300"] and ["time_control", "300", "0"] are different time controls — and with leading zeros excluded, comparing configurations for identity (kind 3419 §Match-terms tags; §Rematch challenge) reduces to element-wise string equality.
The semantics:
| Configuration | Semantic |
|---|---|
duration only |
Fixed time bank; duration depletes linearly per ply. |
duration + increment |
Fischer time control: increment is added to the budget after each ply. |
duration + increment + plies |
Quota-based time control (byōyomi, Canadian): the period covers plies plies, then resets. |
duration: 0 + increment + plies |
Per-move time accounting with no main clock. |
When multiple time_control tags are present, they are applied sequentially in tag order (e.g., main time bank followed by overtime periods). The precise per-ply accounting — spend rules, increment timing, period transitions, and timeout detection — is rule-system-defined; the rule-system document named by rules specifies it (for the reference sanki rule system: Time Accounting — Sanki, incorporated in the kernel the manifest names).
A seat tag declares the seat the signer (the challenger) claims in the proposed session:
["seat", "<seat_name>"]
- The second element is a seat-name, one of exactly two protocol-defined values:
first(moves first) orsecond(moves second). Seat-names are play-order indices, not game vocabulary: every two-player, alternating-turn game has a first mover, which is the only invariant the protocol needs. The traditional labels (white/blackfor chess, sente/gote for shogi, red/black for xiangqi) are presentation: the rule system maps its labels ontofirst/second, and applications display whichever the game calls for. - The challenged player implicitly receives the other value (
first↔second). - Because the Direct Challenge is signed by the challenger, the seat carried by the
seattag is always the challenger’s. To assign the challenged player to a specific seat, the challenger declares the other value for themselves.
If the seat tag is absent, the seat assignment is unfixed and supplied by the challenged player in the Game Session.
A rules tag names the rule system the two players commit to for the session:
["rules", "<digest>", "<hint>"]
- The second element is the SHA-256 digest, as 64 lowercase hexadecimal characters (
^[0-9a-f]{64}$), of the rule-system document governing the session: the complete, machine-readable statement of the rules under which every Ply is judged and the session is concluded — movement, legality, terminations, time accounting, and whatever else the game requires. The document is content-addressed: its digest is its identity, and a document with a different digest is a different rule system, even for the samegame. - The third element is OPTIONAL: a retrieval hint, the URL of a server from which the document can be fetched by its digest (a Blossom server, or any HTTP location the application understands). It is a convenience, never an identity: a consumer that retrieves the document MUST verify that its SHA-256 equals the digest and MUST NOT trust a document whose digest differs, whatever the hint. An empty string MAY stand in its position.
The format of the rule-system document is defined by the consuming application for each game — this NIP does not prescribe it, exactly as it does not prescribe the encodings of positions and moves. For the reference sanki rule system, the document is the manifest specified in Rule System — Sanki (in this suite’s application layer): the identifier and digests of the fixed rule kernel it targets, the pseudo-legal movement tables (GGN) of each variant, and the parameters of the game’s terminations, captures and promotions. The document MUST prescribe a deterministic initial position for the session’s variants: the suite designates no party to draw a randomized setup.
rules completes game and variant: game names the family, variant selects each player’s piece set and movement within the document, rules says which document. Every consumer of the session — the players’ clients, observers, rating authorities — applies exactly this document and nothing else, and a session can be replayed from public events alone to the same verdict by anyone holding the document: this is what allows the suite to do without an arbiter, the founding (kind 3422) and the conclusion (kind 3425) being signed by the players and verified, not believed. A rule-system document is frozen for the life of the session: a revised document (a rebalancing, a fix) is a new digest, which applies only to sessions founded under it.
Lifecycle tags
| Tag | Cardinality | Value | Description |
|---|---|---|---|
accept_until |
exactly one | unix timestamp (seconds) | the latest moment at which a Game Session founded on this challenge is valid |
nonce |
exactly one | nonce + difficulty | NIP-13 proof of work |
The accept_until value (second element) MUST be a Unix timestamp in seconds, strictly greater than the event’s created_at. A Game Session whose canonical timing is greater than this value MUST be treated as invalid by verifiers; see kind 3422 §Semantic constraints (constraint 9) and Canonical Timing. A NIP-40 expiration tag MUST NOT be carried: NIP-40 authorizes relays to delete the event, and a consumed Direct Challenge must remain retrievable indefinitely — the Game Session’s founding reference is verified against it (kind 3422 §Semantic constraints), so garbage-collecting an expired-but-accepted challenge would amputate the founding evidence of a live session.
The nonce tag follows NIP-13.
Rematch tags
| Tag | Cardinality | Value | Description |
|---|---|---|---|
e rematch_of |
zero or one | event id, optional relay hint, marker rematch_of |
the concluded Game Session (kind 3422) whose rematch is proposed |
e concluded_by |
zero or one | event id, optional relay hint, marker concluded_by |
a Conclusion (kind 3425) of that same session — the proof that it is over |
A Direct Challenge carries either both rematch tags or neither. When both are present the event is a rematch challenge and §Rematch challenge applies; otherwise it carries no e tag at all (a fresh challenge references no prior suite event).
Rematch challenge
A rematch challenge proposes to replay a concluded session between the same two players, with the seats swapped and every other term reused. It is an ordinary Direct Challenge — accepted the same way, by the Game Session (kind 3422) the challenged player publishes — whose terms are constrained rather than chosen:
- Players. The signer MUST be one of the two
player-marked pubkeys of the concluded Game Session, and theopponentMUST be the other. - Proof of conclusion.
concluded_byMUST reference a conforming Conclusion (kind3425) whosegame_sessionreference names the same event asrematch_of. Any conforming Conclusion of the session serves, canonical or not: it is a proof of conclusion, not of outcome — the session’s outcome remains the canonical Conclusion’s alone (kind3425§Idempotence and finality). A rematch cannot be proposed for a session that has not concluded. - Inherited terms. The
gameandrulestags MUST equal the concluded Game Session’s. Thetime_controltags MUST equal the concluded session’s element-wise and in order. Bothvarianttags MUST be present and MUST assign each player the variant they held in the concluded session (the concluded Game Session’svarianttags). The timing designation MUST equal the concluded Game Session’s (the sametimestamper, or the sametiming_relayset). - Swapped seats. The
seattag MUST be present and MUST carry the other value than the one the signer held in the concluded session (itsseattag there): the challenged player thereby receives the seat the challenger held.
Nothing is left open: a rematch challenge fixes both variants and the seat, so the Game Session founded on it supplies no term of its own. Its accept_until is the offerer’s choice, as for any challenge; its content is free.
One rematch per concluded session. The two players MAY each publish a rematch challenge of the same session, in either order or simultaneously; each is acceptable by its addressee. Whichever Game Session is founded first canonically occupies the slot (rematch, concluded session) (kind 3422 §Idempotence and race resolution), and every other rematch challenge of that session — accepted or not — is moot: one concluded session founds at most one rematch, however many challenges its players publish. A rematch of the rematch is proposed, in turn, against the new concluded session.
Content
The content field MAY carry a free-form text message from the challenger to the challenged player (e.g., a greeting, context, or terms clarification). It is OPTIONAL: an empty string is permitted.
The content field MUST satisfy:
- Length: 0 to 255 Unicode code points inclusive (measured after NIP-01 JSON unescaping).
- Encoding: valid UTF-8.
- Forbidden characters: any code point in the
contentfield MUST NOT fall in the following ranges: - U+0000 to U+0008, U+000B, U+000C, U+000E to U+001F (C0 control characters, except line feed U+000A and horizontal tab U+0009).
- U+007F to U+009F (DEL and C1 control characters).
- U+202A to U+202E (bidirectional formatting and override characters).
- U+2066 to U+2069 (directional isolate characters).
The content field MUST NOT alter the interpretation of the challenge. The challenged player’s client MAY display it to the user, but MUST NOT base any normative decision (acceptance, seat selection, terms negotiation) on its value. The semantic meaning of the event is fully carried by the tags.
The protocol intentionally does NOT restrict characters such as <, >, &, ", ', `, or \ — these are legitimate text in many natural contexts (mathematics, programming notation, casual messaging across multiple scripts); handling them is the job of output-layer escaping in the client (see §Security considerations).
Semantic constraints
A conforming Direct Challenge event MUST satisfy all of the following:
- The event contains exactly one
opponent-markedptag and exactly one timing designation: either exactly onetimestamper-markedptag and notiming_relaytag (attested mode), or notimestamperand one or moretiming_relaytags each carrying awss://URL (self-timed mode, the default) — per the Canonical Timing NIP §Timing modes and mode selection. - The pubkeys of all
ptags MUST be distinct from the event’spubkey(the challenger). Theopponentpubkey MUST be distinct from thetimestamperpubkey (attested mode). - Exactly one
gametag is present, with a valid game identifier matching^[a-z][a-z0-9]{0,31}$. - Zero, one, or two
varianttags are present. Eachvariant’s third element is a valid variant identifier matching^[a-z][a-z0-9]{0,31}$. - Each
varianttag’s second element matches either the challenger’s pubkey or the challenged player’s pubkey. At most onevarianttag is present per pubkey. - At least one
time_controltag is present. Eachtime_controltag has a valid format (per §Match-terms tags: no leading zeros;duration0only in the three-element per-move form). - If a
seattag is present, its second element isfirstorsecond. - Exactly one
accept_untiltag is present, with a value strictly greater thancreated_at. - 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). - Exactly one
rulestag is present, whose second element matches^[0-9a-f]{64}$(per §Match-terms tags). - The event carries either no
etag, or exactly onerematch_of-marked and exactly oneconcluded_by-markedetag and no other. In the latter case (a rematch challenge) the event satisfies §Rematch challenge: the signer and theopponentare the two players of the referenced concluded Game Session;concluded_byreferences a conforming Conclusion of that session;game,rules, thetime_controlsequence, bothvarianttags and the timing designation equal the concluded Game Session’s; and theseattag is present and is the other value than the signer’s seat there.
Constraints 1–10 are checkable from the event alone; constraint 11 requires cross-event validation against the referenced Game Session and Conclusion. Whether the rule-system document named by rules is retrievable is not a conformance matter (§Validation responsibility).
Validation responsibility
Validation of a Direct Challenge against the semantic constraints (§Semantic constraints) is the responsibility of the client and the relay. A relay additionally enforces its advertised NIP-13 minimum difficulty at ingestion — a relay policy, distinct from conformance.
Validation of the proposed terms in the broader sense (e.g., whether the rule-system document named by rules is retrievable and implementable by the client, whether it defines the proposed game / variant combination, or whether the time-control configuration is one the client can account for) is the responsibility of the challenged player’s client when deciding whether to found the session. A client MUST NOT publish a Game Session for a challenge whose rule-system document it cannot retrieve and execute: it would be committing to a session it cannot play or conclude.
Separately, a deployment MAY gate the eligibility to impose certain optional terms behind its own policy — independent of format validity, which stays as above. Such gates are deployment policy layered on top of this generic NIP, not part of it.
Examples
The examples below use placeholder pubkeys, relay URLs, and timestamps for illustration.
Example 1 — All terms fixed by the challenger
Alice proposes a session to Bob in attested mode, designating Dave as timestamper. The game context is chess (a standalone chess application), and both players play the standard chess variant. Alice plays seat first with a 5-minute Fischer time control with 3-second increment.
{
"kind": 3420,
"pubkey": "<alice_pubkey>",
"created_at": 1700000000,
"tags": [
["p", "<bob_pubkey>", "wss://relay.example.com", "opponent"],
["p", "<dave_pubkey>", "wss://relay.example.com", "timestamper"],
["game", "chess"],
["rules", "3f6d1a0c9e4b2a7d5c8e1f0a9b3c7d2e4f6a8b0c1d3e5f7a9b2c4d6e8f0a1b3c", "https://blobs.example.com"],
["variant", "<alice_pubkey>", "chess"],
["variant", "<bob_pubkey>", "chess"],
["seat", "first"],
["time_control", "300", "3"],
["accept_until", "1700003600"],
["nonce", "62473", "16"]
],
"content": "Up for a game?",
"id": "...",
"sig": "..."
}
Bob accepts by publishing the Game Session (kind 3422) referencing this event; its canonical timing must not exceed 1700003600.
Example 2 — Terms partially deferred
Alice proposes a session against Bob in the chess game context. She fixes her own variant (chess) but leaves Bob’s variant and the seat assignment open for Bob to decide in the Game Session.
{
"kind": 3420,
"pubkey": "<alice_pubkey>",
"created_at": 1700000000,
"tags": [
["p", "<bob_pubkey>", "wss://relay.example.com", "opponent"],
["p", "<dave_pubkey>", "wss://relay.example.com", "timestamper"],
["game", "chess"],
["rules", "3f6d1a0c9e4b2a7d5c8e1f0a9b3c7d2e4f6a8b0c1d3e5f7a9b2c4d6e8f0a1b3c", "https://blobs.example.com"],
["variant", "<alice_pubkey>", "chess"],
["time_control", "600"],
["accept_until", "1700003600"],
["nonce", "11337", "16"]
],
"content": "",
"id": "...",
"sig": "..."
}
Example 3 — Fully deferred variants
Bob challenges Alice in the sanki game context without fixing any variant: he delegates the choice of his own variant to Alice, and leaves Alice free to choose hers. Alice supplies both in the Game Session (kind 3422), explicitly or via her client’s deterministic default.
{
"kind": 3420,
"pubkey": "<bob_pubkey>",
"created_at": 1700000000,
"tags": [
["p", "<alice_pubkey>", "wss://relay.example.com", "opponent"],
["timing_relay", "wss://relay.example.com"],
["game", "sanki"],
["rules", "3f6d1a0c9e4b2a7d5c8e1f0a9b3c7d2e4f6a8b0c1d3e5f7a9b2c4d6e8f0a1b3c", "https://blobs.example.com"],
["time_control", "300", "3"],
["accept_until", "1700003600"],
["nonce", "27193", "16"]
],
"content": "Your pick — both sides.",
"id": "...",
"sig": "..."
}
No eligibility gate is involved: Bob imposes nothing on Alice, and Alice completing Bob’s variant in the Game Session is a delegation Bob consented to by omission (see §Validation responsibility).
Example 4 — Self-timed session (no timestamper)
Alice proposes a sanki session to Bob in self-timed mode: no timestamper is designated, and a timing_relay tag names the relay whose acceptance will establish the session events’ canonical timing — its strict ingestion window (it refuses any event whose created_at is in the past relative to its clock, beyond a small past tolerance) makes each accepted event’s own created_at trustworthy.
{
"kind": 3420,
"pubkey": "<alice_pubkey>",
"created_at": 1700000000,
"tags": [
["p", "<bob_pubkey>", "wss://relay.example.com", "opponent"],
["timing_relay", "wss://relay.example.com"],
["game", "sanki"],
["rules", "3f6d1a0c9e4b2a7d5c8e1f0a9b3c7d2e4f6a8b0c1d3e5f7a9b2c4d6e8f0a1b3c", "https://blobs.example.com"],
["variant", "<alice_pubkey>", "ogi"],
["variant", "<bob_pubkey>", "chess"],
["seat", "first"],
["time_control", "300", "3"],
["accept_until", "1700003600"],
["nonce", "62473", "16"]
],
"content": "Up for a game of sanki?",
"id": "...",
"sig": "..."
}
Example 5 — Sashité Sanki session with multi-period time control
Alice proposes a Sanki session against Bob: the game is sanki (a multi-game 8x8 platform), and both players choose the ogi variant (an 8x8 adaptation of shōgi). 60 minutes main time, followed by 30 seconds per ply overtime.
{
"kind": 3420,
"pubkey": "<alice_pubkey>",
"created_at": 1700000000,
"tags": [
["p", "<bob_pubkey>", "wss://relay.example.com", "opponent"],
["p", "<dave_pubkey>", "wss://relay.example.com", "timestamper"],
["game", "sanki"],
["rules", "3f6d1a0c9e4b2a7d5c8e1f0a9b3c7d2e4f6a8b0c1d3e5f7a9b2c4d6e8f0a1b3c", "https://blobs.example.com"],
["variant", "<alice_pubkey>", "ogi"],
["variant", "<bob_pubkey>", "ogi"],
["time_control", "3600"],
["time_control", "0", "30", "1"],
["accept_until", "1700003600"],
["nonce", "84217", "16"]
],
"content": "",
"id": "...",
"sig": "..."
}
The two time_control tags express sequential periods: first a 60-minute fixed bank, then per-ply overtime.
Example 6 — Rematch challenge
Alice and Bob’s self-timed sanki session (Game Session <session_id>, Alice first playing ogi, Bob second playing chess, 5+3) has concluded — Bob published its Conclusion (<conclusion_id>). Alice proposes the rematch: same terms, and she now takes seat second. Bob accepts by publishing the new Game Session (kind 3422), which occupies the slot (rematch, <session_id>).
{
"kind": 3420,
"pubkey": "<alice_pubkey>",
"created_at": 1700009000,
"tags": [
["e", "<session_id>", "wss://relay.example.com", "rematch_of"],
["e", "<conclusion_id>", "wss://relay.example.com", "concluded_by"],
["p", "<bob_pubkey>", "wss://relay.example.com", "opponent"],
["timing_relay", "wss://relay.example.com"],
["game", "sanki"],
["rules", "3f6d1a0c9e4b2a7d5c8e1f0a9b3c7d2e4f6a8b0c1d3e5f7a9b2c4d6e8f0a1b3c", "https://blobs.example.com"],
["variant", "<alice_pubkey>", "ogi"],
["variant", "<bob_pubkey>", "chess"],
["seat", "second"],
["time_control", "300", "3"],
["accept_until", "1700009600"],
["nonce", "40961", "16"]
],
"content": "Again?",
"id": "...",
"sig": "..."
}
Had Bob published his own rematch challenge at the same time (seat first, addressed to Alice), either Game Session founded on either challenge would do; the first canonical one wins the slot and the other challenge is moot.
Client guidelines
Clients implementing kind: 3420 SHOULD:
- Subscribe to kind
3420with a#pfilter on the user’s pubkey to receive incoming challenges. - Inspect the role marker (fourth element of each
ptag) to identify whether the user is theopponentof a given challenge. - Surface to the user the session’s timing designation — the designated timestamper’s identity (attested mode) or the designated timing relay URLs (self-timed mode) — since it provides authoritative timing for both clock accounting and race resolution. The user SHOULD assess whether they trust the designated parties before accepting.
- Validate received Direct Challenge events against the semantic constraints (§Semantic constraints) before presenting them to the user.
- Before offering to accept, retrieve the rule-system document named by
rules, verify its digest, and check that the client implements the kernel it names; a challenge whose document the client cannot execute SHOULD be presented as unplayable, never accepted. - Filter out challenges whose
accept_untilhas already elapsed. - Allow users to dismiss unwanted challenges locally without publishing any event.
- Respect the user’s NIP-51 mute list (kind
10000): Direct Challenges signed by a pubkey listed in the user’s mute list SHOULD be silently filtered out from display, unless the user explicitly opts in to view challenges from muted senders. - Before sending a Direct Challenge, fetch the recipient’s Challenge Policy (kind
30420) for the proposedgameand apply the lookup rules defined in that NIP’s §Mode lookup (the policy is per-game and variant-agnostic). If the policy is likely to filter the challenge (e.g.,nobodymode,followingmode without the sender being followed,ratingmode with the sender outside the rating threshold — or with no determinable comparison pool), warn the sender before publishing — the challenge would consume proof of work without ever being surfaced to the recipient. - When receiving a Direct Challenge, apply the user’s Challenge Policy (kind
30420) filter for the proposedgame(per that NIP’s §Mode lookup), after the NIP-51 mute list filter. Challenges that do not satisfy the policy SHOULD be silently filtered out from display, unless the user explicitly opts in. - When displaying a Direct Challenge to the user, attempt to verify the NIP-05 identifier of the challenger (event signer). Display verified NIP-05 identifiers prominently. When verification fails (network error, DNS issue, or no identifier published), display the pubkey directly and indicate the identity as unverified — but do not block the challenge solely on this basis.
- Present a rematch challenge as such — “rematch of your game against …” — and offer it only against a session whose Conclusion the client has itself observed, since the challenge’s proof of conclusion is checkable.
Clients MAY:
- Allow the challenger to attach a free-form text message via
content, subject to the constraints in §Content (255 code points, no control or bidi-override characters). - Display received
contentto the challenged player with appropriate output escaping (§Security considerations) and without altering challenge logic. - Offer the user a “rematch” affordance on a concluded session that publishes a rematch challenge with the swapped seat and the inherited terms filled in.
Security considerations
-
Signatures: Direct Challenge events are signed by the challenger. Receiving clients MUST verify signatures per NIP-01 before presenting them to the user.
-
Proof of work: The
noncetag follows NIP-13 and protects against unsolicited spam challenges. Relays SHOULD enforce a minimum difficulty appropriate to their policy. The nonce values shown in this NIP’s examples are illustrative placeholders; real implementations compute the nonce via proof-of-work brute force until the event ID achieves the target leading-zero-bit difficulty. -
Deadline enforcement: The
accept_untiltag is a signed business-logic constraint. Verifiers MUST reject Game Sessions whose canonical timing is greater than the corresponding challenge’saccept_until; see kind3422§Semantic constraints (constraint 9) and Canonical Timing. -
Decline by silence: This NIP does not define a “decline” event. A challenged player refuses by not publishing a Game Session before
accept_until. Clients MAY mark unwanted challenges as locally dismissed for UX purposes without publishing any event. -
Trust model: The suite designates no arbiter: the rules are the content-addressed document both players sign, and every verdict is recomputable from public events, so no party’s honesty is presumed on the rules axis. What a player still trusts is the timing authority — a timestamper to attest honestly, or a timing relay to enforce its ingestion window and not to backdate or drop events — since timing is an input of the verdict, not a computation. The protocol cryptographically constrains specific adversarial behaviors but is explicit about its limits with respect to others. In brief: signatures bind WHAT a party may declare; the rule-system document binds what a verdict may say; WHETHER and WHEN the timing authority acts is reputational; NIP-26-delegated signatures from a timestamper are rejected by default; and NIP-05 identity verification mitigates vanity-pubkey impersonation. The full enumeration belongs to the consuming application’s documentation (this suite’s application layer maintains one); challengers SHOULD review it before designating a timing authority for stakes that would justify dispute.
-
Rule-system availability: A challenge names its rule system by digest; if the document is not retrievable, the session cannot be played or verified. Clients SHOULD retrieve and cache the document before founding (§Client guidelines), and deployments SHOULD keep every document they ever named retrievable for as long as any session, rating pool or composition refers to it.
-
Rematch proof: A rematch challenge is valid only against a concluded session, which it proves by reference to a conforming Conclusion; a verifier checks that proof against the Conclusion’s own
game_sessionreference, never against the challenger’s word. A challenge against a session that has not concluded is non-conforming (constraint 11) and MUST be ignored. -
Privacy implications: Publishing game events discloses information about the player and the session — the player’s pubkey, opponent pubkeys, move data, outcomes, timing, and (via aggregation) activity patterns and skill profiles. None of this can be hidden by a public-relay protocol; the mitigations are identity-side (a dedicated pubkey per context). The full enumeration of direct and inferable disclosures belongs to the consuming application’s documentation (this suite’s application layer maintains one); players SHOULD review it before publishing events under an identity they wish to protect.
- Harassment mitigation: A determined adversary can repeatedly send Direct Challenges to a target. The NIP-13 proof of work (§Lifecycle tags) raises the per-event cost but does not eliminate targeted harassment from a motivated sender. To protect users:
- Clients SHOULD integrate NIP-51 mute lists (kind
10000): incoming Direct Challenges from pubkeys in the user’s mute list SHOULD NOT be surfaced without explicit opt-in. - Clients SHOULD provide a UI affordance to mute a challenger from within the challenge view (e.g., a “block this user” action that adds the challenger’s pubkey to the user’s mute list).
- Relays MAY apply mute-list-aware filtering on the wire, though this is out of scope for this NIP.
NIP-13 proof of work and NIP-51 mute lists are complementary: PoW raises the per-event cost of bulk spam; mute lists eliminate persistent senders entirely.
-
Challenge policy: Users MAY publish a Challenge Policy (kind
30420) declaring which Direct Challenges they wish to receive for each game (the policy is variant-agnostic within a game). The policy is advisory: it does not prevent a determined sender from publishing challenges that will be filtered by the recipient’s client. Compliant sender clients SHOULD respect the policy to avoid wasting proof of work on challenges that will be silently filtered. The policy is applied AFTER the NIP-51 mute list filter — a muted pubkey is filtered regardless of policy mode. - Content rendering: The
contentfield is untrusted user input controlled by the challenger. Although the protocol restricts the character set (§Content) to exclude C0/C1 control characters and Unicode bidirectional override characters — mitigating “trojan source” attacks (CVE-2021-42574), null-byte injection, and ANSI-escape injection — clients displaying the field MUST apply context-appropriate output escaping: - HTML context: escape
<,>,&,",'. - Markdown context: escape characters significant to the rendering flavor (
`,*,_,[,], etc.). - Terminal context: avoid passing the content through interpreters that may execute escape sequences.
The protocol-level character restrictions do not replace context-appropriate output escaping. They close a narrow class of attacks that escaping alone cannot address.
-
Content trust: A malicious challenger can place anything within the allowed character set into
content, including phishing attempts, social engineering, or misleading claims about the challenger’s identity. Clients SHOULD treat the field as adversarial input and present it accordingly (e.g., visually distinct from system-generated text, clearly attributed to the challenger). -
Scope: This NIP does not itself provide the session’s founding, play, or termination semantics. Those concerns are addressed by other NIPs in the suite (Game Session, Ply, Conclusion).
References
- NIP-01 — Basic protocol flow description
- NIP-05 — Mapping Nostr keys to DNS-based internet identifiers
- NIP-09 — Event Deletion Request
- NIP-13 — Proof of Work
- NIP-51 — Lists
- Event Timestamp Attestation — the primitive on which the suite’s timing is built
- Canonical Timing — timing modes, mode selection, canonical timing, meta-resolution
- Self-Timed Timing Relay — the relay profile behind the
timing_relaydesignation
