Proven: encrypted DM sent through relay.damus.io and nos.lol, fetched and decrypted. Library: nostr-sdk v0.44 (pip install nostr-sdk). Path to replace Telegram: keypairs per wizard, NIP-17 gift-wrapped DMs.
6.7 KiB
6.7 KiB
Nostr Protocol for Agent-to-Agent Communication - Research Report
1. How Nostr Relays Work for Private/Encrypted Messaging
Protocol Overview
- Nostr is a decentralized protocol based on WebSocket relays
- Clients connect to relays, publish signed events, and subscribe to event streams
- No accounts, no API keys, no registration - just secp256k1 keypairs
- Events are JSON objects with: id, pubkey, created_at, kind, tags, content, sig
NIP-04 (Legacy Encrypted DMs - Kind 4)
- Uses shared secret via ECDH (secp256k1 Diffie-Hellman)
- Content encrypted with AES-256-CBC
- Format:
<encrypted_base64>?iv=<iv_base64> - P-tag reveals recipient pubkey (metadata leak)
- Widely supported by all relays and clients
- GOOD ENOUGH for agent communication (agents don't need metadata privacy)
NIP-44 (Modern Encrypted DMs)
- Uses XChaCha20-Poly1305 with HKDF key derivation
- Better padding, authenticated encryption
- Used with NIP-17 (kind 1059 gift-wrapped DMs) for metadata privacy
- Recommended for new implementations
Relay Behavior for DMs
- Relays store kind:4 events and serve them to subscribers
- Filter by pubkey (p-tag) to get DMs addressed to you
- Most relays keep events indefinitely (or until storage limits)
- No relay authentication needed for basic usage
2. Python Libraries for Nostr
nostr-sdk (RECOMMENDED)
pip install nostr-sdk(v0.44.2)- Rust bindings via UniFFI - very fast, full-featured
- Built-in: NIP-04, NIP-44, relay client, event builder, filters
- Async support, WebSocket transport included
- 3.4MB wheel, no compilation needed
pynostr
pip install pynostr(v0.7.0)- Pure Python, lightweight
- NIP-04 encrypted DMs via EncryptedDirectMessage class
- RelayManager for WebSocket connections
- Good for simple use cases, more manual
nostr (python-nostr)
pip install nostr(v0.0.2)- Very minimal, older
- Basic key generation only
- NOT recommended for production
3. Keypair Generation & Encrypted DMs
Using nostr-sdk (recommended):
from nostr_sdk import Keys, nip04_encrypt, nip04_decrypt, nip44_encrypt, nip44_decrypt, Nip44Version
# Generate keypair
keys = Keys.generate()
print(keys.public_key().to_bech32()) # npub1...
print(keys.secret_key().to_bech32()) # nsec1...
# NIP-04 encrypt/decrypt
encrypted = nip04_encrypt(sender_sk, recipient_pk, "message")
decrypted = nip04_decrypt(recipient_sk, sender_pk, encrypted)
# NIP-44 encrypt/decrypt (recommended)
encrypted = nip44_encrypt(sender_sk, recipient_pk, "message", Nip44Version.V2)
decrypted = nip44_decrypt(recipient_sk, sender_pk, encrypted)
Using pynostr:
from pynostr.key import PrivateKey
key = PrivateKey() # Generate
encrypted = key.encrypt_message("hello", recipient_pubkey_hex)
decrypted = recipient_key.decrypt_message(encrypted, sender_pubkey_hex)
4. Minimum Viable Setup (TESTED & WORKING)
Full working code (nostr-sdk):
import asyncio
from datetime import timedelta
from nostr_sdk import (
Keys, ClientBuilder, EventBuilder, Filter, Kind,
nip04_encrypt, nip04_decrypt, Tag, NostrSigner, RelayUrl
)
RELAYS = ["wss://relay.damus.io", "wss://nos.lol"]
async def main():
# Generate 3 agent keys
timmy = Keys.generate()
ezra = Keys.generate()
bezalel = Keys.generate()
# Connect Timmy to relays
client = ClientBuilder().signer(NostrSigner.keys(timmy)).build()
for r in RELAYS:
await client.add_relay(RelayUrl.parse(r))
await client.connect()
await asyncio.sleep(3)
# Send encrypted DM: Timmy -> Ezra
msg = "Build complete. Deploy approved."
encrypted = nip04_encrypt(timmy.secret_key(), ezra.public_key(), msg)
builder = EventBuilder(Kind(4), encrypted).tags([
Tag.public_key(ezra.public_key())
])
output = await client.send_event_builder(builder)
print(f"Sent to {len(output.success)} relays")
# Fetch as Ezra
ezra_client = ClientBuilder().signer(NostrSigner.keys(ezra)).build()
for r in RELAYS:
await ezra_client.add_relay(RelayUrl.parse(r))
await ezra_client.connect()
await asyncio.sleep(3)
dm_filter = Filter().kind(Kind(4)).pubkey(ezra.public_key()).limit(10)
events = await ezra_client.fetch_events(dm_filter, timedelta(seconds=10))
for event in events.to_vec():
decrypted = nip04_decrypt(ezra.secret_key(), event.author(), event.content())
print(f"Received: {decrypted}")
asyncio.run(main())
TESTED RESULTS:
- 3 keypairs generated successfully
- Message sent to 2 public relays (relay.damus.io, nos.lol)
- Message fetched and decrypted by recipient
- NIP-04 and NIP-44 both verified working
- Total time: ~10 seconds including relay connections
5. Recommended Public Relays
| Relay | URL | Notes |
|---|---|---|
| Damus | wss://relay.damus.io | Popular, reliable |
| nos.lol | wss://nos.lol | Fast, good uptime |
| Nostr.band | wss://relay.nostr.band | Good for search |
| Nostr Wine | wss://relay.nostr.wine | Paid, very reliable |
| Purplepag.es | wss://purplepag.es | Good for discovery |
6. Can Nostr Replace Telegram for Agent Dispatch?
YES - with caveats:
Advantages over Telegram:
- No API key or bot token needed
- No account registration
- No rate limits from a central service
- End-to-end encrypted (Telegram bot API is NOT e2e encrypted)
- Decentralized - no single point of failure
- Free, no terms of service to violate
- Agents only need a keypair (32 bytes)
- Messages persist on relays (no need to be online simultaneously)
Challenges:
- No push notifications (must poll or maintain WebSocket)
- No guaranteed delivery (relay might be down)
- Relay selection matters for reliability (use 2-3 relays)
- No built-in message ordering guarantee
- Slightly more latency than Telegram (~1-3s relay propagation)
- No rich media (files, buttons) - text only for DMs
For Agent Dispatch Specifically:
- EXCELLENT for: status updates, task dispatch, coordination
- Messages are JSON-friendly (put structured data in content)
- Can use custom event kinds for different message types
- Subscription model lets agents listen for real-time events
- Perfect for fire-and-forget status messages
Recommended Architecture:
- Each agent has a persistent keypair (stored in config)
- All agents connect to 2-3 public relays
- Dispatch = encrypted DM with JSON payload
- Status updates = encrypted DMs back to coordinator
- Use NIP-04 for simplicity, NIP-44 for better security
- Maintain WebSocket connection for real-time, with polling fallback
Verdict: Nostr is a STRONG candidate for replacing Telegram
- Zero infrastructure needed
- More secure (e2e encrypted vs Telegram bot API)
- No API key management
- Works without any server we control
- Only dependency: public relays (many free ones available)