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

NIP-XX

Pairing

draft optional

This NIP defines a Nostr event kind by which a designated matchmaker pairs two compatible Open Challenges (kind 3418) into a declared session for a turn-based, two-player abstract strategy board game of the chess family. The Pairing is the founding event of the matchmaking path: it stands in the same position as the Direct Challenge (kind 3420) does for the directed path, and the resulting session is founded, played, and concluded by the same downstream NIPs.

Abstract

Defines kind: 3419, a regular event signed by a matchmaker that references two Open Challenges (kind 3418) and binds them into a session. The Pairing names the two players, mirrors the timing designation authorized by both Open Challenges, resolves each player’s variant, draws the seats, and carries the agreed time control and rule system. Either player then founds the session by publishing a Game Session (kind 3422) that references the Pairing; the Pairing leaves nothing open, so the Game Session is fully determined.

Neither player signs the Pairing. Their consent is pre-committed in their Open Challenges: a Pairing is conforming only if it respects the terms both referenced Open Challenges declared (§Consent constraints). This bounds the matchmaker’s authority to selecting which compatible pair to match, when, and which concrete values to choose within each Open Challenge’s authorization.

Motivation

A symmetric matchmaking pool needs an event that turns two independent “I am available” authorizations into one session, signed by a party neither player is. The Pairing is that event. The matchmaker observes the pool of Open Challenges (kind 3418) that authorize it, finds two that are mutually compatible, and publishes a Pairing referencing both.

The design separates pool entry (kind 3418, signed by each player) from pairing (this NIP, signed by the matchmaker) so that:

The Pairing declares the session. It becomes playable when either player publishes the Game Session (kind 3422); time-control accounting anchors at t₀, established by the Game Session’s canonical timing. See §From Pairing to Game Session.

Specification

Event kind

kind: 3419

Pairing events are regular events per NIP-01: immutable once signed and broadcast.

Signing party

The event MUST be signed by the pubkey designated as matchmaker by both referenced Open Challenges (kind 3418). Any other signer’s event is not a conforming Pairing for those Open Challenges and MUST be ignored. The matchmaker is identified by signature; it is not carried as a p tag.

NIP-26-delegated signatures from the matchmaker are NOT accepted as authoritative, consistent with the suite’s delegation policy for designated parties (kind 3420 §Security considerations).

Attestation by the designated timestamper (attested mode)

This section applies in attested mode only (a timestamper is designated by both Open Challenges). In self-timed mode the Pairing’s canonical timing is its own created_at, established by acceptance at a designated timing relay (Canonical Timing §Canonical timing of an event); it then serves the same roles — greedy matching, the accept-deadline check and the founding window — without an attestation.

The designated timestamper publishes an Event Timestamp Attestation (kind 3410) referencing this Pairing via the required attests-marked e tag. The attestation’s created_at is then the Pairing’s canonical timing for greedy matching (§Race resolution) and for the accept-deadline check against each Open Challenge’s accept_until (§Consent constraints).

["e", "<pairing_event_id>", "<relay_hint>", "attests"]

Only the attests-marked e tag is required; it carries no discovery marker, because the Game Session does not yet exist when the Pairing is attested. Until the Pairing has canonical timing (an attestation in attested mode, or established acceptance by a designated timing relay in self-timed mode), it is pending (Canonical Timing §The pending state): it has no position in greedy matching and cannot yet be founded. NIP-26-delegated signatures from the timestamper are NOT accepted as authoritative.

In attested mode, the Pairing’s own created_at is informational and MUST NOT be used for any timing-dependent decision; in self-timed mode it IS the canonical timing, made non-backdatable by the designated timing relay’s ingestion window. See Canonical Timing.

Tags

Reference tags

Tag Cardinality Value Description
e open_challenge exactly two event id, optional relay hint, marker the two Open Challenges (kind 3418) being paired

Each reference is ["e", "<open_challenge_event_id>", "<relay_hint>", "open_challenge"]. The two referenced Open Challenges MUST have distinct signers (the two players). The relay hint is OPTIONAL but SHOULD be present.

Player and authority tags

Tag Cardinality Value Description
p two or three pubkey, optional relay hint, role marker exactly two player; and, in attested mode, exactly one timestamper (absent in self-timed mode, the default)

