Skip to content

CORD-04: Roles

Granular, hierarchy-based roles with customisable permissions, validated by clients and built on dumb relays.

There are two kinds of permission, enforced two different ways:

Read access: who may see a Channel. Enforced by key possession (CORD-02/03): you either hold the key or you cannot decrypt it.

Write authority: who may manage the Community (grant Roles, ban, rename, edit Channels). Enforced by a member’s rank in an owner-rooted Roster, checked independently by every member.

Every authority action is an edition on the Control Plane (CORD-02), signed inside the encryption by the actor’s real npub. Publishing there takes the staff-held control_root (CORD-02 §2) — a spam gate, never a verdict. Authority is rejection, not prevention: any control_root holder can publish an action, everyone else drops the ones that don’t map to a qualifying rank.


The Control Plane is a set of per-entity editions, each a rumor (CORD-01) sealed by the actor’s real identity, carrying:

  • entity — which logical thing it edits (a Role, a Grant, a Channel’s metadata, the Banlist, the Community’s metadata), by its stable coordinate.
  • version — a per-entity counter, starting at 1, that only ever climbs.
  • prev — the hash of the edition it supersedes (absent on the first), forming a chain.
  • content — the entity’s new state.

On the wire, an edition is a kind 3308 rumor at the Control Plane’s address, its fields riding tags. The Control Plane uses a plaintext seal (kind 20014, CORD-02 §5): the wrap is encrypted like every stream, but the seal’s content is the rumor’s serialized JSON string rather than ciphertext (byte-verbatim, CORD-01), which is what lets a compaction re-wrap the signed rumor into a new epoch with its signature intact (CORD-06):

{
"kind": 1059, // gift wrap, at the Control Plane address
"pubkey": "<control_pk>", // control_root-derived signer (CORD-02 §5): only staff can mint this wrap
"content": nip44_encrypt(control_conv_key, { // wrap: encrypted under the community_root-derived read key (CORD-02 §5)
"kind": 20014, // PLAINTEXT seal, signed by the actor
"pubkey": "<actor's real npub>",
"content": json_stringify({ // the rumor's serialized JSON string, not a nip44_encrypt(...) ciphertext
"kind": 3308,
"pubkey": "<actor's real npub>",
"content": "<the entity's new state, the §2 structure as JSON>",
"tags": [
["vsk", "3"], // the entity type (the registry, CORD-02 Appendix B)
["eid", "<entity id, 32-byte hex>"], // the stable coordinate
["ev", "4"], // this edition's version
["ep", "<prev edition hash, hex>"], // the chain link, absent on the first edition
["vac", "<grant eid>", "<grant version>", "<grant edition hash>"]
],
"created_at": 1686840217
}),
"tags": [],
"created_at": 1686840217,
"sig": "<actor's real signature>" // authorship + authority actor, survives re-wrapping
}),
"tags": [ ["p", "<ephemeral pubkey>"] ], // ephemeral, like any stream event
"created_at": 1686840217,
"sig": "<control signer signature>"
}

The vac tag is the authority citation (§5): the exact Grant edition the actor claims their rank under, pinned by coordinate, version, and content hash. It is a sync floor, not the verdict: a verifier won’t act on the edition until it has synced at least that Grant, and then resolves the actor’s rank against its current refuse-downgrade roster. An actor whose cited Grant the verifier already holds superseded (a demotion) is dropped, never grandfathered by an old-but-once-valid citation. It’s absent when the owner acts.

Edition hash. An edition’s identity — what the next edition’s ep cites — is sha256 over a length-prefixed, domain-separated preimage, so two clients holding the same edition compute the same hash:

edition_hash = sha256(
len64(label) || label // label = utf8 "vector-community/v1/edition"; len64 = u64 big-endian
|| entity_id[32]
|| version_be[8]
|| (prev ? 0x01 || prev[32] : 0x00 || zero[32])
|| len64(content) || content ) // content = the rumor's content bytes, verbatim

