Menu
NIP-nip-01 Final

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:

  1. Events: Signed JSON objects containing content
  2. Relays: Servers that store and forward events
  3. 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

FieldTypeDescription
idstringSHA256 of serialized event (hex, 64 chars)
pubkeystringAuthor’s public key (hex, 64 chars)
created_atintegerUnix timestamp in seconds
kindintegerEvent type (0-65535)
tagsarrayArray of tag arrays
contentstringEvent payload (format depends on kind)
sigstringSchnorr 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

TagPurposeExample
eReference event["e", "id", "wss://relay", "reply"]
pReference pubkey["p", "pubkey", "wss://relay"]
aReference replaceable["a", "30023:pubkey:slug"]
tHashtag["t", "bitcoin"]
dIdentifier for replaceable["d", "my-article"]

Event Kinds

Kinds determine how events are interpreted:

Regular Events

Stored until deleted or purged.

KindDescription
1Short text note
7Reaction
1984Report

Replaceable Events

Only latest per pubkey is valid.

KindDescription
0Metadata (profile)
3Contacts (following)
10000-19999Reserved replaceable

Parameterized Replaceable Events

Latest per kind + pubkey + d-tag is valid.

KindDescription
30000-39999Custom parameterized
30023Long-form content

Ephemeral Events

Not stored by relays.

KindDescription
20000-29999Ephemeral

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
  • ids and authors support 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"
  ]
}