1
[Spec] Nostr Protocol Specification
jay edited this page 2025-12-30 09:59:21 -05:00

1. Event Structure

An event is the fundamental data unit. Every event contains exactly seven fields.

Event {
  id: 64-character lowercase hexadecimal string
  pubkey: 64-character lowercase hexadecimal string  
  created_at: integer (Unix timestamp in seconds)
  kind: integer
  tags: array of arrays of strings
  content: string
  sig: 128-character lowercase hexadecimal signature
}

Example:

{
  "id": "4376c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65",
  "pubkey": "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
  "created_at": 1673347337,
  "kind": 1,
  "tags": [["p", "91cf9..4e5ca"], ["e", "ae3f2..7b29d"]],
  "content": "Hello, Nostr!",
  "sig": "908a15e46fb4d8675bab026fc230a0e3542bfade63da02d542fb78b2a8513fcd0092619a2c8c1221e581946e0191f2af505dfdf8657a414dbca329186f009262"
}

Rule 1.1: Field Requirements

All seven fields must be present. No field may be null or omitted.

Rule 1.2: Hexadecimal Encoding

The id, pubkey, and sig fields use lowercase hexadecimal encoding without prefix.

  • id: 32 bytes = 64 hex characters
  • pubkey: 32 bytes = 64 hex characters
  • sig: 64 bytes = 128 hex characters

2. Event ID Calculation

The event ID is a SHA-256 hash of a serialized form.

Rule 2.1: Serialization Format

Serialize as a JSON array with six elements in this exact order:

[0, pubkey, created_at, kind, tags, content]

The zero is a protocol version indicator.

Example serialization:

[0,"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",1673347337,1,[["p","91cf9..4e5ca"]],"Hello"]

Rule 2.2: Canonical JSON

The serialization must use canonical JSON:

  • No whitespace between elements
  • No whitespace after separators
  • UTF-8 encoding
  • Minimum representation (no trailing zeros on numbers)

Rule 2.3: Hash Computation

serialized_bytes = UTF8_encode(canonical_json)
hash_bytes = SHA256(serialized_bytes)
event.id = lowercase_hex_encode(hash_bytes)

Example:

Input: [0,"6e46...",1673347337,1,[],"Hello"]
SHA-256: 4376c65d2f232afbe9b882a35baa4f6fe8667c4e684749af565f981833ed6a65

3. Event Signature

The signature proves the event was created by the holder of the private key corresponding to pubkey.

Rule 3.1: Signature Generation

signature_bytes = schnorr_sign(private_key, event_id_bytes)
event.sig = lowercase_hex_encode(signature_bytes)

Uses Schnorr signature scheme over the secp256k1 elliptic curve.

Rule 3.2: Signature Verification

is_valid = schnorr_verify(
  public_key: event.pubkey_bytes,
  message: event.id_bytes,
  signature: event.sig_bytes
)

Returns true if signature is valid, false otherwise.

Rule 3.3: Key Derivation

The public key is derived from the private key using secp256k1 scalar multiplication:

public_key_point = private_key * G
event.pubkey = lowercase_hex_encode(public_key_point.x_coordinate)

Where G is the secp256k1 generator point.

4. Tag Structure

Tags are arrays of strings. The first element identifies the tag type.

Rule 4.1: Minimum Tag Length

Every tag must contain at least two elements: a tag name and a value.

valid_tag = ["e", "ae3f2..7b29d"]
invalid_tag = ["e"]  // missing value

Rule 4.2: Tag Array Format

Event.tags = [
  [tag_name, value, optional_1, optional_2, ...],
  [tag_name, value, optional_1, optional_2, ...],
  ...
]

Example:

"tags": [
  ["e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://relay.example.com"],
  ["p", "91cf9..4e5ca", "", "alice"],
  ["d", "my-article-slug"]
]

Rule 4.3: Common Tag Types

Standard single-letter tag names:

  • e: Event ID reference
  • p: Public key reference
  • a: Address reference (for replaceable events)
  • d: Identifier (for parameterized replaceable events)
  • t: Topic/hashtag

Additional elements in the array provide context (relay hints, markers, etc.).

5. WebSocket Message Protocol

All communication uses JSON arrays over WebSocket connections.

