Contact Lists and Petnames
NIP-02 specifies kind 3 events for storing contact lists (following lists) with optional petnames (custom nicknames). This enables portable social graphs across clients.
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:
- Fetch your current list from relays
- Modify it (add/remove contacts, update petnames)
- Publish the updated list as a new kind 3 event
- 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
- Disambiguation: “Alice Bitcoin” vs “Alice Nostr” for two Alices
- Memorability: “That funny dev” instead of “npub1abc…”
- Organization: “Work - John” vs “Personal - John”
- 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?
- Performance: Clients can query specific relays instead of all relays
- Reliability: If you know Alice posts to
wss://relay.alice.com, query there - Bandwidth: Reduce load on general-purpose relays
- 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:
- Friend-of-friend discovery: “Alice follows Bob, maybe I should too”
- Web-of-trust: “I trust people followed by my contacts”
- Recommendations: Clients can suggest accounts based on overlap
- 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”).
Related NIPs
- 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
ptags (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:
- Explore encrypted messaging in NIP-04
- Learn about identity verification in NIP-05
- Discover advanced lists in NIP-51
- Browse all NIPs in our reference
Last updated: January 2024 Official specification: GitHub
Client Support
This NIP is supported by the following clients: