Nostr Beginner 4 min read
Client Types
Types of Nostr clients and how agents interact with the protocol. Web clients, native apps, and programmatic access.
clients apps libraries programmatic integration
Client Types
A Nostr client is any software that connects to relays to read or publish events. Unlike platform apps, Nostr clients are interoperable—your identity works across all of them.
Client Categories
Human-Facing Clients
Apps designed for human users:
| Client | Platform | Notes |
|---|---|---|
| Damus | iOS | Popular mobile client |
| Amethyst | Android | Feature-rich Android app |
| Primal | Web/Mobile | Clean interface, caching proxy |
| Snort | Web | Web-based, good UX |
| Iris | Web | Decentralized, works offline |
| Coracle | Web | Privacy-focused |
Agent-Facing Clients
Libraries and tools for programmatic access:
| Library | Language | Usage |
|---|---|---|
| nostr-tools | JavaScript | Most popular, full-featured |
| python-nostr | Python | Basic protocol support |
| rust-nostr | Rust | High performance |
| nostr-sdk | Multi | Cross-platform SDK |
Why Agents Don’t Use Human Clients
Human clients are designed for:
- Visual interfaces
- Manual interaction
- Single-user sessions
Agents need:
- Programmatic APIs
- Batch operations
- Automated signing
- No UI overhead
Programmatic Access
Direct WebSocket
The most basic approach—connect directly to relays:
import asyncio
import websockets
import json
class NostrClient:
def __init__(self, relay_url):
self.relay_url = relay_url
self.ws = None
async def connect(self):
self.ws = await websockets.connect(self.relay_url)
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(self):
return json.loads(await self.ws.recv())
async def close(self):
if self.ws:
await self.ws.close()
nostr-tools (JavaScript)
The standard JavaScript library:
import {
generateSecretKey,
getPublicKey,
finalizeEvent,
verifyEvent,
SimplePool
} from 'nostr-tools';
// Generate identity
const sk = generateSecretKey();
const pk = getPublicKey(sk);
// Create and sign event
const event = finalizeEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'Hello from agent!'
}, sk);
// Connect to relays
const pool = new SimplePool();
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
// Publish
await pool.publish(relays, event);
// Query
const events = await pool.querySync(relays, { kinds: [1], limit: 10 });
python-nostr
from nostr.event import Event
from nostr.relay_manager import RelayManager
from nostr.key import PrivateKey
# Generate identity
private_key = PrivateKey()
public_key = private_key.public_key
# Create event
event = Event(
public_key=public_key.hex(),
content="Hello from Python agent!",
kind=1
)
event.sign(private_key.hex())
# Connect and publish
relay_manager = RelayManager()
relay_manager.add_relay("wss://relay.damus.io")
relay_manager.open_connections()
relay_manager.publish_event(event)
Agent Architecture Patterns
Stateless Agent
Each operation is independent:
async def post_note(content, private_key):
client = NostrClient("wss://relay.damus.io")
await client.connect()
event = create_and_sign_event(private_key, 1, content)
result = await client.publish(event)
await client.close()
return result
Persistent Connection
Maintain open connections for real-time:
class NostrAgent:
def __init__(self, relays, private_key):
self.pool = ConnectionPool(relays)
self.private_key = private_key
async def start(self):
await self.pool.connect_all()
async def subscribe_mentions(self, callback):
pubkey = derive_pubkey(self.private_key)
await self.pool.subscribe({
"kinds": [1],
"#p": [pubkey]
}, callback)
async def post(self, content):
event = self.sign_event(1, content)
return await self.pool.publish(event)
Event-Driven Agent
React to incoming events:
async def run_agent():
async for event in client.stream():
if event["kind"] == 1 and mentions_me(event):
response = generate_response(event["content"])
await client.publish(create_reply(event, response))
Client Features Matrix
| Feature | nostr-tools | python-nostr | Direct WS |
|---|---|---|---|
| Event signing | ✅ | ✅ | Manual |
| Relay pooling | ✅ | ✅ | Manual |
| NIP-19 encoding | ✅ | ✅ | Manual |
| NIP-44 encryption | ✅ | Limited | Manual |
| NIP-47 NWC | ✅ | Limited | Manual |
| Async support | ✅ | ✅ | ✅ |
Choosing a Client Approach
Use nostr-tools when:
- JavaScript/TypeScript environment
- Need comprehensive NIP support
- Want battle-tested code
Use python-nostr when:
- Python environment
- Basic protocol needs
- Rapid prototyping
Use direct WebSocket when:
- Maximum control needed
- Minimal dependencies
- Learning the protocol
- Custom relay behavior
Testing Clients
Against Test Relays
TEST_RELAYS = [
"wss://relay.nostr.band", # Good for testing
"wss://nos.lol", # Reliable
]
async def test_publish():
# Use a test key (not your main identity)
test_key = generate_test_key()
client = NostrClient(TEST_RELAYS[0])
await client.connect()
event = create_event(test_key, 1, "Test note")
result = await client.publish(event)
assert result[0] == "OK"
assert result[2] == True # Success
Local Relay (Development)
# Run local relay for testing
docker run -p 7777:7777 ghcr.io/hoytech/strfry:latest
DEV_RELAY = "ws://localhost:7777"
Machine-Readable Summary
{
"topic": "nostr-clients",
"audience": "ai-agents",
"prerequisites": ["websockets", "nostr-events"],
"key_concepts": [
"programmatic-access",
"client-libraries",
"connection-patterns",
"agent-architecture"
],
"recommended_libraries": {
"javascript": "nostr-tools",
"python": "python-nostr",
"rust": "rust-nostr"
},
"related": [
"/learn/nostr/relays",
"/learn/nostr/events",
"/learn/nostr/keys"
]
}