| name | nostr-social-graph |
| description | Implement Nostr social-graph and list features when the task involves follows, relay lists, mute/bookmark lists, NIP-51 lists, or outbox-model relay discovery. |
Nostr Social Graph
Overview
Build and traverse Nostr social graphs: follow lists, relay discovery, the
outbox model, and NIP-51 lists. This skill handles the non-obvious parts —
correct tag structures for each list kind, the outbox model's read/write relay
routing, private list item encryption, and efficient graph traversal patterns.
When to Use
- The task involves Nostr follow graphs, relay lists, mute/bookmark sets, NIP-51 lists, or outbox-model relay discovery.
- The user is implementing list semantics, social traversal, or feed sourcing from relationship data.
- The problem is graph/list behavior across users and relays, not individual-note publishing.
- The request needs event structures or algorithms for follows, lists, or social discovery.
Do NOT use when:
- The task is building a single note, reply, or other standalone event.
- The work is relay protocol or generic client lifecycle logic.
- The problem is query-filter syntax rather than social-graph semantics.
Response format
Always structure the final response with these top-level sections, in this order:
- Summary — state the task, scope, and main conclusion in 1-3 sentences.
- Decision / Approach — state the key classification, assumptions, or chosen path.
- Artifacts — provide the primary deliverable(s) for this skill. Use clear subheadings for multiple files, commands, JSON payloads, queries, or documents.
- Validation — state checks performed, important risks, caveats, or unresolved questions.
- Next steps — list concrete follow-up actions, or write
None if nothing remains.
Rules:
- Do not omit a section; write
None when a section does not apply.
- If files are produced, list each file path under Artifacts before its contents.
- If commands, JSON, SQL, YAML, or code are produced, put each artifact in fenced code blocks with the correct language tag when possible.
- Keep section names exactly as written above so output stays predictable across skills.
Workflow
1. Identify the List Kind
Ask: "What social graph data is the developer working with?"
| Intent | Kind | Category | NIP |
|---|
| Follow/unfollow users | 3 | Replaceable | NIP-02 |
| Mute users, words, hashtags | 10000 | Replaceable | NIP-51 |
| Pin notes to profile | 10001 | Replaceable | NIP-51 |
| Advertise read/write relays | 10002 | Replaceable | NIP-65 |
| Bookmark events | 10003 | Replaceable | NIP-51 |
| Set DM relay preferences | 10050 | Replaceable | NIP-51 |
| Categorized follow groups | 30000 | Addressable | NIP-51 |
| User-defined relay groups | 30002 | Addressable | NIP-51 |
See references/list-kinds.md for all list kinds with
full tag structures.
2. Build the Event Structure
Follow List (Kind:3)
{
"kind": 3,
"tags": [
["p", "<pubkey-hex>", "<relay-url>", "<petname>"],
["p", "<pubkey-hex>", "<relay-url>"],
["p", "<pubkey-hex>"]
],
"content": ""
}
Rules:
- Replaceable: only the latest kind:3 per pubkey is kept
- Each
p tag = one followed pubkey
- Relay URL and petname are optional (can be empty string or omitted)
content is empty (historically held relay prefs, now use kind:10002)
- Append new follows to the end for chronological ordering
- Publish the complete list every time — new event replaces the old one
Relay List Metadata (Kind:10002)
{
"kind": 10002,
"tags": [
["r", "wss://relay.example.com"],
["r", "wss://write-only.example.com", "write"],
["r", "wss://read-only.example.com", "read"]
],
"content": ""
}
Rules:
- Replaceable: only the latest kind:10002 per pubkey is kept
r tags with relay URLs
- No marker = both read AND write
"read" marker = read only
"write" marker = write only
- Keep small: guide users to 2-4 relays per category
- Spread this event widely for discoverability
Mute List (Kind:10000)
{
"kind": 10000,
"tags": [
["p", "<pubkey-hex>"],
["t", "hashtag-to-mute"],
["word", "lowercase-word"],
["e", "<thread-event-id>"]
],
"content": "<encrypted-private-items-or-empty>"
}
Rules:
- Public items go in
tags
- Private items go in
content as encrypted JSON
- Supports
p (pubkeys), t (hashtags), word (strings), e (threads)
- Words should be lowercase for case-insensitive matching
3. Handle Private List Items (NIP-51 Encryption)
Any NIP-51 list can have private items. Public items live in tags, private
items are encrypted in content.
Encryption process:
const privateItems = [
["p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"],
["word", "nsfw"],
];
const encrypted = nip44.encrypt(
JSON.stringify(privateItems),
conversationKey(authorPrivkey, authorPubkey),
);
event.content = encrypted;
Decryption and backward compatibility:
function decryptPrivateItems(event, authorPrivkey) {
if (!event.content || event.content === "") return [];
if (event.content.includes("?iv=")) {
return JSON.parse(
nip04.decrypt(authorPrivkey, authorPubkey, event.content),
);
} else {
return JSON.parse(nip44.decrypt(
conversationKey(authorPrivkey, authorPubkey),
event.content,
));
}
}
4. Implement the Outbox Model
The outbox model is the correct way to route event fetching across relays. See
references/outbox-model.md for the full
explanation.
Core rules:
| Action | Which relays to use |
|---|
| Fetch events FROM a user | That user's WRITE relays |
| Fetch events ABOUT a user (mentions) | That user's READ relays |
| Publish your own event | Your WRITE relays |
| Notify tagged users | Each tagged user's READ relays |
| Spread your kind:10002 | As many relays as viable |
Implementation pattern:
async function buildFeedSubscriptions(myFollows: string[]) {
const relayLists = await fetchRelayLists(myFollows);
const relayToUsers = new Map<string, string[]>();
for (const [pubkey, relays] of relayLists) {
const writeRelays = getWriteRelays(relays);
for (const relay of writeRelays) {
if (!relayToUsers.has(relay)) relayToUsers.set(relay, []);
relayToUsers.get(relay)!.push(pubkey);
}
}
for (const [relay, users] of relayToUsers) {
subscribe(relay, { kinds: [1], authors: users });
}
}
function getWriteRelays(tags: string[][]): string[] {
return tags
.filter((t) => t[0] === "r" && (t.length === 2 || t[2] === "write"))
.map((t) => t[1]);
}
function getReadRelays(tags: string[][]): string[] {
return tags
.filter((t) => t[0] === "r" && (t.length === 2 || t[2] === "read"))
.map((t) => t[1]);
}
5. Traverse the Social Graph
Follows-of-follows (2-hop):
async function getFollowsOfFollows(pubkey: string) {
const myFollowList = await fetchKind3(pubkey);
const directFollows = myFollowList.tags
.filter((t) => t[0] === "p")
.map((t) => t[1]);
const secondHop = await Promise.all(
directFollows.map((pk) => fetchKind3(pk)),
);
const scores = new Map<string, number>();
for (const followList of secondHop) {
for (const tag of followList.tags.filter((t) => t[0] === "p")) {
const pk = tag[1];
if (pk !== pubkey && !directFollows.includes(pk)) {
scores.set(pk, (scores.get(pk) || 0) + 1);
}
}
}
return [...scores.entries()].sort((a, b) => b[1] - a[1]);
}
Web of Trust scoring:
function wotScore(
target: string,
myFollows: string[],
followLists: Map<string, string[]>,
): number {
let score = 0;
for (const follow of myFollows) {
const theirFollows = followLists.get(follow) || [];
if (theirFollows.includes(target)) score++;
}
return score;
}
6. Validate Before Publishing
Common Mistakes
| Mistake | Why It Breaks | Fix |
|---|
| Publishing only new follows instead of the full list | Kind:3 is replaceable — new event replaces old, so partial list = lost follows | Always publish the complete follow list |
| Using kind:3 content for relay preferences | Deprecated; clients ignore it | Use kind:10002 for relay list metadata |
Treating unmarked r tags as write-only | No marker means BOTH read and write | ["r", "wss://..."] = read + write |
| Fetching from a user's READ relays for their posts | READ relays are where they expect mentions, not where they publish | Use WRITE relays to fetch a user's own events |
| Broadcasting to all known relays | Wastes bandwidth, defeats the outbox model | Use targeted relay routing per the outbox model |
| Using NIP-04 for new private list items | NIP-04 is deprecated for this purpose | Use NIP-44 encryption for new events |
| Forgetting to spread kind:10002 widely | Others can't discover your relays | Publish kind:10002 to well-known indexer relays |
Uppercase words in mute list word tags | Matching should be case-insensitive | Always store words as lowercase |
| Missing relay URL normalization | Duplicate relay entries with different formatting | Normalize URLs (lowercase host, remove default port, trailing slash) |
Quick Reference
| Operation | Kind | Key Tags | Content |
|---|
| Follow user | 3 | ["p", "<hex>", "<relay>", "<petname>"] | "" |
| Set relays | 10002 | ["r", "<url>", "<read|write>"] | "" |
| Mute user/word | 10000 | ["p", "<hex>"], ["word", "<str>"] | encrypted private items |
| Pin note | 10001 | ["e", "<event-id>"] | "" |
| Bookmark | 10003 | ["e", "<id>"], ["a", "<coord>"] | encrypted private items |
| DM relays | 10050 | ["relay", "<url>"] | "" |
| Follow set | 30000 | ["d", "<id>"], ["p", "<hex>"] | encrypted private items |
| Relay set | 30002 | ["d", "<id>"], ["relay", "<url>"] | encrypted private items |
Key Principles
-
Replaceable means complete — Kind:3, 10000, 10001, 10002, 10003 are
replaceable events. Every publish must contain the FULL list, not just
changes. The new event completely replaces the old one.
-
Outbox model is not optional — Fetching events without consulting
kind:10002 relay lists leads to missed content and wasted connections. Always
resolve a user's write relays before fetching their events.
-
Private items use NIP-44 — New encrypted list content MUST use NIP-44.
When reading, detect NIP-04 legacy format by checking for ?iv= in the
ciphertext and handle both.
-
Relay lists should be small — Guide users to 2-4 relays per category
(read/write). Large relay lists defeat the purpose of targeted routing and
increase load on the network.
-
Graph traversal requires relay awareness — You can't just fetch kind:3
from one relay. Use the outbox model to find each user's kind:3 on their
write relays, then use those follow lists to discover the next hop.
Optimization Notes
- Preserve the user's requested output shape exactly and do not substitute generic advice for concrete artifacts.
- Include exact commands, code structures, protocol fields, tags, parameters, file paths, or deliverable sections when the task asks for them.
- Make safety gates explicit before irreversible, destructive, externally visible, or compliance-sensitive actions.
- For multi-step work, present steps in execution order and include validation or rollback checks where relevant.
- Avoid overfitting to a single eval example: express lessons as reusable rules, not as task-specific answers.