Sashité for Developers
  1. Sashité for Developers
  2. Nostr
  3. NIPs
  4. Challenge Policy

NIP-XX

Challenge Policy

draft optional

This NIP defines a Nostr event kind by which a user declares their policy regarding incoming Direct Challenges (kind 3420) for a given game. The policy is advisory: it informs both the recipient’s client (which should filter incoming challenges accordingly) and the sender’s client (which should warn before sending a challenge that would be filtered).

Abstract

Defines kind: 30420, an addressable (parameterized replaceable) event by which a user declares, for a given game, the policy under which they will receive Direct Challenges. Four canonical modes are defined: everyone, following, rating, and nobody. A wildcard (*) provides a default policy applying to all games for which no specific policy exists. The policy is deliberately variant-agnostic: within a game, one mode governs the entire variant space (see §Mode lookup §Variant independence).

Scope: Direct Challenges versus matchmaking

This policy governs Direct Challenges (kind 3420) — the directed founding path, where a specific opponent is named. It does not govern the matchmaking path. In matchmaking, a player controls who they may be paired with through the optional filter tag carried in their own Open Challenge (kind 3418), which reuses this NIP’s mode vocabulary (everyone, following, and rating with max_delta and a pinned rating source) on a per-pool-entry basis; the matchmaker enforces it by pairing only counterparties that satisfy it (kind 3419 §Consent constraints). The nobody mode has no matchmaking analogue — a player who wants no pairings simply publishes no Open Challenge. The two mechanisms are deliberately separate: a standing, per-game policy for unsolicited directed challenges here, versus a per-entry eligibility filter chosen each time a player enters a pool there.

Motivation

The Direct Challenge (kind 3420) admits any pubkey as challenger by default. NIP-13 proof of work raises the cost of bulk spam, and NIP-51 mute lists let users block specific senders. Neither addresses two related needs:

The Challenge Policy provides this granularity. The policy is:

The policy is advisory, not enforced at the wire. A sender’s client can ignore the recipient’s policy, but the resulting challenge will be filtered by the recipient’s client. The sender wastes proof of work.

Specification

Event kind

kind: 30420

Challenge Policy events are addressable (parameterized replaceable) events per NIP-01: for a given (pubkey, kind, d-tag) triple, the latest signed event supersedes prior ones.

Tags

Tag Cardinality Value Description
d exactly one <game> or wildcard scope of this policy
mode exactly one canonical mode identifier policy mode
max_delta conditional positive integer required iff mode == "rating", forbidden otherwise

d tag

The d tag identifies the scope of the policy as a game:

The d tag MUST be present exactly once. Its second element MUST match ^([a-z][a-z0-9]{0,31}|\*)$.

The scope is deliberately the game, not the (game, variant) pair: within a game, one policy governs the entire variant space, and variant-scoped keys are NOT supported (see §Mode lookup §Variant independence). A user wishing to declare policies for multiple games publishes multiple Challenge Policy events, each with its own d tag.

mode tag

The mode tag identifies the policy. Four canonical modes are defined:

Mode Semantics
everyone Accept Direct Challenges from any pubkey (subject to mute-list filtering — see §Mute-list interaction).
following Accept Direct Challenges only from pubkeys the user follows (per the user’s NIP-02 contact list).
rating Accept Direct Challenges only from pubkeys whose rating is within max_delta of the user’s rating, in the comparison pool applicable to this game (see §Rating mode and external rating data). The rating system and its computation are out of scope for this NIP.
nobody Decline all Direct Challenges for this game.

The mode tag MUST be present exactly once, with its second element being one of the four canonical identifiers.

max_delta tag

When mode == "rating", the max_delta tag MUST be present. It specifies the maximum allowed absolute difference between the sender’s rating and the user’s rating (in the rating system and comparison pool applicable to the game — see §Rating mode and external rating data).

["max_delta", "<positive_integer>"]

The integer MUST match the regular expression ^[1-9][0-9]{0,3}$ and represent a value in the range 1 to 1000 inclusive — the same bound as the rating filter’s max_delta (kind 3418 §Match-terms tags); the two carry one shared vocabulary.