Every field is fixed-width or length-prefixed, so distinct inputs can never collide, and content is hashed as the exact bytes on the wire, never re-serialized, so a compaction re-wrap preserves the hash.

Every entity’s coordinate is deterministic, so two clients always fold the same chains:

Entity eid
Community metadata the community_id itself
Role its role_id, random 32 bytes minted at creation
Channel metadata the channel_id
Grant derived from (community_id, member) (CORD-02, A.6)
Banlist derived from (community_id) (CORD-02, A.6)
Invite Registry derived from (community_id, creator) (CORD-05 §5)
Pin List derived from (community_id, channel_id) (§7; CORD-02, A.6)

The derived coordinates bind to the community_id and never to a key or an epoch, deliberately: they survive every Refounding, which is what lets a compaction re-wrap current heads verbatim (CORD-06), and a fresh joiner holding only the newest root derives the same coordinates.

Clients fold every edition they hold into current state, per entity, taking the highest version whose chain is intact, and refuse to downgrade (a lower version is ignored), so a relay replaying a stale Grant or a lifted Ban is rejected. Two members editing the same entity at the same version converge deterministically: authority first, then the lower rumor id, never the author-settable timestamp. Judging “by authority” is not circular even for conflicts about authority: the fold starts at the owner, whose rank comes from the community_id itself rather than any fold, and resolves outward, so by the time any edition is judged, its signer’s rank is already settled by the chain behind it. An edition whose signer isn’t authorized is dropped, and if two authorized editions still tie, the lowest id wins. Every client walks the same chain and lands on the same head.

Folding across a Refounding. A compaction (CORD-06) re-wraps each entity’s current head under the new epoch, so that head’s prev cites an edition that no longer exists there — the chain-intact rule would reject it. It doesn’t, because the rule is for steady state and a Refounding resets the floor: a fresh joiner, starting from nothing at the new epoch, accepts the highest authority-verified head as its baseline despite the dangling prev (the signature plus current-authority check is the whole test), while a tracking client that already held the prior chain treats an unresolvable prev as a gap and fails closed for that entity, suspending it and refetching. Honest history can’t be truncated on a synced client, and a joiner is never locked out.

The chain created by these actions is the audit log: every action names its actor by real signature, unforgeable and readable by any member.

Authority lives in two entity types, both keyless, each its own edition (§1):

// Role (vsk 1), coordinate = the role_id
{ "role_id": "<hex32>", "name": "<string, ≤ 64 bytes>", "position": <u32>,
"permissions": "<decimal string, §3>",
"scope": {"kind":"server"}, // or {"kind":"channel","channel_id":"<hex32>"}
"color": <u32> } // cosmetic badge tint, 0 = theme default
// Grant (vsk 3), coordinate = grant_locator(community_id, member) (CORD-02 A.6)
{ "member": "<hex32>", "role_ids": ["<hex32>", ], // empty role_ids = a revoke
"control_wrap": "<base64>" } // optional: the staff write key, granter↔member pairwise ciphertext (§3)

A Role is a named bundle of permissions at a position. It mints no key, so granting it hands a member rank, never a secret. A Grant maps a member’s npub to their Roles, honored only if its signer outranks every Role it hands out (§3), and may carry the staff write key beside them (control_wrap, §3) — delivery, never authority. A role name caps at 64 bytes, a member holds at most 64 Roles, and a Community carries at most 100 Roles (a client folds the 100 lowest role_ids and ignores the rest).

The Roster is owner-rooted: every Grant and Role is signed by an npub the Roster ranks strictly above it, and the chain terminates at the owner, who is proven by the community_id itself (CORD-02), occupies position 0, and is supreme and unremovable. A member folds this chain bottom-up; an entry that doesn’t trace to the owner is not authority, no matter how validly it’s signed.

A member’s effective permissions are the union of their Roles’ bits, one u64 field whose bit positions are frozen: a new permission claims the next free bit, a retired one is burned, never renumbered or reused:

