PR #2331: NIP-9B Verifiable Community Rules
Recreation note. This is a copy of pull request nostr-protocol/nips#2331, preserved as a custom NIP because my GitHub account (
mstrofnone) is currently suspended. The narrative below is the PR body; the spec text follows verbatim from the proposed9B.md.
PR body
NIP-9B: Verifiable Community Rules
Adds a new NIP defining kind:34551 — a machine-readable, signed rules document for communities. Clients fetch this event before drafting a post, locally validate the draft against allowed kinds, byte limits, per-author quotas, allow/deny patterns, and required tags, and reject the send before it hits the wire.
Complements rather than replaces existing moderation flows:
- NIP-72 communities define moderators and rely on post-hoc approval events (
kind:4550). The community definition (kind:34550) already carries a freeformrulestag intended for human readers; this NIP adds the machine-readable companion. - NIP-29 groups define roles and capabilities in opaque relay-side policy. This NIP exposes a portable subset of that policy to clients so they can pre-filter without round-tripping to the relay.
Why this NIP
The model is borrowed from a successful prior art: HelloZeroNet/ZeroNet's content.json user_contents mechanism, which since 2017 has shipped signed per-kind permission/quota rules to thousands of self-hosted sites. Full analysis of why this is a real gap in Nostr's moderation stack is in nips0.txt, summarised in the NIP body's "Background" section.
The rules object is declarative (what is allowed) and signed by the community owner, so any client or relay can replicate enforcement without trusting a single implementation. Untrusted relays still see only the events they receive; the win is that the publishing side knows up-front whether a draft will be rejected.
Scope discipline
- One new event kind:
34551(parameterised replaceable,dmirrors the community). - No changes to existing NIPs.
- Binding to NIP-72 communities via
atag, to NIP-29 groups viahtag — both optional, both already-existing tag conventions. - Numbering: slot
9Aoriginally chosen, renumbered to9Bafter hodlbod flagged that9Ais claimed by #2194. The two commits in this PR reflect that history; happy to squash if preferred.
Implementation evidence
Reference implementation:
- Client/composer side: vitorpamplona/amethyst#2839 (composer-side validation) + #2840 (feed-side filter) + #2798–2800 (NIP-9A wiring across composer/feed)
- Relay enforcement / loader: mstrofnone/strfry-namecoin-policy loader + parser + validator + handler test suites (
test/nip9a-*.test.js) - Demo community: rules JSON live at
nmcLightningService/NIP-9A-DEMO.md, published askind:34551to the standard 8-relay public set vianmcLightningService/publish-nip9a-rules.js.
Companion spec: see the partner PR adding a nip9b field to NIP-11 so relays can advertise the addressable rules document URI machine-readably.
Notes for review
- No changes to existing NIPs in this PR.
- The companion
nip9bNIP-11 pointer is a separate small PR deliberately, to keep this one tight. kind:34551is the next free slot in the replaceable range for moderation/policy artefacts; happy to renumber to whatever the maintainer prefers.
CC: none — opening cold so the broadest set of reviewers can chime in.
Proposed NIP text (9B.md)
NIP-9B
Verifiable Community Rules
draft optional
This NIP defines an addressable event that lets a community owner publish a machine-readable, signed rules document for a community. Clients can fetch this event before submitting a post and reject the draft locally if it would violate any rule, surfacing the violation to the user before send.
This complements rather than replaces existing moderation flows:
- [NIP-72](72.md) communities define moderators and rely on post-hoc approval
events (kind:4550). The community definition (kind:34550) already carries a freeform rules tag intended for human readers; this NIP adds the machine-readable companion.
- [NIP-29](29.md) groups define roles and capabilities in opaque relay-side
policy. This NIP exposes a portable subset of that policy to clients.
The rules object is declarative (what is allowed) and signed by the community owner, so any client or relay can replicate enforcement without trusting a single implementation.
Event definition
The rules document is a parameterised replaceable event of kind:34551, addressable by the same d tag as the community it governs.
{
"kind": 34551,
"pubkey": "<community-owner-pubkey>",
"tags": [
// Identity of this rules document. Mirrors the d-tag of the community.
["d", "<community-d-identifier>"],
// Bind to the community this rules document governs.
// For NIP-72: a 34550 address pointer.
["a", "34550:<community-author-pubkey>:<community-d-identifier>"],
// For NIP-29: alternative binding by host'group-id (see "Bindings" below).
// ["h", "<host>'<group-id>"],
// Allowed event kinds. May appear multiple times.
// Form: ["k", "<kind>", "<max-bytes?>", "<max-per-author-per-day?>"]
["k", "1111", "16384", "50"],
["k", "20", "262144", "10"],
["k", "30023", "262144", "5"],
// Per-pubkey policy overrides. May appear multiple times.
// Form: ["p", "<hex>", "<allow|deny>", "<role?>"]
["p", "<trusted-pubkey>", "allow", "contributor"],
["p", "<bad-actor-pubkey>", "deny"],
// Optional web-of-trust gate: posts only allowed if the author is reachable
// from <root-pubkey> through follow lists within <depth> hops.
["wot", "<root-pubkey>", "2"],
// Hard size cap independent of kind (defence in depth).
["max_event_size", "524288"],
// Anti-rollback ratchet: clients MUST refuse rules events whose
// `created_at` is less than this value, even if signed by the owner.
["min_rules_created_at", "1746604800"]
],
"content": ""
}Tag semantics:
- **
d** — REQUIRED. Identifier of this rules document. SHOULD match the
community's own d tag for clarity.
- **
aorh** — REQUIRED, exactly one. Binds this rules document to a
specific community. a for NIP-72 (34550:<pubkey>:<d>); h for NIP-29 (<host>'<group-id> per NIP-29).
- **
k** — REQUIRED, one or more. Each occurrence whitelists one event kind
for the community. Position 2 is the kind (decimal string). Position 3 is optional max-bytes (size of the JSON-encoded event); absent or empty means "no kind-specific limit". Position 4 is optional max-per-author-per-day quota; absent or empty means "no quota". Events whose kind has no k entry MUST be rejected.
- **
p** — OPTIONAL, zero or more. Position 2 is the pubkey hex. Position 3
is "allow" or "deny". Position 4 is an optional role label (free-form, not normative). deny overrides any allow and any other rule.
- **
wot** — OPTIONAL, zero or one. Position 2 is the root pubkey of the
trust graph. Position 3 is the maximum follow-graph depth (positive integer). The lookup mechanism is unspecified; clients MAY use NIP-02 follow lists, NIP-85 trusted assertions, or any locally available source. Multiple wot tags MUST be treated as an OR (any one passing is enough).
- **
max_event_size** — OPTIONAL, zero or one. Hard cap on the JSON-encoded
event byte size, applied in addition to any k-tag size limit.
- **
min_rules_created_at** — OPTIONAL, zero or one. A monotonic ratchet:
clients MUST ignore any 34551 event from this owner whose created_at is below this value, including older versions of the rules themselves found on stale relays.
Behaviour
Owner
- Only the community owner (the same pubkey that signs
kind:34550for
NIP-72, or the relay master key for NIP-29) MAY publish a 34551 event. Events signed by anyone else MUST be ignored by clients and SHOULD be rejected by relays that recognise this kind.
- Owners SHOULD increment
min_rules_created_atwhenever they intentionally
tighten rules, to defend against an attacker replaying an older laxer version.
Client (composer)
Before publishing any event into a community, clients SHOULD:
- Fetch the latest
34551event for the community. - Locally validate the draft event against the rules.
- If the draft would be rejected, surface the specific violation to the user
before sending (e.g. "this kind is not allowed in this community", "your post is over the 16 KB limit", "you have hit your 50 posts/day quota").
- Either block the send entirely or require explicit user confirmation,
per client policy.
A client MAY proceed with sending if no 34551 is found for the community. Absence of rules MUST NOT be treated as deny-by-default.
Client (reader)
Clients displaying a community feed MAY filter out events that violate the rules document, in addition to any NIP-72 approval-based filtering. This is useful for displaying NIP-72 communities where moderators have not yet issued 4550 approvals.
Relay
Relays MAY enforce the rules server-side by rejecting EVENT submissions that violate the latest 34551 for the community. This is OPTIONAL: the rules are signed and verifiable, so honest enforcement can occur at any layer. Relays SHOULD NOT silently strip non-conformant events.
Security considerations
Stolen owner key
A stolen owner key can publish maximally permissive rules. The min_rules_created_at ratchet limits the damage window only to the period between key compromise and detection; once detected, the legitimate owner SHOULD rotate the underlying community pubkey (NIP-72 community owners can do this by publishing a new 34550 from a new key — there is no recovery path for the original community identity).
Stale rules on dead relays
Replaceable events can linger on relays that have stopped receiving updates. The min_rules_created_at ratchet is the primary defence: once the owner ratchets it forward, clients are required to reject any older version. Clients SHOULD prefer rules from relays the community itself advertises (NIP-72 relay tag with requests or approvals markers).
Rule-driven content blocking
A malicious owner could weaponise per-pubkey deny to silence dissent within their community. This is a property of any owner-controlled forum and not specific to this NIP. NIP-72 already permits this by virtue of the moderator list. Clients SHOULD make the rules document visible to readers so members can see the deny-list and choose to leave.
WoT denial of service
The wot tag asks clients to traverse a follow graph at submission time. Clients SHOULD cap the cost of this traversal locally and SHOULD cache the result. Relays enforcing wot server-side MUST cap traversal cost or risk amplification attacks via crafted communities.
Mapping from ZeroNet permission_rules
This NIP is informed by ZeroNet's content.json user_contents feature. For implementers familiar with ZeroNet:
| ZeroNet | This NIP |
|---|---|
permission_rules.".*".max_size | max_event_size tag |
permission_rules.".*".files_allowed | k tag list |
permission_rules."bitid/.*@zeroid.bit".max_size | (deferred — see Open Issues) |
permissions."[email protected]": false | p tag with deny |
cert_signers | wot tag |
Open issues
- Per-author-pattern overrides. ZeroNet uses regex patterns over
bitid/<id>@<cert-issuer>. Nostr identities are flat pubkeys; the closest equivalent is a per-pubkey p tag, which does not scale to large allowlists. A future revision could add ["p_size", "<hex>", "<max-bytes>"] for per-author size overrides, but v1 keeps the schema small.
- Replaceable events with non-trivial diffs. When a user edits a
long-form post (kind:30023), the new version may pass rules while the old version did not (or vice-versa). v1 leaves this to client policy; future revisions could specify "rules apply at original publication time of the address".
- NIP-29 binding. The
htag form binds to a NIP-29 group, but the
signing pubkey for NIP-29 metadata is the relay master key, not a user. Reviewers please confirm whether 34551 from the relay master key for an h-bound community is the right ergonomics, or whether NIP-29 should use a different kind entirely.