When mode != "rating", the max_delta tag MUST NOT be present.

Content

The content field MUST be the empty string ("").

A Challenge Policy carries no narrative payload: the semantics are fully expressed by the tags. Any non-empty content would either be ignored (creating a misleading impression of relevance) or introduce a side channel for unstructured data in an event meant to be purely declarative. Forcing the empty string removes this ambiguity.

Mode lookup

For a Direct Challenge addressed to the user, let G = the Direct Challenge’s game tag value (always present). Resolve the applicable policy:

  1. Find the most recent event (by NIP-01 replaceable semantics) with d == "<G>". If found, that policy applies.
  2. Otherwise, find the most recent event with d == "*". If found, that policy applies.
  3. Otherwise, the implicit default is everyone.

The lookup is keyed on the game alone: the challenge’s variant tags — both fixed, one fixed, or none, for either player (kind 3420 §Match-terms tags) — play no role in policy resolution. The lookup is therefore total: every conforming Direct Challenge resolves to exactly one policy. (The variant tags can still matter to the evaluation of a resolved rating-mode policy when the rating source pools per-(game, variant) — see §Rating mode and external rating data; that dependence is fail-closed, never a bypass.)

The sender’s client (kind 3420 §Client guidelines) MUST apply the same lookup when deciding whether its challenge is likely to be filtered, so that sender-side warnings and recipient-side filtering agree.

Variant independence

The policy is deliberately keyed on the game alone, not on (game, variant) pairs:

Mute-list interaction

The Challenge Policy is applied AFTER the user’s NIP-51 mute list filter. A pubkey listed in the user’s mute list is filtered regardless of the policy. Equivalently:

Rating mode and external rating data

The rating mode requires that:

The policy is per-game, but ratings live in pools, selected by a pool scope — the same two-value vocabulary as the rating filter of kind 3418 (§Match-terms tags): pergame (one pool per game, unifying its variants) or pervariant (one pool per (game, variant)). Here no filter carries the scope: the evaluation is recipient-side, so the recipient’s client applies the scope of the rating source it consumes (e.g., the reference deployment’s sanki authority computes pergame — one Elo rating across chess / ōgi / xiongqi). The comparison pool for a policy on game G is determined as follows:

This suite provides two candidate rating specifications, both of which derive ratings from Adjudication events (kind 3425) signed by arbiters in a verifier-chosen trusted set:

As a reference example, the reference deployment’s rating authority for sanki publishes Elo snapshots (kind 3426) and its client displays Elo, chosen for implementation robustness and cross-implementation reproducibility: the update is a closed-form formula with no iterative solver, so independent implementations agree exactly. Glicko-2 remains available as a confidence-aware alternative for verifiers who prefer it. The choice of rating system is verifier-side: the recipient’s client decides which specification applies when evaluating the rating mode of incoming challenges.

When a client uses signed rating attestations (kind 3426 or 3427) as the rating source:

Additional alternative rating systems (TrueSkill, etc.) MAY be defined in further separate specifications. Any deterministic rating mechanism computable from publicly-signed events is admissible for this NIP’s purposes; the recipient’s client choice of system determines the policy evaluation.

If the rating of either the sender or the user cannot be determined in the comparison pool (e.g., no determinable pool, insufficient adjudicated history, no signed Attestations from a trusted rating authority), the challenge SHOULD be treated as not meeting the policy (i.e., filtered).

Semantic constraints

A conforming Challenge Policy event MUST satisfy all of the following:

  1. Exactly one d tag, whose second element matches ^([a-z][a-z0-9]{0,31}|\*)$.
  2. Exactly one mode tag, whose second element is one of: everyone, following, rating, nobody.
  3. The max_delta tag is present exactly once if mode == "rating", and absent otherwise. When present, its second element matches ^[1-9][0-9]{0,3}$ and represents a value in the range 1 to 1000 inclusive (both conditions apply; the pattern alone admits values up to 9999).
  4. The content field is the empty string ("").

All four constraints are checkable from the event alone.

Validation responsibility

