NIP-02

Contact Lists and Petnames

final social features

NIP-02 specifies kind 3 events for storing contact lists (following lists) with optional petnames (custom nicknames). This enables portable social graphs across clients.

Author
fiatjaf, arcbtc
Last Updated
15 January 2024
Official Spec
View on GitHub →

NIP-02: Contact Lists and Petnames

Status: Final Authors: fiatjaf, arcbtc Category: Social Features


Overview

NIP-02 defines how Nostr users create and manage their contact lists (also called “following lists”). This is the Nostr equivalent of following people on Twitter or following accounts on Mastodon.

Key Features:

  • Portable following lists - Your contacts sync across all clients
  • Petnames - Custom nicknames for people you follow
  • Relay recommendations - Suggest specific relays per contact
  • Decentralized social graph - No central authority controls who you can follow

Unlike traditional social media, your following list is yours. It’s a signed event you own, stored on relays you choose, and visible to any client you use.


Why Contact Lists Matter

1. Portable Social Graph

On traditional platforms:

  • Your following list is locked to that platform
  • Switching platforms means starting over
  • Platform bans mean losing your network

On Nostr with NIP-02:

  • Your following list follows you, not the app
  • Switch clients instantly without losing connections
  • No platform can revoke your social graph

2. Client Interoperability

Every Nostr client can read your contact list:

  • Follow someone in Damus (iOS)
  • See them in your feed on Primal (web)
  • Manage your list in Amethyst (Android)
  • All clients stay in sync automatically

3. Relay Hints

You can suggest specific relays for each contact:

  • Improve message delivery
  • Reduce relay load
  • Support your contacts’ preferred infrastructure

How It Works

Event Structure

Contact lists use kind 3 events with this structure:

{
  "id": "...",
  "pubkey": "your_public_key",
  "created_at": 1673347337,
  "kind": 3,
  "tags": [
    ["p", "pubkey1", "wss://relay.damus.io", "Alice"],
    ["p", "pubkey2", "wss://relay.nostr.band", "Bob"],
    ["p", "pubkey3", "", "Charlie"],
    ["p", "pubkey4"]
  ],
  "content": "",
  "sig": "..."
}

P Tag Format

Each p tag represents one followed user:

["p", <pubkey>, <relay-url>, <petname>]

Fields:

  • <pubkey> (required): The hex public key of the person you’re following
  • <relay-url> (optional): Suggested relay to find this person’s events
  • <petname> (optional): Your custom nickname for this person

Examples:

// Minimal - just the pubkey
["p", "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"]

// With relay hint
["p", "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", "wss://relay.damus.io"]

// With relay and petname
["p", "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", "wss://relay.damus.io", "Alice"]

// Just petname (no relay)
["p", "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", "", "Alice"]

Content Field

The content field is typically empty for kind 3 events, but some implementations use it for:

  • JSON metadata (non-standard)
  • Notes about your following list
  • Client-specific data

Best practice: Leave it empty to ensure maximum compatibility.


Creating Your Contact List

Example: JavaScript

import { getPublicKey, signEvent } from 'nostr-tools';

// Your contacts
const contacts = [
  {
    pubkey: "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
    relay: "wss://relay.damus.io",
    petname: "Alice"
  },
  {
    pubkey: "bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce",
    relay: "wss://relay.nostr.band",
    petname: "Bob"
  }
];

// Build the event
const event = {
  kind: 3,
  created_at: Math.floor(Date.now() / 1000),
  tags: contacts.map(c => ["p", c.pubkey, c.relay || "", c.petname || ""]),
  content: "",
  pubkey: getPublicKey(privateKey)
};

// Sign and publish
const signedEvent = signEvent(event, privateKey);
relay.publish(signedEvent);

Updating Your Contact List

The Replacement Pattern

Each new kind 3 event replaces the previous one. Relays and clients should always use your most recent contact list.

Workflow:

  1. Fetch your current list from relays
  2. Modify it (add/remove contacts, update petnames)
  3. Publish the updated list as a new kind 3 event
  4. Older list is superseded (but still stored on relays)

Example: Adding a Contact

// 1. Fetch current contact list
const currentList = await relay.get({
  kinds: [3],
  authors: [myPubkey],
  limit: 1
});

// 2. Add new contact
const updatedTags = [
  ...currentList.tags,
  ["p", newContactPubkey, "wss://relay.example.com", "NewFriend"]
];

// 3. Publish updated list
const updatedEvent = {
  kind: 3,
  created_at: Math.floor(Date.now() / 1000),
  tags: updatedTags,
  content: "",
  pubkey: myPubkey
};

const signedEvent = signEvent(updatedEvent, privateKey);
relay.publish(signedEvent);

Example: Removing a Contact

// Remove a specific pubkey
const updatedTags = currentList.tags.filter(
  tag => tag[0] === "p" && tag[1] !== pubkeyToRemove
);

// Publish updated list (same as adding)

Petnames Explained

Petnames are your personal nicknames for people you follow. They’re private to you (though publicly visible in your kind 3 event).

Use Cases

  1. Disambiguation: “Alice Bitcoin” vs “Alice Nostr” for two Alices
  2. Memorability: “That funny dev” instead of “npub1abc…”
  3. Organization: “Work - John” vs “Personal - John”
  4. Real names: “Mom” instead of their public profile name

Important Notes

  • Not global usernames: Petnames are just for you, not verified identities
  • Client-specific display: Some clients show petnames, others don’t
  • Public but personal: Anyone can see your petnames in your kind 3 event
  • Not tied to profiles: Independent of the person’s actual profile (kind 0)

Relay Hints

The optional relay URL suggests where to find a contact’s events.