bit permission
1<<0 MANAGE_ROLES
1<<1 MANAGE_CHANNELS
1<<2 MANAGE_METADATA
1<<3 KICK
1<<4 BAN
1<<5 MANAGE_MESSAGES
1<<6 CREATE_INVITE
1<<7 retired (was MANAGE_INVITES)
1<<8 VIEW_AUDIT_LOG
1<<9 MENTION_EVERYONE
1<<10, 1<<12 reserved (MANAGE_EMOJI, MANAGE_EVENTS)
1<<11 PIN_MESSAGES (§7)

There is no all-powerful bit: an “admin” holds the union of the management bits, so a Role granted everything today does not inherit a permission added tomorrow. permissions rides the wire as a decimal string, never a bare number — a JSON number is a 64-bit float in JavaScript and silently corrupts past 2^53. A reader accepts either form (a number from an older edition, a string henceforth) and always writes the string.

position orders authority, lower is higher: the owner is position 0 (never a Role), a roleless member is effectively last, and a member’s rank is the lowest position among their Roles. One hard rule binds every action: the actor must hold the required bit and strictly outrank its target — equal cannot act on equal (an admin cannot ban a peer admin) — and no edition may claim a position at or above its own signer, so nobody can promote themselves toward the top. That binds the owner too: no Role may ever claim position 0, or an owner could create a peer nobody outranks. Two Roles MAY share a position — they are peers, neither acts on the other — and a display list breaks the tie by the lower role_id so every client renders one order. Authoritative drag-reorder (a single ordered RoleOrder entity) is possible future work; position-on-role is the frozen baseline.

Staff. Six bits gate actions that land as Control editions — MANAGE_ROLES, MANAGE_CHANNELS, MANAGE_METADATA, BAN, CREATE_INVITE, and PIN_MESSAGES — and a member holding any of them, plus always the owner, is staff: the set that holds the control_root (CORD-02 §2), since their authorized actions are exactly the ones published to the Control Plane. (KICK writes to the Guestbook and MANAGE_MESSAGES to Chat planes; neither needs it.) This list is normative: a future CORD introducing a permission whose actions are Control editions MUST amend it explicitly, so no implementation has to judge membership of the set for itself.

A Grant that first makes its member staff obliges the granter — necessarily staff themselves, granting takes MANAGE_ROLES — to deliver the current control_root in the Grant itself: the edition’s content carries control_wrap (§2), the secret NIP-44-encrypted under the granter↔member pairwise conversation key, its plaintext fixed-width — epoch_be[8] ‖ control_root[32], 40 bytes — the rekey-blob discipline (CORD-06 §1), one ECDH either side can compute, so a NIP-46 bunker account opens it with a single nip44_decrypt. Promotion and delivery are one signed edition: nothing separate to send, race, or watch an inbox for — the key arrives on the plane the recipient already folds, opaque pairwise ciphertext to every other reader (the Pin List’s sealed form is precedent, §7).

The recipient adopts the secret only if it derives to exactly the control_pk they hold for the named epoch (CORD-02 §5); any mismatch is dropped, never adopted. The epoch rides inside the ciphertext because staleness is structural: compaction re-wraps a Grant head verbatim across Refoundings (CORD-06), so a folded head can carry a wrap minted for a prior epoch’s key — harmless, since the check fails closed and staff crossing a rotation receive the new secret in their 136-byte base blob regardless (CORD-06 §1). A staff-making edition MUST carry a wrap fresh for the current epoch, and any current staffer MAY re-issue a Grant edition with a fresh wrap to re-deliver (a lost key, a head superseded before its member fetched it). There is no revocation because none is needed: a demoted member’s editions are dropped by rank regardless, and the secret turns over at the next Refounding (CORD-06). A garbage wrap is attributable griefing, nothing worse: only the edition’s signer authors it, a Grant edit is already a ranked authority action, and the cost — the promotee cannot write until re-delivery — arrives with a signature on it.