The p tags serve a dual purpose: semantic (the participants and the timing authority) and structural (routing via standard #p filters, so each player receives the Pairing). The timing designation (timestamper, or timing_relay tags) is carried explicitly so the Game Session (kind 3422) can mirror and verify it by the same logic it uses for the directed path.

Match-terms tags

Tag Cardinality Value Description
game exactly one game identifier the game context (equal to both Open Challenges’)
variant exactly two (one per player) pubkey + variant identifier each player’s resolved variant
time_control one or more duration + optional increment + optional plies the agreed time-control configuration
rules exactly one digest + optional retrieval hint the rule-system document (equal to both Open Challenges’)
seat exactly two (one per player) pubkey + seat-name each player’s seat, drawn by the matchmaker

The game tag is ["game", "<game_id>"] and MUST equal the game of both referenced Open Challenges.

Each variant tag is ["variant", "<player_pubkey>", "<variant_id>"]pubkey-based, now that both players are known (the role→pubkey transition described in kind 3418 §Role-based ownership). Exactly one per player, unconditionally: the Pairing resolves every preference left open by the Open Challenges, so both players’ variants are fixed here — a single-variant game simply repeats its one style’s identifier. The resolved variants MUST satisfy both Open Challenges’ role preferences (§Consent constraints). The identifiers’ meaning stays opaque to the protocol (the rule-system document named by rules interprets them), mirroring kind 3422 §Variants.

The time_control tag uses the format and multi-period semantics of the Direct Challenge (kind 3420 §Match-terms tags) and MUST be identical to the time-control configuration of both referenced Open Challenges. Identity is compared on the parsed period sequence: the same number of periods in the same order, each pair equal in duration, in increment presence and value, and in plies presence and value. No normalization is applied: a period with no increment and a period with an explicit increment of 0 are not identical (kind 3420 §Match-terms tags).

The rules tag is ["rules", "<digest>", "<hint>"] (kind 3420 §Match-terms tags) and MUST carry the digest of both referenced Open Challenges, which MUST be equal: the rule-system document is a matching term, not a term the matchmaker resolves. The hint MAY be either Open Challenge’s or the matchmaker’s own.

Each seat tag is ["seat", "<player_pubkey>", "<seat_name>"], the seat-name being first or second (kind 3420 §Match-terms tags); the two players MUST hold the two distinct values. The matchmaker draws the seats — a fair random draw, typically — since the pool path has no challenger to declare one and the suite designates no other party to draw: the matchmaker is already the party whose choices within consented bounds the pool accepts, and an explicit draw here is preferable to any derivation from event ids it could grind. The Game Session mirrors the assignment (kind 3422 §Seats).

Lifecycle tags

Tag Cardinality Value Description
found_until exactly one unix timestamp (seconds) the latest moment at which a Game Session founded on this Pairing is valid

The found_until value MUST be a Unix timestamp in seconds, strictly greater than the event’s created_at. It bounds the founding window: a Game Session (kind 3422) whose canonical timing exceeds it is non-conforming, the Pairing lapses, and the two players re-enter the pool. The window is the matchmaker’s choice — a deployment sets it to the interval within which a connected client founds automatically (a minute or two), so that a player who disconnected between pool entry and pairing does not hold the other in a session that never starts.

There is no nonce tag: a Pairing is signed by a designated third party, not subjected to proof of work. Spam resistance for the founding path is carried by the proof of work on the Open Challenges (kind 3418).

Content

The content field MUST be the empty string (""). A Pairing carries no narrative payload; its semantics are fully expressed by its tags.

A conforming Pairing event MUST satisfy all of the following. Constraints 4 and 12–15 are checkable from the event alone; the rest require the two referenced Open Challenges (and, for constraints 10 and 11, external data: the Pairing’s canonical timing, the filtering players’ contact lists, and the pinned rating attestations).

  1. Exactly two open_challenge-marked e tags, referencing retrievable, signature-valid, conforming Open Challenge events (kind 3418, per its §Semantic constraints) whose signers are distinct.
  2. The event’s pubkey (the matchmaker) equals the matchmaker-marked pubkey of both referenced Open Challenges.
  3. Exactly two player-marked p tags, whose pubkeys equal the signers of the two referenced Open Challenges (one each).
  4. Exactly two seat tags, one per player pubkey, carrying first and second (one each).
  5. Timing designation matches both Open Challenges (Canonical Timing §Timing modes and mode selection): in attested mode (both designate the same timestamper), exactly one timestamper-marked p tag whose pubkey equals that designation, and no timing_relay tag; in self-timed mode (both designate the same timing_relay set), timing_relay tags equal to that set (as a set), and no timestamper-marked p tag. A Pairing whose timing designation disagrees with either Open Challenge — or two Open Challenges whose designations differ from each other — cannot found a session; such a Pairing is non-conforming.
  6. Exactly one game tag whose value equals the game of both Open Challenges.
  7. Exactly two variant tags are present, one per player pubkey, and the resolved per-player variants satisfy, for each player P with resolved variant vP: (a) if P’s own Open Challenge carries a self variant, vP equals it; and (b) if the other player’s Open Challenge carries an opponent variant, vP equals it. A player whose variant is constrained by neither (a) nor (b) MAY be assigned any variant by the matchmaker — but the assignment is always written: the Pairing carries one resolved variant per player, unconditionally.
  8. The time_control configuration is identical to that of both Open Challenges.
  9. Exactly one rules tag is present, whose digest (second element) equals the rules digest of both Open Challenges. Two Open Challenges with different rules digests are incompatible and MUST NOT be paired, whatever their other terms.
  10. Each player satisfies the other player’s Open Challenge filter, evaluated at the Pairing’s canonical timing:
    • for everyone (or an absent filter): trivially;
    • for following: the filtering player follows the other, per the filtering player’s most recent NIP-02 contact list (kind 3) whose created_at is less than or equal to the Pairing’s canonical timing — the suite’s uniform at-or-before anchoring for consulting a replaceable list. The contact list being a replaceable event, this leg is fully verifiable only by consumers who observed the list at the relevant time (the matchmaker, the players, live observers); a late verifier who can no longer reconstruct the list state SHOULD treat the matchmaker’s pairing and the players’ founding of the session as evidence of satisfaction rather than presume non-conformance;
    • for rating: both players’ ratings come from the pinned rating source carried by the filter (the rating-authority pubkey, attestation kind, and declared pool scope — kind 3418 §Match-terms tags). Each player’s rating is the most recent conforming attestation by that authority for that player in the scope the filter declarespergame: attestations carrying the session’s game, regardless of variant tags; pervariant: attestations whose game and single variant tag match that player’s resolved variant in this Pairing — with created_at less than or equal to the Pairing’s canonical timing; the other player’s rating MUST be within max_delta of the filtering player’s. A player with no such attestation is unrated and the filter is NOT satisfied. Under a pervariant scope, a rating filter is satisfiable only when the Pairing resolves the same variant for both players (a multi-variant session has no shared (game, variant) pool to compare in); under a pergame scope, the single game pool is shared regardless of the resolved variants, and the filter binds across any variant combination. This leg is objective and retroactively verifiable from public data alone: rating attestations are regular, persistent events, and the scope is written in the filter.
  11. The Pairing’s canonical timing is less than or equal to the smaller of the two Open Challenges’ accept_until values. A Pairing whose canonical timing exceeds either deadline (or whose referenced Open Challenge has none) is non-conforming.
  12. The matchmaker pubkey is distinct from both player pubkeys.
  13. The content field is the empty string ("").
  14. Exactly one found_until tag is present, with a value strictly greater than created_at.
  15. No arbiter-marked p tag is present (the suite designates no arbiter).

A Pairing violating any constraint is non-conforming and MUST be ignored by all consumers (clients, rating verifiers). Non-conforming Pairings are not retracted from relays; they simply have no effect. This is the mechanism that bounds the matchmaker: it may choose the pair, the timing, the seats, and any value left free by both Open Challenges, but it cannot bind a player to a timestamper, opponent, variant, time control, rule system, or counterparty outside that player’s signed authorization.

Race resolution — greedy matching

A given Open Challenge participates in at most one canonical Pairing. Because a Pairing references two Open Challenges, canonicalization is a matching problem rather than a single-slot selection; it is resolved deterministically and anchored, like the rest of the suite, on canonical timing (Canonical Timing §Meta-resolution):

  1. Consider all conforming Pairings that have canonical timing. Order them by their canonical timing (ascending), with the smallest Pairing event id as tiebreaker.
  2. Process them in that order, maintaining a set of “consumed” Open Challenges (initially empty). A Pairing is canonical iff both of its referenced Open Challenges are still free; on acceptance, mark both as consumed. A Pairing referencing an already-consumed Open Challenge is non-canonical and is skipped.

This greedy procedure yields the same matching for every consumer from the public events alone. If an Open Challenge appears in two conforming Pairings, the earlier-attested Pairing wins it; the later Pairing is void, and the other player it named remains free to be matched by a subsequent Pairing (in practice, their client re-enters the pool). A Pairing with no canonical timing does not participate.

From Pairing to Game Session

The Pairing declares the session; it does not make it playable. Either player founds it by publishing a Game Session (kind 3422) whose founding-reference e tag points to the canonical Pairing (marker pairing), within the Pairing’s found_until window; should both publish, the canonical Game Session is decided by meta-resolution (kind 3422 §Idempotence and race resolution) — the two candidates being identical in content, since the Pairing leaves nothing open. The Game Session:

From the Game Session onward the lifecycle is identical to the directed path: Ply (kind 3423), Conclusion (kind 3425), and optional rating attestations (kinds 3426/3427) all apply unchanged. A matchmade session contributes to ratings exactly as a directed one does, subject to the pool scope in effect (a pervariant scope excludes multi-variant sessions from rating filters; a pergame scope rates them together).

Validation responsibility

Validation of a Pairing against the consent constraints is the responsibility of the client (each player’s client MUST validate before founding) and rating verifiers. A non-conforming Pairing does not declare a session; a client MUST NOT publish a Game Session for it.

Examples

The examples below use placeholder pubkeys, event ids, relay URLs, and timestamps for illustration.

Example 1 — Same-variant pairing

The matchmaker pairs Alice’s and Bob’s sanki ōgi 5+3 Open Challenges:

{
 "kind": 3419,
 "pubkey": "<matchmaker_pubkey>",
 "created_at": 1700000100,
 "tags": [
 ["e", "<alice_open_challenge_id>", "wss://relay.example.com", "open_challenge"],
 ["e", "<bob_open_challenge_id>", "wss://relay.example.com", "open_challenge"],
 ["p", "<alice_pubkey>", "wss://relay.example.com", "player"],
 ["p", "<bob_pubkey>", "wss://relay.example.com", "player"],
 ["timing_relay", "wss://relay.example.com"],
 ["game", "sanki"],
 ["rules", "3f6d1a0c9e4b2a7d5c8e1f0a9b3c7d2e4f6a8b0c1d3e5f7a9b2c4d6e8f0a1b3c", "https://blobs.example.com"],
 ["variant", "<alice_pubkey>", "ogi"],
 ["variant", "<bob_pubkey>", "ogi"],
 ["seat", "<bob_pubkey>", "first"],
 ["seat", "<alice_pubkey>", "second"],
 ["time_control", "300", "3"],
 ["found_until", "1700000220"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}

Alice or Bob then publishes the Game Session (kind 3422) referencing this Pairing (marker pairing), mirroring the drawn seats, before 1700000220.

Example 2 — Resolved multi-variant pairing

Bob’s Open Challenge fixed self = ogi and left opponent open; Dana’s fixed self = chess and left opponent open. The matchmaker resolves a multi-variant session (Bob ōgi vs Dana chess), valid because neither Open Challenge constrained the other’s variant:

{
 "kind": 3419,
 "pubkey": "<matchmaker_pubkey>",
 "created_at": 1700000100,
 "tags": [
 ["e", "<bob_open_challenge_id>", "wss://relay.example.com", "open_challenge"],
 ["e", "<dana_open_challenge_id>", "wss://relay.example.com", "open_challenge"],
 ["p", "<bob_pubkey>", "wss://relay.example.com", "player"],
 ["p", "<dana_pubkey>", "wss://relay.example.com", "player"],
 ["timing_relay", "wss://relay.example.com"],
 ["game", "sanki"],
 ["rules", "3f6d1a0c9e4b2a7d5c8e1f0a9b3c7d2e4f6a8b0c1d3e5f7a9b2c4d6e8f0a1b3c", "https://blobs.example.com"],
 ["variant", "<bob_pubkey>", "ogi"],
 ["variant", "<dana_pubkey>", "chess"],
 ["seat", "<dana_pubkey>", "first"],
 ["seat", "<bob_pubkey>", "second"],
 ["time_control", "300", "3"],
 ["found_until", "1700000220"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}

This is a multi-variant session; a rating filter declaring the pergame scope binds across it, while a pervariant declaration would have excluded it (constraint 9). Had Bob’s Open Challenge fixed opponent = ogi, this Pairing would be non-conforming (constraint 7) and ignored.

Client guidelines

Clients implementing kind: 3419 SHOULD:

  1. Subscribe to kind 3419 with a #p filter on the user’s pubkey to receive Pairings naming the user as player.
  2. Validate a received Pairing against all consent constraints (§Consent constraints), retrieving both referenced Open Challenges, before treating the user as matched.
  3. Apply the greedy matching rule (§Race resolution) when an Open Challenge is referenced by more than one Pairing; act only on the canonical Pairing.
  4. After the canonical Pairing, found the session: publish the Game Session (kind 3422) immediately — its content is fully determined — unless a canonical Game Session for the Pairing is already observed; then follow the Game Session and do not allow Ply submission until t₀ is established.

Matchmaker implementations SHOULD draw the seats with a fair source of randomness and SHOULD publish their found_until policy, so that players know how long a Pairing waits for a founder.

Security considerations

References