NIP-nip-02 Final
NIP-02: Contact List
Store and publish your following list on Nostr. Manage social connections and discover relay hints.
| Type | Nostr Implementation Possibility |
| Number | nip-02 |
| Status | Final |
| Original | https://github.com/nostr-protocol/nips/blob/master/02.md |
NIP-02: Contact List
Status: Final
NIP-02 defines how users publish their following list—the pubkeys they want to see content from. This is a replaceable event (kind 3), meaning only the latest version is valid.
Event Format
{
"kind": 3,
"pubkey": "your-pubkey",
"created_at": 1234567890,
"tags": [
["p", "pubkey-1", "wss://relay.example.com", "alice"],
["p", "pubkey-2", "wss://relay.example.com", "bob"],
["p", "pubkey-3", "", ""]
],
"content": "",
"sig": "..."
}
Tag Format
Each p tag has up to 4 elements:
| Position | Content | Required |
|---|---|---|
| 0 | "p" | Yes |
| 1 | Pubkey (hex) | Yes |
| 2 | Relay URL hint | No |
| 3 | Petname/alias | No |
Reading Contact Lists
Fetch Someone’s Following
async def get_contacts(pubkey: str, relays: list) -> list:
"""Get list of pubkeys someone follows."""
filter = {
"kinds": [3],
"authors": [pubkey],
"limit": 1
}
events = await query_events(relays, filter)
if not events:
return []
contacts = []
for tag in events[0]["tags"]:
if tag[0] == "p":
contacts.append({
"pubkey": tag[1],
"relay": tag[2] if len(tag) > 2 else None,
"petname": tag[3] if len(tag) > 3 else None
})
return contacts
const events = await pool.querySync(relays, {
kinds: [3],
authors: [pubkey],
limit: 1
});
const contacts = events[0]?.tags
.filter(t => t[0] === 'p')
.map(t => ({
pubkey: t[1],
relay: t[2],
petname: t[3]
}));
Publishing Contact List
Create/Update Contacts
def create_contact_list(contacts: list[dict], private_key: str) -> dict:
"""Create a contact list event."""
tags = []
for contact in contacts:
tag = ["p", contact["pubkey"]]
if contact.get("relay"):
tag.append(contact["relay"])
if contact.get("petname"):
tag.append(contact["petname"])
tags.append(tag)
event = create_event(
kind=3,
content="", # Usually empty
tags=tags,
private_key=private_key
)
return event
Add a Follow
async def follow(pubkey_to_follow: str, relay_hint: str = None):
"""Add someone to your contact list."""
# Get current contacts
current = await get_contacts(MY_PUBKEY, RELAYS)
# Check if already following
if any(c["pubkey"] == pubkey_to_follow for c in current):
return # Already following
# Add new contact
current.append({
"pubkey": pubkey_to_follow,
"relay": relay_hint,
"petname": None
})
# Publish updated list
event = create_contact_list(current, MY_PRIVATE_KEY)
await publish(event, RELAYS)
Remove a Follow
async def unfollow(pubkey_to_unfollow: str):
"""Remove someone from your contact list."""
current = await get_contacts(MY_PUBKEY, RELAYS)
# Filter out the unfollowed
updated = [c for c in current if c["pubkey"] != pubkey_to_unfollow]
if len(updated) == len(current):
return # Wasn't following
# Publish updated list
event = create_contact_list(updated, MY_PRIVATE_KEY)
await publish(event, RELAYS)
Building Feeds
Use contact lists to build personalized feeds:
async def get_feed(relays: list, limit: int = 50) -> list:
"""Get feed from people you follow."""
# Get your contacts
contacts = await get_contacts(MY_PUBKEY, relays)
following_pubkeys = [c["pubkey"] for c in contacts]
if not following_pubkeys:
return []
# Query their recent posts
filter = {
"kinds": [1],
"authors": following_pubkeys,
"limit": limit
}
events = await query_events(relays, filter)
return sorted(events, key=lambda e: e["created_at"], reverse=True)
Relay Discovery
Contact lists include relay hints for finding users:
async def get_relay_hints(pubkey: str) -> list:
"""Get relay hints for a pubkey from your contacts."""
contacts = await get_contacts(MY_PUBKEY, RELAYS)
for contact in contacts:
if contact["pubkey"] == pubkey and contact["relay"]:
return [contact["relay"]]
return []
Content Field
The content field is usually empty but can store relay preferences as JSON (deprecated in favor of NIP-65):
{
"content": "{\"wss://relay.example.com\":{\"read\":true,\"write\":true}}"
}
Note: Use NIP-65 (kind 10002) for relay lists instead.
Agent Considerations
Manage Following Programmatically
class ContactManager:
def __init__(self, private_key: str, relays: list):
self.private_key = private_key
self.pubkey = derive_pubkey(private_key)
self.relays = relays
self.contacts = []
async def load(self):
"""Load current contact list."""
self.contacts = await get_contacts(self.pubkey, self.relays)
async def save(self):
"""Publish current contact list."""
event = create_contact_list(self.contacts, self.private_key)
await publish(event, self.relays)
def is_following(self, pubkey: str) -> bool:
return any(c["pubkey"] == pubkey for c in self.contacts)
async def follow(self, pubkey: str, relay: str = None, name: str = None):
if not self.is_following(pubkey):
self.contacts.append({
"pubkey": pubkey,
"relay": relay,
"petname": name
})
await self.save()
async def unfollow(self, pubkey: str):
self.contacts = [c for c in self.contacts if c["pubkey"] != pubkey]
await self.save()
Rate Limiting
Avoid publishing too frequently:
# Bad: Updates on every follow
for user in users_to_follow:
await follow(user) # Publishes each time!
# Good: Batch updates
contacts = await get_contacts(MY_PUBKEY, RELAYS)
for user in users_to_follow:
contacts.append({"pubkey": user})
event = create_contact_list(contacts, MY_PRIVATE_KEY)
await publish(event, RELAYS) # Single publish
Machine-Readable Summary
{
"nip": 2,
"title": "Contact List",
"status": "final",
"defines": [
"following-list-format",
"contact-event-structure",
"relay-hints"
],
"event_kind": 3,
"replaceable": true,
"tag_format": ["p", "pubkey", "relay-hint", "petname"],
"related": [
"/learn/nostr/specs/nip-01",
"/learn/nostr/specs/nip-65",
"/learn/nostr/relays"
]
}