The Banlist is the one anti-roster: a signed list of npubs, honored only if its signer holds BAN. Every honest client drops every event from a banned npub — message, reaction, edit, or authority action — so a banned member vanishes entirely. It silences instantly and for free; the cryptographic read-cut (a rekey, CORD-06) is the separate, heavier step that also revokes access.

On the wire it’s one Community-wide entity (vsk 4, coordinate banlist_locator(community_id), CORD-02 A.6), its content the whole list of banned npubs, replaced entire on every edit:

["<hex32>", "<hex32>", ] // lowercase-hex pubkeys

The list is unbounded by rule but not by physics: an edition must fit its NIP-44 envelope at every layer (CORD-02, Appendix B), a practical ceiling near 500 npubs. A community outgrowing that needs sharding, future work; until then a client refuses an edit that would not fit.

Because it’s a single replaced document, two admins banning different members at the same version collide: the fold keeps one edition and the other’s addition drops until re-applied. Clients close that three ways, all over this unchanged wire: bulk-ban (every target in one edition, then a single rekey for the batch, CORD-06), a local rate-limit on ban writes, and re-heal (after publishing, re-fold, and if your addition isn’t in the head, re-apply it atop the winner). Re-heal guarantees convergence to the union, so the list is never permanently short a ban, only briefly.

To honor an action, a reader:

  1. Verifies the seal, learning the actor’s real npub.
  2. Folds the Roster and resolves that npub’s effective permissions and position.
  3. Confirms the actor holds the action’s required bit and strictly outranks its target, traced to the owner.

The Control Plane’s write gate (CORD-02 §2) plays no part in this judgment: a wrap’s valid signature proves only that some control_root holder published it. Provenance is the seal’s, rank is the Roster’s — an edition is honored or dropped on its actor alone.

Because the Roster propagates eventually, an authority action cites the exact Grant it acts under, and a reader will not honor it until it has synced that Grant to at least the cited version (block-until-synced), then resolves the actor’s rank against its current roster, not the cited moment. A just-demoted member’s stale action is dropped the instant the verifier holds the superseding demotion; citing an old valid Grant grandfathers nothing. The pin is three-part and the hash is checked: a citation whose hash doesn’t match the edition the verifier holds at that version parks exactly like an unsynced one, so a forged or forked citation never resolves. Reads never block, only enforcement does: a member always paints the newest state it can fetch and converges as truth arrives. An action citing a version that never resolves stays parked, and it parks only its own author’s action, so an absurd citation griefs nobody but the actor; clients just bound their re-fetching.

What a user sees as “remove this member” is three mechanisms with three guarantees, composed deliberately, in order:

Role Removal (Control Plane): a Grant edition strips the target’s Roles, revoking their authority. They remain a member, reading and writing freely; they simply outrank nobody, and their pending vac citations die with the revoked Grant.

Cooperative Kick (Guestbook, CORD-02 §5): an authorized KICK directive marks the target as departed and asks their client to leave. Cooperative is the honest word: a compliant client tears the Community down locally, a defiant one still holds every key. A Kick alone never enforces anything — it’s the polite removal, and a kicked member may be re-invited or re-join.

Cryptographic Removal (CORD-06): a Ban entry (§4) plus its Refounding, or a Private-Channel rekey for a channel-scoped cut. The only removal that enforces: everything after the rotation is unreadable to the target, no cooperation required.

Composition is the client’s duty. A Kick is Role Removal then the directive — strip first, so the target’s rank is gone before the departure lands. A Ban is all three, in the order their guarantees arrive: the Banlist edition first (silencing is instant and free), the Grant strip alongside, the Refounding last (severance is heavy and asynchronous; it can propagate while the target is already silenced and rankless). Each layer validates independently by its own rule — a Kick from a non-KICK holder is dropped, a Ban from a non-BAN holder is dropped, a rotation from an unauthorized Rotator is dropped — so a partially propagated removal degrades to a weaker removal, never to a broken one.

A Pin lifts a message onto the Control Plane, where compaction (CORD-06 §3) re-wraps it across every rotation — so a pin reaches every member, with no history replay and no old keys: a public Channel’s pins unconditionally, a private Channel’s for whoever holds the named key epoch. Pinning is curation, gated by PIN_MESSAGES (§3) and authorized like any edition (§5, vac included).

One Pin List per Channel — vsk 11, coordinate pins_locator(community_id, channel_id) (CORD-02 A.6; derived from the community_id, so it binds to its Community by construction) — replaced entire per edit, chained and versioned like any entity (§1). Unpinning is the next edition without the entry; there is no deletion event. Replace-entire collisions resolve by §4’s re-heal: re-fold, and re-apply your change if it lost.

Replace-entire cuts the other way too, and sharply: a writer MUST NOT build an edition from a list it could not read. An empty view has two innocent causes that are indistinguishable from an empty list — the content is sealed under a key epoch this client never held, or this fold served no edition for the entity at all — and publishing from either silently drops every entry the writer cannot see, after which compaction prunes the ancestors that still carried them. A client MUST therefore withhold the write, not the render: show the pins it cannot read as unavailable, and refuse to pin into them.

The content is one of two self-describing forms:

// Public Channel: plaintext — the plane's wrap is the gate, and compaction
// re-wrapping it to each new root IS the re-encryption.
{ "entries": [ <entry>, ] }
// Private Channel: sealed under the Channel's group key at the named epoch
// (decimal-string u64), the self-ECDH conversation-key construction of CORD-01.
{ "epoch": "4", "sealed": nip44_encrypt(channel_conv_key, { "entries": [ ] }) }

A reader accepts either form regardless of its metadata fold; a writer MUST use the form matching the Channel’s folded type. On a private→public conversion the list MUST NOT be mechanically re-formed — CORD-03 §2 promises a post-switch member reads only the now-public history, and a re-form would republish private-era pins to everyone. Entries cross that boundary only by a curator’s deliberate edition.

After a private Channel rekey, entries sealed under the old key go dark for members who never held it. Any PIN_MESSAGES holder MAY republish the list under the current key, and a Rotator holding the bit SHOULD, as a rotation duty (CORD-06) — the proof bundles are static bytes; only the envelope changes. Where the Banlist drops a pinner’s editions (§4), the fold reverts to the newest non-banned edition, unpinning innocent authors’ messages as collateral; any authorized holder SHOULD republish the surviving list, the same re-heal.

A pin does not quote a message; it proves one:

