Table of Contents
- 1. Event Structure
- 2. Event ID Calculation
- 3. Event Signature
- 4. Tag Structure
- 5. WebSocket Message Protocol
- 6. Filter Structure
- Rule 6.1: Filter Fields
- Rule 6.2: Prefix Matching
- Rule 6.3: Tag Filters
- Rule 6.4: Time Range Filters
- Rule 6.5: Limit
- 7. Event Validation
- Optional Features
- Optional: Event Kind Classification
- Optional Rule 8.1: Kind Ranges
- Optional Rule 8.2: Replaceable Event Behavior
- Optional Rule 8.3: Parameterized Replaceable Events
- Optional: Address Format
- Optional: Proof of Work
- Optional Rule 10.1: Difficulty Measurement
- Optional Rule 10.2: Nonce Tag
- Optional Rule 10.3: Mining Process
- Optional: Full-Text Search
- Optional: Event Counting
- Optional: Event Relationships
- Optional: Event Expiration
- Optional: Event Sorting
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 characterspubkey: 32 bytes = 64 hex characterssig: 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 referencep: Public key referencea: 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 valueuntil: 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:
- All seven required fields exist
- Field types match specification
- Hex fields have correct length
- Tags is an array of arrays
- Each tag has at least two elements
- 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:
- Find existing events with same (kind, pubkey)
- Compare timestamps
- Keep only the newest event
- 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
Optional: Full-Text Search
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.