Menu
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:

PositionContentRequired
0"p"Yes
1Pubkey (hex)Yes
2Relay URL hintNo
3Petname/aliasNo

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"
  ]
}