Rule 5.1: Client to Relay Messages

EVENT - Publish an event:

["EVENT", <event-object>]

REQ - Subscribe to events:

["REQ", <subscription-id>, <filter-object>, <filter-object>, ...]

CLOSE - End subscription:

["CLOSE", <subscription-id>]

AUTH - Authenticate with relay:

["AUTH", <signed-event>]

Example:

["REQ", "my-sub-1", {"kinds": [1], "limit": 10}]

Rule 5.2: Relay to Client Messages

EVENT - Deliver matching event:

["EVENT", <subscription-id>, <event-object>]

EOSE - End of stored events:

["EOSE", <subscription-id>]

OK - Confirm event acceptance:

["OK", <event-id>, <true|false>, <message>]

CLOSED - Subscription ended:

["CLOSED", <subscription-id>, <message>]

NOTICE - Human-readable message:

["NOTICE", <message>]

AUTH - Request authentication:

["AUTH", <challenge-string>]

Example:

["OK", "4376c65d...", true, "Event accepted"]

Rule 5.3: Message Ordering

Relays send stored events first, then EOSE, then real-time events for active subscriptions.

CLIENT: ["REQ", "sub1", {...filter...}]
RELAY:  ["EVENT", "sub1", {...stored event 1...}]
RELAY:  ["EVENT", "sub1", {...stored event 2...}]
RELAY:  ["EOSE", "sub1"]
RELAY:  ["EVENT", "sub1", {...new real-time event...}]

6. Filter Structure

Filters define subscription criteria. All conditions within a filter are combined with AND logic. Multiple filters in one REQ are combined with OR logic.

Rule 6.1: Filter Fields

Filter {
  ids: [string, ...]           // event ID prefixes
  authors: [string, ...]       // pubkey prefixes
  kinds: [integer, ...]        // event kinds
  since: integer               // minimum timestamp
  until: integer               // maximum timestamp
  limit: integer               // maximum results
  #<letter>: [string, ...]     // tag filters
}

All fields are optional. An empty filter matches all events.

Example:

{
  "ids": ["4376c65d"],
  "authors": ["6e468422", "91cf9b32"],
  "kinds": [1, 6, 7],
  "since": 1673347337,
  "until": 1673433737,
  "limit": 100,
  "#e": ["5c83da77", "ae3f2a91"],
  "#p": ["91cf9b32"]
}

Rule 6.2: Prefix Matching

The ids and authors fields support prefix matching. A filter value matches if it equals the initial substring of the event field.

filter.ids = ["4376c6"]
matches: "4376c65d2f232afbe9b882a35baa4f6fe8667c4e..."
rejects: "5c83da77..."

Minimum recommended prefix length: 4 characters (8 hex digits).

Rule 6.3: Tag Filters

Tag filters use the format #<single-letter> as the field name.

filter["#e"] = ["5c83da77...", "ae3f2a91..."]

Matches events containing tags where:

  • First element (tag name) equals the letter
  • Second element (tag value) matches any value in the filter array

Example matching:

Event tags: [["e", "5c83da77..."], ["p", "91cf9..."]]
Filter: {"#e": ["5c83da77..."]}
Result: MATCH (event has "e" tag with matching value)

Rule 6.4: Time Range Filters

  • since: Event timestamp must be ≥ this value
  • until: Event timestamp must be ≤ this value

Both use Unix timestamps (seconds since 1970-01-01 00:00:00 UTC).

Rule 6.5: Limit

The limit field caps the number of events returned. Relays should return the most recent events when applying limits.

7. Event Validation

Rule 7.1: Structure Validation

Check that:

  1. All seven required fields exist
  2. Field types match specification
  3. Hex fields have correct length
  4. Tags is an array of arrays
  5. Each tag has at least two elements
  6. Timestamp is an integer

Rule 7.2: ID Validation

Recompute the event ID and verify it matches the stored id field:

computed_id = sha256_hex(serialize([0, pubkey, created_at, kind, tags, content]))
is_valid = (computed_id == event.id)

Rule 7.3: Signature Validation

Verify the Schnorr signature:

is_valid = schnorr_verify(
  pubkey_bytes,
  id_bytes,
  sig_bytes
)

An event must pass both ID validation and signature validation to be considered authentic.


