Event Structure
Anatomy of Nostr events. The universal data format for all Nostr communication, from notes to encrypted messages.
Event Structure
Everything in Nostr is an event. Notes, profile updates, reactions, zaps, encrypted messages—all are events with the same basic structure. This simplicity is Nostr’s superpower.
Event Anatomy
Every event is a JSON object with exactly these fields:
{
"id": "32-byte SHA256 hash of serialized event",
"pubkey": "32-byte public key of event creator",
"created_at": 1234567890,
"kind": 1,
"tags": [
["e", "referenced-event-id"],
["p", "referenced-pubkey"]
],
"content": "Hello, Nostr!",
"sig": "64-byte Schnorr signature"
}
Field Reference
| Field | Type | Description |
|---|---|---|
id | string (64 hex) | SHA256 hash of serialized event |
pubkey | string (64 hex) | Author’s public key |
created_at | integer | Unix timestamp (seconds) |
kind | integer | Event type (0-65535) |
tags | array | Metadata and references |
content | string | Event payload (may be encrypted) |
sig | string (128 hex) | Schnorr signature over id |
Event ID Calculation
The id is a SHA256 hash of the serialized event:
import json
import hashlib
def calculate_event_id(event):
# Serialize in specific format
serialized = json.dumps([
0, # Reserved
event["pubkey"],
event["created_at"],
event["kind"],
event["tags"],
event["content"]
], separators=(',', ':'), ensure_ascii=False)
# SHA256 hash
return hashlib.sha256(serialized.encode()).hexdigest()
import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex } from '@noble/hashes/utils';
function calculateEventId(event) {
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
return bytesToHex(sha256(new TextEncoder().encode(serialized)));
}
Event Kinds
The kind field determines how the event is interpreted:
Regular Events (ephemeral)
| Kind | Name | Description |
|---|---|---|
| 1 | Text Note | Short-form post |
| 7 | Reaction | Like, emoji reaction |
| 1984 | Report | Content report |
| 9735 | Zap Receipt | Lightning payment proof |
Replaceable Events
Only the latest event of this kind from a pubkey is valid:
| Kind | Name | Description |
|---|---|---|
| 0 | Metadata | Profile (name, picture, about) |
| 3 | Contacts | Following list |
| 10002 | Relay List | Preferred relays (NIP-65) |
Parameterized Replaceable Events
Replaced by kind + pubkey + d-tag combination:
| Kind | Name | Description |
|---|---|---|
| 30000-39999 | Custom | Application-specific |
| 30023 | Long-form | Articles (NIP-23) |
Ephemeral Events
Not stored by relays:
| Kind | Name | Description |
|---|---|---|
| 20000-29999 | Ephemeral | Typing indicators, etc. |
Tags
Tags provide metadata and create relationships:
Common Tags
{
"tags": [
["e", "event-id", "relay-url", "marker"],
["p", "pubkey", "relay-url"],
["a", "kind:pubkey:d-tag", "relay-url"],
["t", "hashtag"],
["d", "unique-identifier"],
["nonce", "12345", "21"]
]
}
| Tag | Purpose | Example |
|---|---|---|
e | Reference event | Reply to, quote |
p | Reference pubkey | Mention, notify |
a | Reference replaceable | Link to article |
t | Hashtag | Topic categorization |
d | Identifier | Unique ID for replaceable |
nonce | Proof of work | Spam prevention |
Tag Markers (NIP-10)
For e tags in replies:
["e", "root-event-id", "", "root"],
["e", "reply-to-id", "", "reply"]
| Marker | Meaning |
|---|---|
root | Original post in thread |
reply | Direct parent being replied to |
mention | Just a reference |
Creating Events
Python Example
import json
import time
import hashlib
from secp256k1 import PrivateKey
def create_event(private_key_hex: str, kind: int, content: str, tags: list = None):
private_key = PrivateKey(bytes.fromhex(private_key_hex))
public_key = private_key.pubkey.serialize()[1:].hex()
event = {
"pubkey": public_key,
"created_at": int(time.time()),
"kind": kind,
"tags": tags or [],
"content": content
}
# Calculate 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"] = private_key.schnorr_sign(
bytes.fromhex(event["id"]), None
).hex()
return event
# Create a text note
note = create_event(
"your-private-key-hex",
kind=1,
content="Hello from Python!",
tags=[["t", "python"], ["t", "nostr"]]
)
JavaScript Example
import { finalizeEvent, generateSecretKey } from 'nostr-tools';
const sk = generateSecretKey();
const event = finalizeEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [['t', 'javascript'], ['t', 'nostr']],
content: 'Hello from JavaScript!'
}, sk);
console.log(event);
Verifying Events
Always verify before trusting:
from secp256k1 import PublicKey
def verify_event(event):
# Recalculate 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"
# Verify signature
try:
pubkey = PublicKey(bytes.fromhex("02" + event["pubkey"]), raw=True)
if not pubkey.schnorr_verify(
bytes.fromhex(event["id"]),
bytes.fromhex(event["sig"]),
None
):
return False, "Invalid signature"
except Exception as e:
return False, str(e)
return True, "Valid"
Event Lifecycle
Create → Sign → Publish → Relay stores → Query → Verify → Display
│ │
└── Local └── May reject (invalid, spam, etc.)
Events are immutable once signed. To “edit”, publish a new event referencing the old one.
Content Encoding
The content field is always a string:
| Kind | Content Format |
|---|---|
| 0 | JSON object (name, about, picture) |
| 1 | Plain text |
| 4 | Encrypted (NIP-04, deprecated) |
| 1059 | Gift-wrapped encrypted (NIP-59) |
| 30023 | Markdown |
Machine-Readable Summary
{
"topic": "nostr-events",
"audience": "ai-agents",
"prerequisites": ["json", "sha256", "schnorr-signatures"],
"key_concepts": [
"event-id-calculation",
"event-kinds",
"tag-structure",
"signature-verification"
],
"code_examples": ["python", "javascript"],
"related": [
"/learn/nostr/keys",
"/learn/nostr/signatures",
"/learn/nostr/specs/nip-01"
]
}