Why Relay Hints?

  1. Performance: Clients can query specific relays instead of all relays
  2. Reliability: If you know Alice posts to wss://relay.alice.com, query there
  3. Bandwidth: Reduce load on general-purpose relays
  4. Support: Help your contacts by directing traffic to their preferred relays

How Clients Use Relay Hints

// Fetch posts from followed users
contacts.forEach(contact => {
  const relay = contact.relayHint || DEFAULT_RELAY;

  // Query this specific relay for this user's events
  relay.sub([{
    kinds: [1], // text notes
    authors: [contact.pubkey],
    since: lastFetchTime
  }]);
});

Best Practices

  • Don’t rely solely on hints: Relays may go offline
  • Use multiple sources: Query main relays + hint relays
  • Update periodically: Users may change preferred relays
  • Respect user relay choices: If they publish relay lists (NIP-65), use those

Multiple Contact Lists

You can only have one active contact list per pubkey (the most recent kind 3 event).

However:

  • Other NIPs extend contact management (e.g., NIP-51 for multiple lists)
  • You can publish new lists anytime to update your following
  • Historic lists remain on relays but are superseded

Privacy Considerations

Public Following List

⚠️ Your contact list is public. Anyone can see:

  • Who you follow
  • Your petnames for them
  • Relay hints you’ve set

This is necessary for:

  • Social graph portability
  • Client interoperability
  • Decentralized follow recommendations

Implications

  • No private follows: Everyone can see your full following list
  • Petnames are visible: Don’t use sensitive information
  • Social graph analysis: Researchers can analyze follow patterns

Alternative: NIP-51 defines private lists using encrypted kind 30000 events (not yet widely adopted).


Client Support

Full Support

  • Damus - Elegant contact list management
  • Primal - Fast sync across devices
  • Amethyst - Android-native list editing
  • Snort - Web-based contact management
  • Iris - Social graph visualization
  • Nostrudel - Power user list tools

Partial Support

  • Some clients may not display petnames
  • Some may ignore relay hints
  • Some may not sync in real-time

For client-specific features, see our Client Directory.


Advanced Topics

Following Lists as Social Signals

Your following list enables:

  1. Friend-of-friend discovery: “Alice follows Bob, maybe I should too”
  2. Web-of-trust: “I trust people followed by my contacts”
  3. Recommendations: Clients can suggest accounts based on overlap
  4. Community detection: Identify interest groups via follow patterns

Relay List Conflicts

If a user publishes their own relay list (NIP-65) and you have a relay hint for them:

Priority: User’s own relay list > Your relay hint

Always respect the user’s published preferences when available.

Large Contact Lists

With thousands of follows:

  • Event size increases (more p tags)
  • Syncing may be slower
  • Some relays may reject very large events
  • Consider using NIP-51 for categorized lists

Practical limit: Most clients handle 1,000-5,000 contacts comfortably.


Common Questions

What happens if I follow someone who deleted their profile?

The p tag remains in your list. The pubkey still exists; their profile (kind 0) is just missing. Clients typically show the pubkey or “Unknown user.”

Can I follow someone without them knowing?

Yes. Following is one-way and doesn’t notify the other person (though your kind 3 event is public, so they can check if they want).

How do I unfollow everyone?

Publish a kind 3 event with no p tags (or an empty tags array).

Can I have multiple following lists (like Twitter lists)?

Not with NIP-02. See NIP-51 for categorized, labeled contact lists (e.g., “Friends”, “News”, “Developers”).


  • NIP-01 - Basic protocol (event structure)
  • NIP-51 - Lists (multiple categorized contact lists)
  • NIP-65 - Relay list metadata (users publish their preferred relays)

For Developers

Implementation Checklist

Reading Contact Lists:

  • Fetch kind 3 events for a given pubkey
  • Parse p tags (pubkey, relay, petname)
  • Use most recent event (highest created_at)
  • Handle missing relay/petname fields gracefully

Writing Contact Lists:

  • Fetch user’s current list first
  • Modify tags array
  • Create new kind 3 event with updated tags
  • Sign and publish to relays
  • Confirm acceptance from relays

Edge Cases:

  • Empty contact lists (valid)
  • Duplicate pubkeys (client should deduplicate)
  • Malformed relay URLs (skip or validate)
  • Very large lists (>10,000 contacts)

Example Libraries

JavaScript:

import { SimplePool, getPublicKey } from 'nostr-tools';

const pool = new SimplePool();

// Fetch someone's contact list
const contacts = await pool.get(['wss://relay.damus.io'], {
  kinds: [3],
  authors: [pubkey],
  limit: 1
});

console.log(contacts.tags); // Array of p tags

Python:

from nostr.event import Event
from nostr.relay_manager import RelayManager

# Fetch contact list
relay_manager = RelayManager()
relay_manager.add_relay("wss://relay.damus.io")

filters = {
    "kinds": [3],
    "authors": [pubkey],
    "limit": 1
}

events = relay_manager.get_events(filters)
contacts = events[0].tags if events else []

Technical Specification

For the complete technical specification, see NIP-02 on GitHub.


Summary

NIP-02 enables portable social graphs on Nostr:

Kind 3 events store your following list ✅ P tags reference followed pubkeys ✅ Petnames let you nickname contacts ✅ Relay hints improve event fetching ✅ Decentralized - you own your social graph

Your contacts follow you across clients, not platforms. This is a fundamental feature that makes Nostr truly user-sovereign.


Next Steps:


Last updated: January 2024 Official specification: GitHub

Client Support

This NIP is supported by the following clients:

damus primal amethyst snort iris coracle nostrudel nos nostur plebstr current yakihonne
View all clients →

Related NIPs

NIP-01 NIP-51 NIP-65
← Browse All NIPs