Menu
Nostr Python Executable Jan 31, 2026

Post Notes to Nostr Relays

Publish text notes (kind:1 events) to Nostr relays via WebSocket connection

#relays #websocket #publish #nip-01

Overview

Publish signed events to Nostr relays. Relays are the message-passing infrastructure of Nostr - you send events to relays, and they distribute them to subscribers.

The Code

"""
Nostr Relay Publisher
Post events to Nostr relays via WebSocket

Requirements: websocket-client (pip install websocket-client)

Note: For production, use nostr-sdk or pynostr for proper event signing.
This example assumes you have a pre-signed event.
"""

import json
import time
import hashlib
import secrets
from typing import List, Optional
import websocket

# Default relays (use your preferred relays)
DEFAULT_RELAYS = [
    "wss://relay.damus.io",
    "wss://nos.lol",
    "wss://relay.nostr.band",
    "wss://nostr.wine"
]


def create_unsigned_event(
    pubkey: str,
    kind: int,
    content: str,
    tags: Optional[List[List[str]]] = None
) -> dict:
    """
    Create an unsigned event structure.
    In production, sign this with your private key.
    """
    if tags is None:
        tags = []

    created_at = int(time.time())

    event = {
        "pubkey": pubkey,
        "created_at": created_at,
        "kind": kind,
        "tags": tags,
        "content": content
    }

    # Calculate event ID
    serialized = json.dumps([
        0, event["pubkey"], event["created_at"],
        event["kind"], event["tags"], event["content"]
    ], separators=(',', ':'), ensure_ascii=False)

    event["id"] = hashlib.sha256(serialized.encode()).hexdigest()

    return event


def publish_to_relay(relay_url: str, event: dict, timeout: int = 10) -> dict:
    """
    Publish an event to a single relay.

    Args:
        relay_url: WebSocket URL of the relay
        event: Signed Nostr event
        timeout: Connection timeout in seconds

    Returns:
        dict with success status and message
    """
    result = {
        "relay": relay_url,
        "success": False,
        "message": ""
    }

    try:
        ws = websocket.create_connection(
            relay_url,
            timeout=timeout
        )

        # Send EVENT message
        message = json.dumps(["EVENT", event])
        ws.send(message)

        # Wait for OK response
        response = ws.recv()
        ws.close()

        data = json.loads(response)

        if data[0] == "OK":
            event_id = data[1]
            accepted = data[2]
            reason = data[3] if len(data) > 3 else ""

            result["success"] = accepted
            result["message"] = reason if reason else "Accepted"
            result["event_id"] = event_id
        else:
            result["message"] = f"Unexpected response: {data[0]}"

    except websocket.WebSocketTimeoutException:
        result["message"] = "Connection timeout"
    except websocket.WebSocketConnectionClosedException:
        result["message"] = "Connection closed"
    except Exception as e:
        result["message"] = str(e)

    return result


def publish_to_relays(
    event: dict,
    relays: Optional[List[str]] = None,
    min_success: int = 1
) -> dict:
    """
    Publish an event to multiple relays.

    Args:
        event: Signed Nostr event
        relays: List of relay URLs (uses defaults if None)
        min_success: Minimum successful publishes required

    Returns:
        dict with overall status and per-relay results
    """
    if relays is None:
        relays = DEFAULT_RELAYS

    results = []
    success_count = 0

    for relay in relays:
        print(f"  Publishing to {relay}...", end=" ", flush=True)
        result = publish_to_relay(relay, event)
        results.append(result)

        if result["success"]:
            success_count += 1
            print(f"OK")
        else:
            print(f"FAILED: {result['message']}")

    return {
        "success": success_count >= min_success,
        "success_count": success_count,
        "total_relays": len(relays),
        "results": results,
        "event_id": event["id"]
    }


def create_text_note(content: str, hashtags: Optional[List[str]] = None) -> dict:
    """
    Create a kind:1 text note event (unsigned).

    Args:
        content: Note text
        hashtags: Optional list of hashtags

    Returns:
        Unsigned event (needs signature)
    """
    tags = []
    if hashtags:
        for tag in hashtags:
            tags.append(["t", tag.lower().strip("#")])

    # Placeholder pubkey - replace with your actual public key
    pubkey = "0" * 64

    return create_unsigned_event(
        pubkey=pubkey,
        kind=1,
        content=content,
        tags=tags
    )


def simulate_signed_event() -> dict:
    """
    Create a simulated signed event for testing.
    In production, use proper key signing.
    """
    event = create_text_note(
        content="Hello from an AI agent! Testing Nostr publishing.",
        hashtags=["nostr", "ai", "agent"]
    )

    # Add a fake signature (won't be accepted by real relays)
    event["sig"] = "0" * 128

    return event


# Example usage
if __name__ == "__main__":
    print("=== Nostr Note Publisher ===\n")

    # Create a test event
    # NOTE: This uses a placeholder signature and won't be accepted
    # In production, sign with your private key
    event = simulate_signed_event()

    print(f"Event ID: {event['id'][:16]}...")
    print(f"Content:  {event['content']}")
    print(f"Tags:     {event['tags']}")

    print(f"\n=== Publishing to Relays ===")

    # Test with a subset of relays
    test_relays = [
        "wss://relay.damus.io",
        "wss://nos.lol"
    ]

    result = publish_to_relays(
        event=event,
        relays=test_relays,
        min_success=1
    )

    print(f"\n=== Results ===")
    print(f"Success: {result['success']}")
    print(f"Accepted by: {result['success_count']}/{result['total_relays']} relays")

    # Note about unsigned events
    print("\n" + "=" * 50)
    print("NOTE: This demo uses unsigned events which relays")
    print("will reject. For real publishing, you need to:")
    print("1. Generate or load your private key")
    print("2. Sign events with BIP-340 Schnorr signatures")
    print("3. Use nostr-sdk or pynostr for production")
    print("=" * 50)

Usage

# Install WebSocket client
pip install websocket-client

# Run the publisher
python post_note.py

Example Output

=== Nostr Note Publisher ===

Event ID: abc123def456...
Content:  Hello from an AI agent! Testing Nostr publishing.
Tags:     [['t', 'nostr'], ['t', 'ai'], ['t', 'agent']]

=== Publishing to Relays ===
  Publishing to wss://relay.damus.io... FAILED: invalid: bad signature
  Publishing to wss://nos.lol... FAILED: invalid: bad signature

=== Results ===
Success: False
Accepted by: 0/2 relays

==================================================
NOTE: This demo uses unsigned events which relays
will reject. For real publishing, you need to:
1. Generate or load your private key
2. Sign events with BIP-340 Schnorr signatures
3. Use nostr-sdk or pynostr for production
==================================================

Agent Notes

Relay selection strategy:

  1. Use multiple relays for redundancy (3-5 recommended)
  2. Include popular relays for reach (damus.io, nos.lol)
  3. Include specialized relays for your use case
  4. Respect relay policies (some require payment or NIP-05)

Publishing best practices:

  • Publish to multiple relays simultaneously
  • Consider at least 1-2 successful publishes as “posted”
  • Retry failed relays with backoff
  • Store relay results for debugging

Rate limits: Most relays have rate limits. For agents:

  • Don’t spam (>1 post/minute may trigger limits)
  • Use reasonable delays between posts
  • Some relays require proof-of-work (NIP-13)