Loading...
Loading...
Build and traverse Nostr social graphs including follow lists (kind:3, NIP-02), relay list metadata (kind:10002, NIP-65), the outbox model for relay-aware event fetching, mute lists (kind:10000), and NIP-51 lists with public and private encrypted items. Use when implementing follow/unfollow, building feeds from followed users, discovering user relays, implementing the outbox model, creating mute or bookmark lists, or working with any NIP-51 list kind.
npx skill4agent add accolver/skill-maker nostr-social-graph| 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 |
{
"kind": 3,
"tags": [
["p", "<pubkey-hex>", "<relay-url>", "<petname>"],
["p", "<pubkey-hex>", "<relay-url>"],
["p", "<pubkey-hex>"]
],
"content": ""
}pcontent{
"kind": 10002,
"tags": [
["r", "wss://relay.example.com"],
["r", "wss://write-only.example.com", "write"],
["r", "wss://read-only.example.com", "read"]
],
"content": ""
}r"read""write"{
"kind": 10000,
"tags": [
["p", "<pubkey-hex>"],
["t", "hashtag-to-mute"],
["word", "lowercase-word"],
["e", "<thread-event-id>"]
],
"content": "<encrypted-private-items-or-empty>"
}tagscontentptwordetagscontent// Private items use the same tag structure as public items
const privateItems = [
["p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"],
["word", "nsfw"],
];
// Encrypt with NIP-44 using the author's own key pair
// The shared key is computed from author's pubkey + privkey
const encrypted = nip44.encrypt(
JSON.stringify(privateItems),
conversationKey(authorPrivkey, authorPubkey),
);
event.content = encrypted;function decryptPrivateItems(event, authorPrivkey) {
if (!event.content || event.content === "") return [];
// Detect NIP-04 vs NIP-44 by checking for "iv" in ciphertext
if (event.content.includes("?iv=")) {
// Legacy NIP-04 format
return JSON.parse(
nip04.decrypt(authorPrivkey, authorPubkey, event.content),
);
} else {
// Current NIP-44 format
return JSON.parse(nip44.decrypt(
conversationKey(authorPrivkey, authorPubkey),
event.content,
));
}
}| 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 |
async function buildFeedSubscriptions(myFollows: string[]) {
// Step 1: Fetch kind:10002 for each followed user
const relayLists = await fetchRelayLists(myFollows);
// Step 2: Group follows by their WRITE relays
const relayToUsers = new Map<string, string[]>();
for (const [pubkey, relays] of relayLists) {
const writeRelays = getWriteRelays(relays); // "write" or no marker
for (const relay of writeRelays) {
if (!relayToUsers.has(relay)) relayToUsers.set(relay, []);
relayToUsers.get(relay)!.push(pubkey);
}
}
// Step 3: Subscribe to each relay for its relevant users
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]);
}async function getFollowsOfFollows(pubkey: string) {
// 1. Get direct follows
const myFollowList = await fetchKind3(pubkey);
const directFollows = myFollowList.tags
.filter((t) => t[0] === "p")
.map((t) => t[1]);
// 2. Fetch kind:3 for each direct follow
const secondHop = await Promise.all(
directFollows.map((pk) => fetchKind3(pk)),
);
// 3. Aggregate and rank by frequency
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]);
}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; // Higher = more trusted
}prwss://"read""write"wordcontentcreated_at| 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 | No marker means BOTH read and 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 | 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) |
| Operation | Kind | Key Tags | Content |
|---|---|---|---|
| Follow user | 3 | | |
| Set relays | 10002 | | |
| Mute user/word | 10000 | | encrypted private items |
| Pin note | 10001 | | |
| Bookmark | 10003 | | encrypted private items |
| DM relays | 10050 | | |
| Follow set | 30000 | | encrypted private items |
| Relay set | 30002 | | encrypted private items |
?iv=