Optional Features

The following features are implemented by some clients and relays but are not required for basic protocol operation.

Optional: Event Kind Classification

Some implementations categorize event kinds by behavior.

Optional Rule 8.1: Kind Ranges

Regular events (default): Stored permanently with unique IDs.

Replaceable events (10000-19999): Newest event per (kind, pubkey) replaces all previous.

  • Also: kinds 0, 3 are treated as replaceable

Ephemeral events (20000-29999): Not stored by relays, only forwarded.

Parameterized replaceable events (30000-39999): Newest event per (kind, pubkey, d-tag-value) replaces previous.

Optional Rule 8.2: Replaceable Event Behavior

When a relay receives a replaceable event:

  1. Find existing events with same (kind, pubkey)
  2. Compare timestamps
  3. Keep only the newest event
  4. Delete older events

Example:

Existing: kind=0, pubkey="abc123", created_at=1000
New:      kind=0, pubkey="abc123", created_at=1500
Action:   Delete existing, store new

Optional Rule 8.3: Parameterized Replaceable Events

Uses a d tag to create unique identifiers:

{
  "kind": 30023,
  "pubkey": "abc123...",
  "tags": [["d", "my-article"]],
  "content": "..."
}

Replacement key: (kind=30023, pubkey="abc123...", d="my-article")

Optional: Address Format

Parameterized replaceable events can be referenced by address instead of ID.

Optional Rule 9.1: Address Construction

address = "<kind>:<pubkey>:<d-tag-value>"

Example:

"30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:my-article"

Optional Rule 9.2: Address Tag

Reference addresses in tags:

["a", "30023:6e4684...:my-article", "wss://relay.example.com"]

Optional: Proof of Work

Events can include proof-of-work by mining a nonce.

Optional Rule 10.1: Difficulty Measurement

Count leading zero bits in the event ID:

difficulty = count_leading_zero_bits(event.id)

Example:

event.id = "000000af4b3c2..." (24 leading zero bits = difficulty 24)

Optional Rule 10.2: Nonce Tag

Include a nonce tag:

["nonce", "12345678", "24"]

Where:

  • First value: nonce counter
  • Second value: target difficulty
  • Third value (optional): commitment

Optional Rule 10.3: Mining Process

target_difficulty = 24
nonce = 0
loop:
  event.tags = [["nonce", string(nonce), string(target_difficulty)]]
  event.id = compute_event_id(event)
  if count_leading_zero_bits(event.id) >= target_difficulty:
    break
  nonce = nonce + 1

Filters may include a search field.

Optional Rule 11.1: Search Filter

{
  "kinds": [1],
  "search": "bitcoin protocol"
}

Matches events where the content field contains the search terms. Implementation-specific (case sensitivity, word boundaries, etc.).

Optional: Event Counting

Request event counts without retrieving full events.

Optional Rule 12.1: COUNT Command

Client sends:

["COUNT", <subscription-id>, <filter-object>, ...]

Relay responds:

["COUNT", <subscription-id>, {"count": 47}]

Optional: Event Relationships

Track parent-child relationships between events.

Optional Rule 13.1: Reply Markers

The e tag can include a marker:

["e", "<parent-event-id>", "<relay-hint>", "reply"]

Markers: reply, root, mention

Optional Rule 13.2: Parent Extraction

get_parent_ids(event):
  return [tag[1] for tag in event.tags where tag[0] == "e"]

get_parent_addresses(event):
  return [tag[1] for tag in event.tags where tag[0] == "a"]

Optional: Event Expiration

Events can specify an expiration time.

Optional Rule 14.1: Expiration Tag

["expiration", "1673433737"]

Value is a Unix timestamp. Relays should delete the event after this time.

Optional Rule 14.2: Expiration Check

is_expired(event, current_time):
  for tag in event.tags:
    if tag[0] == "expiration":
      return current_time > parse_int(tag[1])
  return false

Optional: Event Sorting

Canonical ordering for event lists.

Optional Rule 15.1: Sort Order

Primary sort: created_at descending (newest first)
Tiebreaker: id ascending (lexicographic)

sort_events(events):
  return sorted(events, key=lambda e: (-e.created_at, e.id))

This gives a deterministic ordering when multiple events have the same timestamp.