NIP-01: Basic Protocol
The foundational Nostr protocol specification. Events, relays, subscriptions, and the core data model.
| Type | Nostr Implementation Possibility |
| Number | nip-01 |
| Status | Final |
| Original | https://github.com/nostr-protocol/nips/blob/master/01.md |
NIP-01: Basic Protocol
Status: Final Required: Yes — This is the foundation of Nostr
NIP-01 defines the core protocol: events, relays, subscriptions, and message types. Every Nostr implementation must support NIP-01.
Overview
Nostr is built on three concepts:
- Events: Signed JSON objects containing content
- Relays: Servers that store and forward events
- Clients: Software that creates and reads events
Client ←──WebSocket──→ Relay ←──WebSocket──→ Client
(wss://) (wss://)
Event Structure
Every event has exactly these fields:
{
"id": "32-byte lowercase hex SHA256 of serialized event",
"pubkey": "32-byte lowercase hex public key",
"created_at": 1234567890,
"kind": 1,
"tags": [
["e", "event-id", "relay-url"],
["p", "pubkey", "relay-url"]
],
"content": "Hello, Nostr!",
"sig": "64-byte lowercase hex Schnorr signature"
}
Field Definitions
| Field | Type | Description |
|---|---|---|
id | string | SHA256 of serialized event (hex, 64 chars) |
pubkey | string | Author’s public key (hex, 64 chars) |
created_at | integer | Unix timestamp in seconds |
kind | integer | Event type (0-65535) |
tags | array | Array of tag arrays |
content | string | Event payload (format depends on kind) |
sig | string | Schnorr signature (hex, 128 chars) |
Event ID Calculation
The id is computed by serializing the event and hashing:
import json
import hashlib
def compute_event_id(event):
# Serialize in specific format
serialized = json.dumps([
0, # Reserved for future use
event["pubkey"],
event["created_at"],
event["kind"],
event["tags"],
event["content"]
], separators=(',', ':'), ensure_ascii=False)
# SHA256 hash
return hashlib.sha256(serialized.encode('utf-8')).hexdigest()
import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex } from '@noble/hashes/utils';
function computeEventId(event) {
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
return bytesToHex(sha256(new TextEncoder().encode(serialized)));
}
Signature
The sig is a BIP-340 Schnorr signature over the event id:
from secp256k1 import PrivateKey
def sign_event(event, private_key_hex):
private_key = PrivateKey(bytes.fromhex(private_key_hex))
# Sign the event ID
event_id_bytes = bytes.fromhex(event["id"])
signature = private_key.schnorr_sign(event_id_bytes, None)
event["sig"] = signature.hex()
return event
Tags
Tags are arrays where the first element is the tag type:
["e", "referenced-event-id", "relay-url", "marker"]
["p", "referenced-pubkey", "relay-url", "petname"]
["a", "kind:pubkey:d-tag", "relay-url"]
["t", "hashtag"]
["d", "identifier"]
Standard Tags
| Tag | Purpose | Example |
|---|---|---|
e | Reference event | ["e", "id", "wss://relay", "reply"] |
p | Reference pubkey | ["p", "pubkey", "wss://relay"] |
a | Reference replaceable | ["a", "30023:pubkey:slug"] |
t | Hashtag | ["t", "bitcoin"] |
d | Identifier for replaceable | ["d", "my-article"] |
Event Kinds
Kinds determine how events are interpreted:
Regular Events
Stored until deleted or purged.
| Kind | Description |
|---|---|
| 1 | Short text note |
| 7 | Reaction |
| 1984 | Report |
Replaceable Events
Only latest per pubkey is valid.
| Kind | Description |
|---|---|
| 0 | Metadata (profile) |
| 3 | Contacts (following) |
| 10000-19999 | Reserved replaceable |
Parameterized Replaceable Events
Latest per kind + pubkey + d-tag is valid.
| Kind | Description |
|---|---|
| 30000-39999 | Custom parameterized |
| 30023 | Long-form content |
Ephemeral Events
Not stored by relays.
| Kind | Description |
|---|---|
| 20000-29999 | Ephemeral |
Client-Relay Communication
Communication uses WebSocket with JSON messages.
Client to Relay
EVENT — Publish
["EVENT", <event JSON>]
REQ — Subscribe
["REQ", <subscription_id>, <filter1>, <filter2>, ...]
CLOSE — Unsubscribe
["CLOSE", <subscription_id>]
Relay to Client
EVENT — Deliver
["EVENT", <subscription_id>, <event JSON>]
OK — Publish Result
["OK", <event_id>, <true|false>, <message>]
EOSE — End of Stored Events
["EOSE", <subscription_id>]
NOTICE — Message
["NOTICE", <message>]
Filters
Filters specify which events to return:
{
"ids": ["event-id-1", "event-id-2"],
"authors": ["pubkey-1", "pubkey-2"],
"kinds": [1, 7],
"#e": ["referenced-event-id"],
"#p": ["referenced-pubkey"],
"#t": ["hashtag"],
"since": 1704067200,
"until": 1704153600,
"limit": 100
}
Filter Logic
- All fields are AND-ed
- Values within arrays are OR-ed
- Omitted fields match everything
idsandauthorssupport prefix matching
Example Queries
User’s recent notes:
{"authors": ["pubkey"], "kinds": [1], "limit": 50}
Replies to an event:
{"kinds": [1], "#e": ["event-id"]}
Notes with hashtag:
{"kinds": [1], "#t": ["bitcoin"]}
Subscription Flow
Client Relay
│ │
│─── REQ "sub-1" {filter} ────►│
│ │
│◄─── EVENT "sub-1" event1 ────│
│◄─── EVENT "sub-1" event2 ────│
│◄─── EVENT "sub-1" event3 ────│
│◄─── EOSE "sub-1" ────────────│
│ │
│ (new matching event) │
│◄─── EVENT "sub-1" event4 ────│
│ │
│─── CLOSE "sub-1" ───────────►│
Implementation Example
import asyncio
import websockets
import json
import time
import hashlib
from secp256k1 import PrivateKey
class NostrClient:
def __init__(self, relay_url, private_key_hex):
self.relay_url = relay_url
self.private_key = PrivateKey(bytes.fromhex(private_key_hex))
self.public_key = self.private_key.pubkey.serialize()[1:].hex()
self.ws = None
async def connect(self):
self.ws = await websockets.connect(self.relay_url)
def create_event(self, kind, content, tags=None):
event = {
"pubkey": self.public_key,
"created_at": int(time.time()),
"kind": kind,
"tags": tags or [],
"content": content
}
# Compute ID
serialized = json.dumps([
0, event["pubkey"], event["created_at"],
event["kind"], event["tags"], event["content"]
], separators=(',', ':'))
event["id"] = hashlib.sha256(serialized.encode()).hexdigest()
# Sign
event["sig"] = self.private_key.schnorr_sign(
bytes.fromhex(event["id"]), None
).hex()
return event
async def publish(self, event):
await self.ws.send(json.dumps(["EVENT", event]))
response = await self.ws.recv()
return json.loads(response)
async def subscribe(self, sub_id, filters):
await self.ws.send(json.dumps(["REQ", sub_id, *filters]))
async def receive_events(self, sub_id):
events = []
while True:
msg = json.loads(await self.ws.recv())
if msg[0] == "EVENT" and msg[1] == sub_id:
events.append(msg[2])
elif msg[0] == "EOSE" and msg[1] == sub_id:
break
return events
async def close(self):
await self.ws.close()
# Usage
async def main():
client = NostrClient(
"wss://relay.damus.io",
"your-private-key-hex"
)
await client.connect()
# Publish a note
event = client.create_event(
kind=1,
content="Hello from NIP-01!"
)
result = await client.publish(event)
print(f"Published: {result}")
# Subscribe to notes
await client.subscribe("my-sub", [{"kinds": [1], "limit": 10}])
events = await client.receive_events("my-sub")
print(f"Received {len(events)} events")
await client.close()
asyncio.run(main())
Verification
Always verify events before trusting:
def verify_event(event):
# 1. Verify ID
serialized = json.dumps([
0, event["pubkey"], event["created_at"],
event["kind"], event["tags"], event["content"]
], separators=(',', ':'))
expected_id = hashlib.sha256(serialized.encode()).hexdigest()
if event["id"] != expected_id:
return False, "ID mismatch"
# 2. Verify signature
from secp256k1 import PublicKey
pubkey = PublicKey(bytes.fromhex("02" + event["pubkey"]), raw=True)
valid = pubkey.schnorr_verify(
bytes.fromhex(event["id"]),
bytes.fromhex(event["sig"]),
None
)
return valid, "Valid" if valid else "Bad signature"
Machine-Readable Summary
{
"nip": 1,
"title": "Basic Protocol",
"status": "final",
"required": true,
"defines": [
"event-structure",
"event-signing",
"relay-protocol",
"filters",
"subscriptions"
],
"message_types": {
"client_to_relay": ["EVENT", "REQ", "CLOSE"],
"relay_to_client": ["EVENT", "OK", "EOSE", "NOTICE"]
},
"event_categories": {
"regular": "stored",
"replaceable": "latest-per-pubkey",
"parameterized_replaceable": "latest-per-pubkey-d-tag",
"ephemeral": "not-stored"
},
"related": [
"/learn/nostr/events",
"/learn/nostr/relays",
"/learn/nostr/signatures"
]
}