{ "seal": { <the original kind-20013 seal event: fields carried exactly, its content string unaltered> },
"keys": "<76 bytes lowercase hex: chacha_key[32] || chacha_nonce[12] || hmac_key[32]>",
"wrap": "<hex event id>", // optional locator hint — see below
"edit": { "seal": { <the Edit rumor's seal> }, "keys": "<76 bytes>" } } // optional — see Edits

NIP-44 v2 derives per-message keys as hkdf-expand(conversation_key, nonce), one-way — so disclosing one message’s 76-byte expansion exposes that message and nothing else: not the conversation key, not the epoch, not the author’s other traffic. Any Channel member at that epoch can compute it; the pinner includes it.

A verifier, holding nothing but the pin and the list’s own coordinates:

  1. Requires seal.kind == 20013; verifies the seal’s signature. The seal’s pubkey is the proven author.
  2. Parses the seal’s content as a NIP-44 v2 payload; checks its MAC with hmac_key.
  3. Decrypts with chacha_key/chacha_nonce; unpads; parses the rumor.
  4. Requires all of: the rumor’s pubkey equals the seal’s (NIP-59’s impersonation check — renderers display rumor fields); the rumor’s kind is 9 or 1111; the rumor carries ["channel", <channel_id>] strict-equal to this list’s Channel, absence failing. This restates CORD-01’s Binding check for a path that decrypts no wrap — without it, a private Channel’s keyholder could pin its messages into a public list, disclosing them Community-wide with proof.
  5. Recomputes the rumor’s id from the decrypted bytes (NIP-01 serialization; an embedded id is never trusted). That id is the entry’s identity, for deduplication and deletion matching.

On any failure the entry is invalid and MUST be dropped; its edition folds normally (see Limits). A passing entry proves the author signed exactly this ciphertext and this ciphertext opens to exactly this rumor: author, words, Channel, and signed created_at, verifiable forever. A chosen-plaintext forgery must defeat both a ChaCha20 keystream preimage and a second HMAC-SHA256 key matching the signed MAC — each an exhaustive key search over a PRF — and random keys yield bytes that fail step 4. A verifier cannot, and need not, confirm the disclosed keys equal the honest expansion: the MAC and signature bind the chain regardless.

wrap is an unverifiable locator hint: a client MUST treat it as untrusted (detected by mismatch, never relied on). Jump-to-context serves only members able to derive that epoch’s plane address from the proven rumor’s own channel and epoch tags; a fresh joiner cannot, and hides the affordance.

Three properties to surface honestly. A message is pinnable only by someone who can read it, only while someone holds its epoch’s key — unopenable history cannot be pinned, correctly. A pin makes its message permanently attributable to every future member — the point, and why the gating bit is editorial power. And the binding stops at the Channel: a channel_id is client-minted, so a keyholding member of Channel Y who controls another Community can mint a same-id Channel there and replay Y’s pins, proof intact. Such an attacker could always leak content, though not previously with a universally verifiable artifact; the rumor deliberately never carries a community_id (CORD-02 §1), so this is a named residual of the tombstone-binding class (CORD-02 §9), not an oversight. Likewise the sealed form guards against non-members, never against its own curators: a keyholding PIN_MESSAGES holder publishing the public form is the same authorized disclosure, made Community-wide.

An Edit (kind 3302) is a Chat-plane rumor: a member holding the Channel’s keys folds it and reads the current text, so for them a pin needs nothing — it names a message, and the timeline is the authority on what that message now says. A client that can apply an Edit locally MUST do so, and the Control Plane stays untouched.

The problem is the reader a pin exists for. A member who joined past a rotation holds none of the Chat plane, so an Edit is unreachable to them: left alone they would read the original text forever, under a proof that presents it as current. So the correction rides the entry, optionally and best-effort:

  • A curator SHOULD republish the list with the Edit’s own proof bundle in edit — the Edit is itself a signed rumor sealed under a Channel key the curator holds, so it discloses exactly like the original, and a keyless reader verifies it with the same steps. This is automatic, not an errand: a client holding the bit and a newer Edit waits a short random interval, re-reads the head, and publishes only if the entry is still behind — the same suppression the deletion omission uses, so simultaneous curators collapse to one publisher. Deferring also coalesces: an author correcting a line three times costs ONE republish, because the push attaches whatever is newest when it fires rather than each revision as it lands.
  • A curator MUST NOT republish corrected text in any other form. An entry whose words are the curator’s rather than a proven author’s is exactly the attested-by-an-admin artifact this section exists to avoid.
  • A client holding an Edit newer than the entry’s proof MUST mark the pin as edited rather than render superseded words as current, whether or not it can republish. This costs no curator and bounds the damage of a pin nobody refreshes.

The edit bundle verifies by the same five steps, with one substitution and two additions. The substitution: an Edit is kind 3302, so step 4 requires that kind in place of 9/1111. Everything else in step 4 stands unchanged — the author equality, and above all the channel binding, so an Edit sealed in another Channel is refused exactly as a foreign message is, and the revision path opens no door the entry path closes. The additions: the Edit’s proven author MUST equal the original’s, since nobody else may revise another member’s words, and its e tag MUST name the original’s recomputed rumor id. Those are deliberately the fold’s own rules for Edits, so a keyless reader reaches the verdict a keyed reader reaches by folding.

Edits target the original rumor and never each other, so an entry carries at most ONE bundle — the newest provable Edit — and a later one REPLACES it rather than appending. A message edited fifty times costs an entry no more than one edited once. Two consequences follow and should be stated rather than assumed: a pin never tracks Edits automatically, so where nobody republishes, a keyless reader sees the message as it was proven at pin time; and because a republish replaces, a curator attaching an OLDER Edit silently reverts the entry, which a keyless reader cannot detect. A curator SHOULD attach the newest Edit it can prove, and a client SHOULD offer the refresh only while it holds something newer than the entry carries.

Two caps: the byte cap judged on the edition’s content field — the exact carried bytes, both forms, never a re-serialization — by every Control Plane reader, Channel member or not; the entry cap by whoever can open the form:

  • 25 entries, and
  • 32,768 bytes of content.

A violating edition is NOT refused from the fold: it folds, chains, and compacts like any other, and every reader MUST treat its content as an empty list. Refusing the edition would fork the version chain between implementations, and a sealed list’s caps hang on keys a refounder may not hold — a compaction could seat a head its own members refuse, ancestors pruned. Content-level refusal keeps every chain identical and costs the violator their whole list.

The byte cap is derived, not chosen, and these figures are measured against a reference implementation rather than estimated. 32,768 bytes of content reaches the wire at 55 KB after the wrap’s NIP-44 padding and base64 — inside the 65,535-byte NIP-44 plaintext cap (CORD-02 Appendix B) and common relay ceilings. It is the largest safe power of two: 40,960 bytes of content reaches 66 KB, past a 64 KB relay ceiling.

An entry has a floor near 1.1 KB whatever the message says — the seal’s id, pubkey and signature, its NIP-44 payload, and the 76-byte disclosure — so a typical ~135-character pin runs about 1.3 KB, and length barely moves it. The two caps therefore bind different forms, and a client should not promise otherwise: a public list of 25 typical pins measures ~32.0 KB, so the entry cap governs and a full 25 is genuinely reachable; a sealed list’s envelope inflates roughly 1.4× inside the same cap, so the byte cap governs at 15–18 typical pins. Because bytes rather than count are the real ceiling for private Channels, a client SHOULD surface remaining budget instead of a bare count. Pinned attachments are cheap — the entry carries the message’s pointer while the blob stays wherever blobs live — and a long announcement simply spends more of the shared budget.

Weight: only the head matters, so a client SHOULD bound its retained pin-edition backlog, SHOULD cache entry verification by entry identity (a re-folding client otherwise redoes every signature, MAC, and decryption per fold), and a compaction SHOULD omit the Pin List of a terminally deleted Channel. The aggregate is bounded and self-inflicted: pins add at most 32,768 bytes of head per Channel to the plane every member keeps and every compaction re-wraps — curation weight the curators choose.

Self-erasure outranks curation. A member’s deletion of their own message (kind 5) reaches a pin by identity — the delete’s e tag matches the entry’s recomputed rumor id (step 5) — and is honored only when the delete’s pubkey equals the entry’s proven author. A reader holding the delete MUST hide the entry immediately; every PIN_MESSAGES holder holding it MUST ensure the folded head no longer carries the entry — not that each publishes — and the obligation re-arms whenever a later fold seats a head that does (a stale writer’s same-version edition can win the tiebreak and re-seat it). Hiding is instant locally, so the edition is never urgent: the pinner SHOULD publish at once (their own pins are client-local knowledge; the wire carries no per-entry authorship, and compaction prunes the history that would), while every other witness SHOULD wait a short random interval, re-fold, and publish only if the head still carries the entry. One immediate edition in the common case, group coverage when the pinner is absent, demoted, or gone — and stragglers converge by the fold’s tiebreak, each omission carrying the same surviving list.

The omitting edition is also how deletion propagates: the kind-5 lives on the Chat plane at the original epoch, unreachable past a rotation, so a future member learns of it only through the edition. Until one lands, the deletion holds for tracking members alone; after Dissolution no edition can land (CORD-02 §9), so the §9 self-scrub carve-out reaches pins only as far as the last pre-dissolution edition. A client implementing any local or future moderation suppression SHOULD extend it to the pin entry.