Validation of a Challenge Policy is the responsibility of:

A Challenge Policy that fails any constraint is invalid and MUST be ignored. Implementations SHOULD fall back to the next policy in the lookup order (§Mode lookup) — first the wildcard, then the implicit default everyone.

Replacement and removal

Per NIP-01 addressable-event semantics, a new event for the same (pubkey, kind, d) triple supersedes prior ones. To revert a game to the most permissive behavior, the user can publish a new policy with mode == "everyone" for that game (or for the wildcard).

This NIP does not define an explicit “deletion” mechanism; clients MAY rely on NIP-09 deletion events when supported, but support is not assumed.

Examples

The examples below use placeholder pubkeys and timestamps for illustration.

Example 1 — “Everyone” for standalone chess

Alice accepts challenges from anyone in the standalone chess game context:

{
 "kind": 30420,
 "pubkey": "<alice_pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["d", "chess"],
 ["mode", "everyone"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}

Example 2 — “Following” wildcard

Bob accepts challenges only from people he follows, with the same policy applying to every game:

{
 "kind": 30420,
 "pubkey": "<bob_pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["d", "*"],
 ["mode", "following"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}

Example 3 — “Rating” within 200 for Sanki

Carol accepts Sanki challenges — whatever the variants — only from players within 200 rating points of her:

{
 "kind": 30420,
 "pubkey": "<carol_pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["d", "sanki"],
 ["mode", "rating"],
 ["max_delta", "200"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}

Example 4 — “Nobody” for xiangqi

Dave declines all challenges in the standalone xiangqi game context:

{
 "kind": 30420,
 "pubkey": "<dave_pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["d", "xiangqi"],
 ["mode", "nobody"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}

Example 5 — Mixed: specific override of a wildcard

Eve has a permissive wildcard (everyone) but a strict rating policy for standalone chess (her competitive game). She publishes two events:

{
 "kind": 30420,
 "pubkey": "<eve_pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["d", "*"],
 ["mode", "everyone"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}
{
 "kind": 30420,
 "pubkey": "<eve_pubkey>",
 "created_at": 1700000001,
 "tags": [
 ["d", "chess"],
 ["mode", "rating"],
 ["max_delta", "150"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}

A challenge in game chess is filtered by the second policy. A challenge in game sanki falls back to the wildcard and is accepted.

Example 6 — Variant independence

Carol publishes a restrictive policy for sanki plus a permissive wildcard:

{
 "kind": 30420,
 "pubkey": "<carol_pubkey>",
 "created_at": 1700000000,
 "tags": [
 ["d", "sanki"],
 ["mode", "following"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}
{
 "kind": 30420,
 "pubkey": "<carol_pubkey>",
 "created_at": 1700000001,
 "tags": [
 ["d", "*"],
 ["mode", "everyone"]
 ],
 "content": "",
 "id": "...",
 "sig": "..."
}

Four incoming Direct Challenges in game="sanki", of every variant shape:

All four resolve identically: lookup d == "sanki"following. Each is surfaced only if Carol follows the sender. The variant tags never affect which policy applies — there is no per-variant key to bypass, and Carol retains full control of the variants she plays through her Accepted Challenge (kind 3421). A challenge in any other game falls back to the wildcard everyone.

Client guidelines

Clients implementing kind: 30420 SHOULD:

  1. Allow users to view, create, update, and effectively remove their Challenge Policy events (via replacement).
  2. Before sending a Direct Challenge (kind 3420), fetch the recipient’s policies for the proposed game and apply the lookup rules (§Mode lookup). If the policy is likely to filter the challenge, warn the user before publishing (which would waste proof of work).
  3. When receiving a Direct Challenge, apply the user’s policy filter in addition to the NIP-51 mute list filter (§Mute-list interaction). Challenges that do not satisfy the policy SHOULD be silently filtered out from display, unless the user explicitly opts in.
  4. Treat the implicit default everyone as the policy when no event is found.
  5. Apply the latest event by created_at (with smallest event ID as tiebreaker) per NIP-01 replaceable semantics.

Clients MAY:

Security considerations

References