- Sashité for Developers
- Nostr
- NIPs
- 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:
- Letting a user refuse all challenges (e.g., when offline, when not in the mood, when prioritizing other matters).
- Letting a user accept challenges only from a curated population — followers, players within a competitive rating range.
The Challenge Policy provides this granularity. The policy is:
- Declarative: published as a signed Nostr event.
- Per-game: a user can have different policies for different games (e.g., open to
chess, closed toxiangqi). Within a game, the policy is variant-agnostic — one mode governs the game’s entire variant space (see §Mode lookup §Variant independence). - Discoverable: senders can fetch the recipient’s policy and warn the user before sending.
- Updatable: addressable (parameterized replaceable); new events supersede prior ones for the same
(pubkey, game)pair.
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:
["d", "<game_id>"]— the policy applies to challenges in that game context.<game_id>MUST match the regular expression^[a-z][a-z0-9]{0,31}$(same constraint as thegametag in NIPs3420and3418).["d", "*"]— wildcard. The policy applies to challenges in any game for which no specific policy exists.
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:
- Find the most recent event (by NIP-01 replaceable semantics) with
d == "<G>". If found, that policy applies. - Otherwise, find the most recent event with
d == "*". If found, that policy applies. - 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:
- The policy answers who may engage me — a question about opponents. What is played is a question the challenge terms themselves negotiate: each player’s variant is carried, or deferred to the acceptance, by the Direct Challenge / Accepted Challenge pair (kinds
3420/3421). The two concerns are kept orthogonal. - Because both
varianttags of a Direct Challenge are OPTIONAL, any variant-keyed lookup would need auxiliary resolution rules for partially- and fully-deferred challenges — and those rules would have to be bypass-proof (an omitted variant must not evade a stricter per-variant policy). Keying on the game removes the problem at the root: whatever variant tags the challenge carries, the same single policy applies, and omission can bypass nothing. - A recipient wanting variant-level granularity (e.g., refusing one variant of a game while accepting another) relies on per-challenge inspection and decline-by-silence, exactly as for any other term the policy does not model (time control, seat assignment, designated arbiter).
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:
mode == "everyone"means “everyone NOT in my mute list”.mode == "following"means “people I follow AND NOT in my mute list”.mode == "rating"means “people meeting the rating criterion AND NOT in my mute list”.mode == "nobody"and a non-empty mute list produces the same effective set (empty).
Rating mode and external rating data
The rating mode requires that:
- The user has a publicly-known rating in the comparison pool (see below).
- The sender’s rating in the same pool is similarly known.
- The rating system is well-defined (algorithm, anchor point, update rules).
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:
- Under a
pergamescope, both players’ ratings inGare compared directly. The challenge’svarianttags are irrelevant, and the evaluation is total — consistent with the policy’s variant independence. - Under a
pervariantscope, the comparison variant is the recipient’s variant when the Direct Challenge fixes it, else the challenger’s variant when the Direct Challenge fixes it. If the challenge fixes neither variant, no comparison pool is determinable and the challenge SHOULD be treated as not meeting the policy (fail-closed, per the closing paragraph of this section). Senders’ clients SHOULD surface this: against apervariantrating source, a fully-open challenge cannot satisfy arating-mode policy.
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:
- Glicko-2 (the reference authority’s supporting document: Rating Specification — Glicko-2): a confidence-aware system with rating deviation and volatility, supporting per-session continuous-time RD growth. Verifiers MAY recompute derivationally from kind
3425Adjudications, OR consume signed snapshots published by a rating authority via kind3427— Glicko-2 Rating Attestation. - Elo (the reference authority’s supporting document: Rating Specification — Elo): a simpler scalar-only system with no confidence tracking and no time-based decay. Verifiers MAY recompute derivationally from kind
3425Adjudications, OR consume signed snapshots published by a rating authority via kind3426— Elo Rating Attestation.
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:
- The latest Attestation from a trusted rating authority for the relevant (player, pool) pair determines the player’s current rating. “Latest” is determined by the event’s own
created_at(largest value first), with the smallest event ID as tiebreaker, per the kind’s own §Client guidelines (the current-rating lookup across that authority’s attestations). This is distinct from the kind’s §Race resolution, which canonicalizes multiple attestations of a single duel by smallestcreated_at: the two rules operate at different scopes (latest-across-duels vs. duplicate-within-a-duel) and MUST NOT be conflated. Kinds3426and3427are generic, suite-independent NIPs and use self-declared timing; the attestation flow does not apply to them. - A player with no Attestation from any trusted rating authority in the relevant pool has no known rating from that source; the verifier MAY fall back to derivational computation (from kind
3425events) or treat the player as unrated. - Different trusted rating authority sets produce different ratings. The client’s policy evaluation uses the client’s own trusted set. The trusted rating authority set is conceptually independent of the trusted arbiter set used for derivational computation.
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:
- Exactly one
dtag, whose second element matches^([a-z][a-z0-9]{0,31}|\*)$. - Exactly one
modetag, whose second element is one of:everyone,following,rating,nobody. - The
max_deltatag is present exactly once ifmode == "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). - The
contentfield is the empty string ("").
All four constraints are checkable from the event alone.
Validation responsibility
Validation of a Challenge Policy is the responsibility of:
- The publishing user’s client (before signing).
- Any other client interpreting the policy (sender’s client before sending a challenge; recipient’s client before surfacing a received challenge).
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:
- Single-variant — both variants fixed to
ogi. - Multi-variant — challenger plays
chess, Carol’s variant fixed toogi. - Deferred — challenger plays
chess, Carol’s variant left open for her to choose at acceptance. - Fully open — neither variant fixed (the challenger delegated their own and left Carol’s open).
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:
- Allow users to view, create, update, and effectively remove their Challenge Policy events (via replacement).
- 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). - 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.
- Treat the implicit default
everyoneas the policy when no event is found. - Apply the latest event by
created_at(with smallest event ID as tiebreaker) per NIP-01 replaceable semantics.
Clients MAY:
- Cache fetched policies locally and refresh on a time-based schedule or on each interaction.
- Allow the user to override the policy filter on a per-challenge basis (e.g., view a filtered challenge anyway).
- Display the policy of other users in their profile views.
Security considerations
-
Advisory nature: The Challenge Policy is not enforced at the wire. A sender’s client can ignore the recipient’s policy and still publish a Direct Challenge. The recipient’s client will filter it. The sender wastes proof of work on a challenge the user will not see.
-
Replay and policy update timing: A policy update has effect from its
created_atforward. Direct Challenges signed before a policy update are not retroactively affected: a challenge that was acceptable when signed remains a valid event but may be filtered by the updated policy at display time. -
Rating mode and ambiguity: When the rating system is undefined or the sender’s rating cannot be retrieved, the policy SHOULD treat the challenge as not meeting the policy (i.e., filter it). Clients SHOULD provide UI affordances for the user to inspect the rating data used and override if desired.
-
Following mode and NIP-02 dependency: The
followingmode depends on the user’s NIP-02 contact list. A user with an empty or unpublished contact list underfollowingmode effectively becomesnobody. Clients SHOULD warn the user of this side effect when configuring the policy. -
Privacy of policy: Challenge Policy events are public. Anyone can see a user’s policy. Users who wish to obscure their policy can refrain from publishing one (yielding the implicit
everyonedefault) and rely on client-side filtering only. -
Idempotence: A given
(pubkey, kind, d)has exactly one canonical policy — the most recent one. Implementations MUST apply the latest event bycreated_at(with smallest event ID as tiebreaker, per NIP-01). -
Mute-list precedence: The NIP-51 mute list filter takes precedence over the Challenge Policy. A muted pubkey is filtered regardless of policy mode (§Mute-list interaction).
-
Content as enumerated empty: The
contentfield is normative — clients MUST validate it as the empty string. A non-empty content renders the event invalid as a Challenge Policy. Clients MUST reject such events and fall back to the next policy in the lookup order.
References
- NIP-01 — Basic protocol flow description
- NIP-02 — Follow List
- NIP-13 — Proof of Work
- NIP-51 — Lists
- Kind
3420— Direct Challenge - Kind
3418— Open Challenge (matchmaking pool entry whosefiltertag reuses this NIP’s mode vocabulary) - Kind
3419— Pairing (the matchmaker enforces both Open Challenges’ filters) - Kind
3425— Adjudication - Kind
3426— Elo Rating Attestation — generic signed-snapshot publication format for Elo ratings - Kind
3427— Glicko-2 Rating Attestation — generic signed-snapshot publication format for Glicko-2 ratings - Rating Specification — Glicko-2 — derivational procedure for Glicko-2 ratings
- Rating Specification — Elo — derivational procedure for Elo ratings (simpler alternative)
