User Lists Specification
Nostr User Lists Specification
Overview
This specification defines three event kinds that represent collections of users: the Follow List (kind 3), the Mute List (kind 10000), and the People List (kind 30000). All three share a common underlying model — a list of public keys with optional relay hints — but differ in their addressability, privacy model, and intended semantics.
| Kind | Name | Lists per Author | Private Entries |
|---|---|---|---|
| 3 | Follow List | 1 | No |
| 10000 | Mute List | 1 | Yes |
| 30000 | People List | Many | Yes |
1. Common Event Structure
1.1 User Entry Tag
In all three kinds, each listed user is represented as a p tag in the event’s tags array:
["p", "<64-char-hex-pubkey>", "<optional-relay-hint-url>"]
The first element is the literal string "p". The second element is the followed user’s public key as a 64-character lowercase hexadecimal string. The third element, if present, is a relay URL hint indicating where that user’s events may be found. Additional elements beyond the third are kind-specific and described in the relevant sections below.
Rule 1.1: Pubkey Format
A pubkey entry must be a 64-character lowercase hexadecimal string. Tags whose second element does not meet this requirement must be ignored.
Rule 1.2: Relay Hint
The relay hint is optional. Its absence is valid and must not be treated as an error. Relay hints are advisory — they assist in routing but are not authoritative.
1.2 Replaceable Event Semantics
Rule 1.3: Single Canonical Event
For kinds 3 and 10000, only one event per pubkey is canonical at any time. A newly received event for the same pubkey and kind replaces the previously held event if and only if it is newer.
For kind 30000, only one event per (pubkey, d-tag) pair is canonical at any time, applying the same replacement rule.
Rule 1.4: Replacement Condition
An incoming event replaces a stored event when:
incoming.created_at > stored.created_at
OR
incoming.created_at == stored.created_at AND incoming.id < stored.id
The second condition uses lexicographic ordering of the event ID hex strings. This tiebreak rule ensures deterministic behavior across implementations.
Rule 1.5: Older Events Discarded
A stored event must not be replaced by an incoming event with a strictly lower created_at, nor by one with the same created_at and a lexicographically greater event ID.
1.3 Mutation Model
Rule 1.6: Full Replacement on Mutation
Every mutation — adding an entry, removing an entry, or modifying list metadata — produces a new complete signed event containing the full desired state of the list. Partial updates are not defined. The published event is always self-contained.
Rule 1.7: Signed Event as Source of Truth
After a mutation is signed, local state must be derived from the signed event object itself. In-memory representations are derived from events, not maintained independently.
Rule 1.8: Idempotent Add
Adding a pubkey that is already present in the list must produce no change. An implementation may skip publishing a new event when the resulting state would be identical to the current state.
Rule 1.9: Idempotent Remove
Removing a pubkey that is not present in the list must produce no change. An implementation may skip publishing a new event when the resulting state would be identical to the current state.
2. Kind 3 — Follow List
2.1 Purpose
A kind 3 event encodes the set of users that the event’s author follows. There is exactly one follow list per author. Publishing a new kind 3 event replaces the previous one.
2.2 Event Structure
{
"kind": 3,
"pubkey": "<author-pubkey>",
"created_at": 1673347337,
"tags": [
["p", "<followed-pubkey-1>", "<relay-hint>"],
["p", "<followed-pubkey-2>"]
],
"content": "",
"id": "...",
"sig": "..."
}
Rule 2.1: Tag Array Carries the Follow List
The follow list is encoded exclusively in the event’s tags array as p tags. Each p tag represents one followed user.
Rule 2.2: Content Field
The content field has no defined role in follow list semantics. Its value must be preserved verbatim across mutations. An implementation must not clear or overwrite this field when performing a follow or unfollow operation.
Rule 2.3: Adding a Follow
To follow a user, append a new p tag for their pubkey to the existing tag array and publish a replacement event:
new_tags = existing_tags + [["p", pubkey]]
publish(kind=3, tags=new_tags, content=existing_content)
Rule 2.4: Removing a Follow
To unfollow a user, remove all p tags whose second element matches the target pubkey and publish a replacement event:
new_tags = existing_tags.filter(tag => tag[0] != "p" || tag[1] != pubkey)
publish(kind=3, tags=new_tags, content=existing_content)
Rule 2.5: No Private Entries
Kind 3 does not support private entries. All follows are public. The encrypted content mechanism defined in Section 3.3 does not apply to kind 3.
3. Kind 10000 — Mute List
3.1 Purpose
A kind 10000 event encodes the set of users that the event’s author wishes to mute. There is exactly one mute list per author. It supports both public entries, visible to anyone reading the event, and private entries, readable only by the list’s author.
3.2 Event Structure
{
"kind": 10000,
"pubkey": "<author-pubkey>",
"created_at": 1673347337,
"tags": [
["p", "<publicly-muted-pubkey>"]
],
"content": "<encrypted-private-tags>",
"id": "...",
"sig": "..."
}
Public muted pubkeys appear as p tags in the tags array. Private muted pubkeys are encoded inside the encrypted content field as described in Section 3.3.
3.3 Private Entry Encoding
Rule 3.1: Private Tag Serialization
Private entries are serialized as a JSON array of tag arrays, using the same tag format as public entries:
[
["p", "<muted-pubkey-1>"],
["p", "<muted-pubkey-2>"]
]
Rule 3.2: Self-Directed Encryption
The serialized private tag array is encrypted using the author’s own keypair, with the author as both encryptor and sole intended recipient. Only the holder of the author’s private key can decrypt the content.
plaintext = json_serialize(private_tags)
ciphertext = encrypt(plaintext, recipient_pubkey=author_pubkey, sender_keypair=author_keypair)
event.content = ciphertext
Rule 3.3: Decryption Requires Author’s Key
An implementation must not attempt to decrypt a mute list event unless it holds the private key corresponding to the event’s pubkey. If the key is unavailable, the private entries are inaccessible and must be treated as absent.
Rule 3.4: Private Tags Not Transmitted
Decrypted private tag content is ephemeral to the reading client. It is never re-published in plaintext.
3.4 Add and Remove Operations
Rule 3.5: Routing by Visibility
When adding an entry, the caller specifies whether the entry should be public or private. A public entry is appended to the tags array. A private entry is appended to the decrypted private tag array, which is then re-encrypted and stored in content.
// Adding publicly:
new_tags = existing_tags + [["p", pubkey]]
new_content = existing_content // unchanged
publish(kind=10000, tags=new_tags, content=new_content)
// Adding privately:
private_tags = decrypt(existing_content)
new_private_tags = private_tags + [["p", pubkey]]
new_content = encrypt(json_serialize(new_private_tags))
publish(kind=10000, tags=existing_tags, content=new_content)
Rule 3.6: Removal from Both Sides
When removing an entry, the target pubkey is removed from both the public tag array and the private tag array simultaneously, without requiring the caller to specify which side it was originally stored on:
new_tags = existing_tags.filter(tag => tag[0] != "p" || tag[1] != pubkey)
private_tags = decrypt(existing_content)
new_private_tags = private_tags.filter(tag => tag[0] != "p" || tag[1] != pubkey)
new_content = encrypt(json_serialize(new_private_tags))
publish(kind=10000, tags=new_tags, content=new_content)
Rule 3.7: Re-encryption Only When Changed
When the private tag array is not modified by an operation (e.g., a public-only removal where the target was not in the private array), the existing ciphertext must be carried forward unchanged. Re-encryption must not be performed unnecessarily.
4. Kind 30000 — People List
4.1 Purpose
A kind 30000 event encodes a named, curated list of users. Unlike kinds 3 and 10000, a single author may have many people lists simultaneously, each identified by a unique d tag. People lists support the same public and private entry model as the mute list.
4.2 Event Structure
{
"kind": 30000,
"pubkey": "<author-pubkey>",
"created_at": 1673347337,
"tags": [
["d", "<unique-list-identifier>"],
["name", "<human-readable-list-name>"],
["p", "<member-pubkey-1>", "<relay-hint>"],
["p", "<member-pubkey-2>"]
],
"content": "<encrypted-private-tags>",
"id": "...",
"sig": "..."
}
Rule 4.1: d Tag Required
Every kind 30000 event must contain exactly one d tag. Its value is the unique identifier for this list among all of the author’s kind 30000 events. An event without a d tag is invalid and must be rejected.
Rule 4.2: Unique d Tag per Author
No two kind 30000 events from the same author may share the same d tag value. An incoming event with a (pubkey, d-tag) pair matching a stored event is treated as a replacement, subject to the rules in Section 1.2.
Rule 4.3: List Name
A kind 30000 event should carry a human-readable name identifying the list, expressed as a tag whose first element is "name". This is distinct from the d tag, which is an opaque identifier. The name is suitable for display in user interfaces.
Rule 4.4: Private Entries
Private entries in kind 30000 follow the same encoding, encryption, add, and remove rules defined in Section 3.3 and Section 3.4 for kind 10000. The content field carries the encrypted private tag array; the tags array carries public member entries.
Rule 4.5: Creating a New List
When creating a new list, a unique d tag value must be generated that does not coincide with any existing kind 30000 d tag for the same author. The initial member set may be empty.
5. Optional Features
The following features are implemented by some clients and relays but are not required for basic protocol operation.
5.1 Entry Format Extensions
Optional Rule 5.1.1: Petname (Kind 3)
A kind 3 p tag may carry an optional fourth element as a locally meaningful human-readable label for the followed user:
["p", "<pubkey>", "<relay-hint>", "<petname>"]
The petname has no defined meaning to other users or relays. It is a local annotation for the list’s author. Implementations that do not support petnames must ignore the fourth element.
Optional Rule 5.1.2: Non-Pubkey Entries in Mute Lists (Kind 10000)
A mute list may contain entries other than pubkeys. Each additional entry type uses a distinct tag name:
| Tag name | Value | Meaning |
|---|---|---|
e |
64-char hex event ID | A muted thread, identified by its root event |
t |
string | A muted topic or hashtag |
word |
string | A muted keyword matched against event content |
All such entries follow the same public/private routing rules as p tags. An implementation that does not support a given entry type must ignore tags of that type without error.
Optional Rule 5.1.3: Additional List Metadata (Kind 30000)
A kind 30000 event may carry additional descriptive tags beyond name, such as a description or an image URL. These are supplemental and have no effect on list membership semantics.
Optional Rule 5.1.4: Human-Readable Event Description
Any list event may carry a tag providing a plain-text description of its purpose, for display in clients that do not natively understand the event kind. This tag does not affect list semantics.
5.2 Addressability and Identity
Optional Rule 5.2.1: d Tag Generation Strategy (Kind 30000)
The d tag value for a new kind 30000 event may be generated as a random opaque string (such as a UUID) or derived deterministically from the list’s human-readable name, provided uniqueness per author is maintained.
Optional Rule 5.2.2: Canonical Mute d Tag (Kind 30000)
Implementations may designate a specific d tag value as the conventional identifier for an author’s canonical mute or block list within kind 30000. A kind 30000 event carrying this designated d tag value is treated as semantically equivalent to a mute list, distinct from general-purpose named lists.
Optional Rule 5.2.3: Relay Preferences in Content (Kind 3)
The content field of a kind 3 event may carry a JSON object mapping relay URLs to read/write preference flags:
{
"wss://relay.example.com": { "read": true, "write": true },
"wss://other.example.com": { "read": true, "write": false }
}
This is a secondary use of the kind 3 event independent of the follow list in the tags. Implementations that do not interpret relay preferences must preserve the existing content value unchanged across mutations.
5.3 Encryption
Optional Rule 5.3.1: Encryption Scheme Negotiation
The specification does not mandate a specific encryption algorithm for private tag content. Multiple schemes may be supported, with the active scheme identifiable from the ciphertext format. Writer and reader must agree on a scheme for interoperability.
Optional Rule 5.3.2: Atomic Visibility Switch
An implementation may support an operation that moves a pubkey entry from private content to public tags, or from public tags to private content, within a single published event. This prevents any intermediate state in which the entry is simultaneously absent from or present in both sides:
// Moving from private to public:
private_tags = decrypt(existing_content)
new_private_tags = private_tags.filter(tag => tag[0] != "p" || tag[1] != pubkey)
new_tags = existing_tags + [["p", pubkey]]
new_content = encrypt(json_serialize(new_private_tags))
publish(kind=10000, tags=new_tags, content=new_content)
Optional Rule 5.3.3: Timestamp Collision Avoidance
Because two events with identical created_at values are resolved by event ID rather than by recency, an implementation may delay publishing a mutation when the current wall-clock second equals the created_at of the event being replaced. This ensures the replacement event has a strictly higher timestamp and is unambiguously newer.
5.4 Mutation Behavior
Optional Rule 5.4.1: Network Prefetch Before Mutation
Before constructing a replacement event, an implementation may fetch the current version of the list event from the network to incorporate any changes published from other sessions since the local copy was last synchronized. This reduces the risk of overwriting concurrent modifications.
Optional Rule 5.4.2: Guard Against First-Event Overwrite
An implementation may treat the case of publishing a new list event when no prior event exists differently from a normal mutation. For example, an implementation may first attempt a bounded network query to confirm no event exists before constructing an initial event, to avoid overwriting a remote event not yet received locally.
Optional Rule 5.4.3: Lazy vs. Strict Creation
An implementation may require a list event to already exist before any mutation can proceed, returning an error if it does not. Alternatively, an implementation may silently create a new list event on the first mutation if none exists locally.
Optional Rule 5.4.4: Deduplication on Add
Before appending a new p tag, an implementation may remove any existing p tag for the same pubkey from the public tag array, ensuring at most one public entry per pubkey:
new_tags = existing_tags.filter(tag => !(tag[0] == "p" && tag[1] == pubkey))
+ [["p", pubkey, optional_relay_hint]]
Optional Rule 5.4.5: Batch Mutation
An implementation may add or remove multiple entries in a single published event rather than publishing one event per change. The resulting event reflects all changes atomically.
Optional Rule 5.4.6: List Deletion (Kind 30000)
An implementation may publish a dedicated deletion event referencing a kind 30000 event by its (kind, pubkey, d-tag) coordinate when a named list is intentionally removed. This signals to relays and other clients that the list is gone, rather than merely absent.
5.5 Pubkey Validation
Optional Rule 5.5.1: Cryptographic Pubkey Verification
Beyond confirming that a tag value is a 64-character hex string, an implementation may attempt to decode the value as a point on the relevant elliptic curve and reject entries that do not correspond to a valid public key. Implementations that skip this check must accept any well-formed 64-character hex string.
5.6 Cross-List and Network Behavior
Optional Rule 5.6.1: Event Deduplication Across Relays
When the same list event is received from multiple relays, an implementation may deduplicate by event ID so that a given event is processed exactly once regardless of how many relays delivered it.
Optional Rule 5.6.2: Relay-Side Enforcement from Mute Lists
A relay may fetch the mute lists of one or more designated author pubkeys and use the public p tag entries from those events as an access-control deny list, rejecting write attempts from any pubkey present in the list.
Private encrypted entries in mute list events are not accessible to the relay and must not be applied.
When kind 30000 events are used for this purpose, only events whose d tag matches the designated canonical mute identifier (Optional Rule 5.2.2) are eligible. All other kind 30000 events must be ignored for enforcement purposes.
6. Web of Trust (Peripheral Concept)
The follow list and mute list together serve as the primary inputs to web-of-trust (WoT) scoring. WoT is an application-layer computation built on top of the list primitives defined in this specification. It is not required for list storage or mutation, but is documented here because the list kinds are its direct data source.
Rule 6.1: Positive Signal from Follow Lists
Each pubkey present in a user’s kind 3 follow list contributes a positive weight toward that pubkey’s WoT score as seen from the user. A common approach increments the score for a candidate pubkey once for each member of the user’s follow set who also follows that candidate:
score[candidate] += 1 for each follow in user.follows
where candidate in follow.follows
Rule 6.2: Negative Signal from Mute Lists
Each pubkey present in a user’s mute list (kind 10000, or a kind 30000 event with the canonical mute d tag) contributes a negative weight toward that pubkey’s WoT score:
score[candidate] -= 1 for each follow in user.follows
where candidate in follow.mutes
Rule 6.3: Hop Depth
WoT computations operate at a defined number of social hops from the origin user. At depth one, only the user’s direct follows are considered. At depth two, the follows-of-follows are included. Implementations operating at depth two commonly require a minimum number of shared connections before a two-hop candidate is admitted, to limit the influence of low-quality data at the graph’s edges.
Rule 6.4: Score Application
WoT scores may be used to rank or filter content in client feeds, to suppress notifications from low-trust accounts, or to grant or deny write access on a relay. The mapping from raw score to an access or display decision is implementation-defined.
Rule 6.5: Private Mutes in WoT
Private mute entries are readable only by the list’s author. An implementation computing WoT from the current user’s own lists may include private mute entries as negative signals. When computing WoT from a third party’s published lists, only public mute entries are available; private entries must be treated as absent.
Looking for comments…
Searching Nostr relays. This may take a moment the first time this article is opened.
Looking for comments…
Searching Nostr relays. This may take a moment the first time this article is opened.