Sashité for Developers
  1. Sashité for Developers
  2. Nostr
  3. NIPs
  4. 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, designates an arbiter and, optionally, a timestamper, and carries the proposed session terms.

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 arbiter and an optional timestamper, and carries the game context (shared), an optional proposed variant for each player, a time-control configuration, an optional seat assignment, and an acceptance deadline.

A complementary acceptance event (defined separately) responds to a Direct Challenge to instantiate the session. A challenge that receives no acceptance before its deadline expires without effect.

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 responds with an acceptance event (instantiating the session) 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 — are determined by the designated arbiter’s rule system.

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 their acceptance event. 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.

Operating mode

This NIP requires a designated arbiter, plus exactly one timing designation selecting the session’s timing mode per the Canonical Timing NIP §Timing modes and mode selection:

The designation fixes the timing mode (Canonical Timing §Canonical timing of an event):

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. The arbiter and, when used, the timestamper MAY be the same pubkey (two p tags with different markers, same value); combined-role configurations weaken the structural defense against timing-related misbehavior (§Security considerations).

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 Accepted Challenge (kind 3421), the Game Session (kind 3422), each Ply (kind 3423), each Adjudication Request (kind 3424), and the Adjudication (kind 3425) — and, on the matchmaking path, the Pairing (kind 3419) in the same position as the Accepted Challenge. 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 role designations propagate to the complementary acceptance event, which MUST mirror them exactly — the same arbiter, and the same timing designation (the same timestamper, or the same timing_relay set).

Tags

Player tags

Tag Cardinality Value Description
p two or three pubkey, optional relay hint, role marker exactly one opponent (the challenged player); exactly one arbiter; 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:

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:

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)

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:

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>"]:

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 acceptance event (kind 3421 §Match-terms tags): the (Direct Challenge, Accepted Challenge) 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) 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 game’s supporting documentation specifies it (for the reference sanki rule system: Time Accounting — Sanki, in this suite’s application layer).

A seat tag declares the seat the signer (the challenger) claims in the proposed session:

["seat", "<seat_name>"]

If the seat tag is absent, the seat assignment is unfixed and supplied in the acceptance event.

Lifecycle tags

Tag Cardinality Value Description
accept_until exactly one unix timestamp (seconds) the latest moment at which an acceptance event 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. An Accepted Challenge whose canonical timing is greater than this value MUST be treated as invalid by verifiers; see kind 3421 §Semantic constraints (constraint 6) for the precise rule 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 Accepted Challenge’s founding reference is verified against it (kind 3421 §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.

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:

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:

  1. The event contains exactly one opponent-marked p tag, exactly one arbiter-marked p tag, and exactly one timing designation: either exactly one timestamper-marked p tag and no timing_relay tag (attested mode), or no timestamper and one or more timing_relay tags each carrying a wss:// URL (self-timed mode, the default) — per the Canonical Timing NIP §Timing modes and mode selection.
  2. The pubkeys of all p tags MUST be distinct from the event’s pubkey (the challenger). The opponent pubkey MUST be distinct from the arbiter and (in attested mode) timestamper pubkeys. The arbiter and timestamper pubkeys MAY be identical (same actor cumulating both roles).
  3. Exactly one game tag is present, with a valid game identifier matching ^[a-z][a-z0-9]{0,31}$.
  4. Zero, one, or two variant tags are present. Each variant’s third element is a valid variant identifier matching ^[a-z][a-z0-9]{0,31}$.
  5. Each variant tag’s second element matches either the challenger’s pubkey or the challenged player’s pubkey. At most one variant tag is present per pubkey.
  6. At least one time_control tag is present. Each time_control tag has a valid format (per §Match-terms tags: no leading zeros; duration 0 only in the three-element per-move form).
  7. If a seat tag is present, its second element is first or second.
  8. Exactly one accept_until tag is present, with a value strictly greater than created_at.
  9. 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 1–9 are checkable from the event alone.

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 designated arbiter supports the proposed game / variant combination, whether the variant is permitted within the game per the application’s rules, or whether the time-control configuration is honored) is the responsibility of the challenged player’s client when deciding whether to accept the challenge. If the challenged player accepts terms that the arbiter cannot honor, the resulting session may fail at the arbitration step (out of scope for this NIP).

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, designating Carol as arbiter. 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", "<carol_pubkey>", "wss://relay.example.com", "arbiter"],
 ["p", "<dave_pubkey>", "wss://relay.example.com", "timestamper"],
 ["game", "chess"],
 ["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": "..."
}

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 at acceptance.

{
 "kind": 3420,
 "pubkey": "<alice_pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["p", "<bob_pubkey>", "wss://relay.example.com", "opponent"],
 ["p", "<carol_pubkey>", "wss://relay.example.com", "arbiter"],
 ["p", "<dave_pubkey>", "wss://relay.example.com", "timestamper"],
 ["game", "chess"],
 ["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 her acceptance (kind 3421), 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"],
 ["p", "<carol_pubkey>", "wss://relay.example.com", "arbiter"],
 ["timing_relay", "wss://relay.example.com"],
 ["game", "sanki"],
 ["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 at acceptance 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, designating Carol as arbiter and running 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"],
 ["p", "<carol_pubkey>", "wss://relay.example.com", "arbiter"],
 ["timing_relay", "wss://relay.example.com"],
 ["game", "sanki"],
 ["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", "<carol_pubkey>", "wss://relay.example.com", "arbiter"],
 ["p", "<dave_pubkey>", "wss://relay.example.com", "timestamper"],
 ["game", "sanki"],
 ["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.

Client guidelines

Clients implementing kind: 3420 SHOULD:

  1. Subscribe to kind 3420 with a #p filter on the user’s pubkey to receive incoming challenges.
  2. Inspect the role marker (fourth element of each p tag) to identify whether the user is the opponent or the arbiter for a given challenge.
  3. 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.
  4. Validate received Direct Challenge events against the semantic constraints (§Semantic constraints) before presenting them to the user.
  5. Verify out-of-band that the designated arbiter is reachable and operational before allowing the user to accept.
  6. Filter out challenges whose accept_until has already elapsed.
  7. Allow users to dismiss unwanted challenges locally without publishing any event.
  8. 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.
  9. Before sending a Direct Challenge, fetch the recipient’s Challenge Policy (kind 30420) for the proposed game and 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., nobody mode, following mode without the sender being followed, rating mode 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.
  10. When receiving a Direct Challenge, apply the user’s Challenge Policy (kind 30420) filter for the proposed game (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.
  11. When displaying a Direct Challenge to the user, attempt to verify the NIP-05 identifier of the challenger (event signer) and of the designated arbiter. 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.

Clients MAY:

Security considerations

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.

The protocol-level character restrictions do not replace context-appropriate output escaping. They close a narrow class of attacks that escaping alone cannot address.

References