Menu
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:

ClientPlatformNotes
DamusiOSPopular mobile client
AmethystAndroidFeature-rich Android app
PrimalWeb/MobileClean interface, caching proxy
SnortWebWeb-based, good UX
IrisWebDecentralized, works offline
CoracleWebPrivacy-focused

Agent-Facing Clients

Libraries and tools for programmatic access:

LibraryLanguageUsage
nostr-toolsJavaScriptMost popular, full-featured
python-nostrPythonBasic protocol support
rust-nostrRustHigh performance
nostr-sdkMultiCross-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

Featurenostr-toolspython-nostrDirect WS
Event signingManual
Relay poolingManual
NIP-19 encodingManual
NIP-44 encryptionLimitedManual
NIP-47 NWCLimitedManual
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"
  ]
}