asgayapedia

Nostr Component

Purpose: Encrypted peer-to-peer messaging for payment instructions between buyer and seller

Complexity: Low - WebSocket connections + NIP-04 encryption


Overview

Nostr enables María and Isabel to coordinate payment details privately:

Key features:


What Is Nostr?

Protocol Overview

Definition: Notes and Other Stuff Transmitted by Relays

Architecture:

No blockchain: Just WebSocket servers passing messages

Why Nostr?


Nostr Identity

Key Pair

Each user has:

Derivation from wallet:

function getNostrKeys():
  // Derive from wallet seed (same entropy as BCH keys)
  wallet_seed = getWalletSeed()
  nostr_private_key = deriveNostrKey(wallet_seed, path="m/44'/1237'/0'/0/0")
  nostr_public_key = getPublicKey(nostr_private_key)
  
  return {
    private_key: nostr_private_key,  // Keep secret
    public_key: nostr_public_key     // Share in listings
  }

Why derive from wallet? One seed phrase backs up everything (BCH + Nostr)

Encoding:


Relay Management

Connecting to Relays

Strategy: Connect to 3-5 public relays for redundancy

Pseudocode:

RELAYS = [
  "wss://relay.damus.io",
  "wss://relay.snort.social",
  "wss://nos.lol"
]

function connectToRelays():
  connections = []
  
  for relay_url in RELAYS:
    try:
      ws = WebSocket(relay_url)
      ws.onopen = () => {
        log("Connected to " + relay_url)
        subscribeToMessages(ws)
      }
      connections.push(ws)
    catch ConnectionError:
      log_warning("Failed to connect to " + relay_url)
      // Continue with other relays
  
  if connections.length == 0:
    return error("No relays available")
  
  return connections

Redundancy: If 1-2 relays fail, others still work

Fallback: If all relays fail, queue messages for retry when connectivity returns


Subscribing to Messages

What: Tell relay to send me messages addressed to my public key

Pseudocode:

function subscribeToMessages(relay_connection):
  my_public_key = getNostrKeys().public_key
  
  subscribe_message = {
    type: "REQ",
    subscription_id: "asgaya_messages",
    filters: [
      {
        kinds: [4],  // Kind 4 = encrypted DM
        "#p": [my_public_key]  // Recipient is me
      }
    ]
  }
  
  relay_connection.send(json_encode(subscribe_message))
  
  relay_connection.onmessage = (event) => {
    handleIncomingMessage(event)
  }

WebSocket message format:

["REQ", "asgaya_messages", {"kinds": [4], "#p": ["npub1..."]}]

Relay response: Sends all past encrypted DMs + streams new ones


Encrypted Messaging (NIP-04)

Sending a Message

What: Encrypt payment instructions, send to Isabel

Pseudocode:

function sendNostrDM(recipient_pubkey, message_text):
  my_keys = getNostrKeys()
  
  // Encrypt with NIP-04 (ECDH + AES-256-CBC)
  encrypted_content = nip04_encrypt(
    sender_private_key: my_keys.private_key,
    recipient_public_key: recipient_pubkey,
    plaintext: message_text
  )
  
  // Build Nostr event
  event = {
    kind: 4,  // Encrypted DM
    pubkey: my_keys.public_key,
    created_at: now(),
    tags: [
      ["p", recipient_pubkey]  // Recipient
    ],
    content: encrypted_content
  }
  
  // Sign event
  event.id = hash(event)
  event.sig = sign(event.id, my_keys.private_key)
  
  // Send to all connected relays
  for relay in active_relays:
    relay.send(json_encode(["EVENT", event]))
  
  return event.id

NIP-04 encryption (simplified):

shared_secret = ECDH(my_private_key, recipient_public_key)
encrypted = AES256_CBC_encrypt(plaintext, key=shared_secret)
content = base64(encrypted)

Actual implementation: Use Nostr library (handles NIP-04 complexity)


Receiving a Message

What: Decrypt incoming DM from relay

Pseudocode:

function handleIncomingMessage(nostr_event):
  // Verify signature
  if not verify_signature(nostr_event):
    log_warning("Invalid signature, ignoring")
    return
  
  // Decrypt content
  my_keys = getNostrKeys()
  sender_pubkey = nostr_event.pubkey
  
  plaintext = nip04_decrypt(
    recipient_private_key: my_keys.private_key,
    sender_public_key: sender_pubkey,
    ciphertext: nostr_event.content
  )
  
  // Parse message (JSON payload)
  message = json_decode(plaintext)
  
  // Handle based on type
  if message.type == "payment_request":
    handlePaymentRequest(message)  // Isabel receives this, responds with payment_instruction
  else if message.type == "payment_instruction":
    handlePaymentInstruction(message)  // María receives this, uses it to pay Isabel
  else if message.type == "covenant_funded":
    handleCovenantFunded(message)  // María receives this (optional notification)
  else:
    log_warning("Unknown message type: " + message.type)

Payment Instruction Format

María → Isabel (Payment Request)

When: After María creates covenant and finds Isabel’s listing

Message payload:

{
  "version": 1,
  "type": "payment_request",
  "covenant_id": "abc123...",
  "recipient_cash_account": "Elena#142",
  "amount_eur": 100
}

Fields:

Purpose: Request Isabel’s payment details so María can pay her


Isabel → María (Payment Instruction)

When: After Isabel receives payment request and verifies covenant

Message payload:

{
  "version": 1,
  "type": "payment_instruction",
  "covenant_id": "abc123...",
  "payment_method": "bizum",
  "payment_details": {
    "phone": "+34654321098",
    "full_name": "Isabel Rodríguez García",
    "reference": "Elena#142"
  },
  "expires_at": 1735689600
}

Fields:

Purpose: Give María everything she needs to complete the Bizum payment

Size: ~200 bytes (no Nostr message limit)


Isabel → María (Covenant Funded Notification)

When: After Isabel locks BCH in covenant (OPTIONAL - Electrum is primary detection)

Message payload:

{
  "version": 1,
  "type": "covenant_funded",
  "covenant_id": "abc123...",
  "funded_at": 1735689000,
  "tx_id": "def456..."
}

Purpose: Prompt María to query Electrum immediately (faster than polling)

Not critical: María will detect funding via Electrum monitoring regardless


Error Handling

Relay Failures

try:
  relay.send(message)
catch WebSocketError:
  // Mark relay as failed
  mark_relay_failed(relay)
  
  // Try remaining relays
  if other_relays_available:
    log("Relay failed, message sent via other relays")
    return success
  else:
    // Queue for retry
    queue_message_for_retry(message)
    show_message("Offline. Message queued.")

Message Not Delivered

Problem: No delivery confirmation in Nostr (fire-and-forget)

Solution: Timeout + fallback

function sendWithRetry(recipient, message):
  sent_at = now()
  
  sendNostrDM(recipient, message)
  
  // Wait for acknowledgment (custom Asgaya convention)
  wait_for_ack(timeout=30_SECONDS)
  
  if not received_ack:
    // Retry once
    log("No ack, retrying")
    sendNostrDM(recipient, message)
    
    wait_for_ack(timeout=30_SECONDS)
    
    if not received_ack:
      // Fallback: On-chain message (OP_RETURN)
      log_warning("Nostr failed, falling back to on-chain")
      sendOnChainMessage(recipient, message)

On-chain fallback (last resort):


Decryption Failures

try:
  plaintext = nip04_decrypt(ciphertext)
catch DecryptionError:
  // Message not for me, or corrupted
  log_warning("Failed to decrypt message from " + sender)
  // Silently ignore (don't crash)

Invalid Message Format

try:
  message = json_decode(plaintext)
  validate_schema(message)
catch ParseError:
  log_warning("Invalid message format from " + sender)
  show_notification("Received malformed message (ignored)")

Platform-Specific Notes

Android

iOS

Web/Desktop


Nostr Libraries (Recommendations)

Android

iOS

Web

Recommendation: Use library for NIP-04 encryption (complex crypto). Raw WebSocket is fine for relay management.


Privacy Considerations

What’s Private

What’s Public (Relay Can See)

Metadata privacy: Relays see who talks to whom, but not what’s said

Mitigation (Phase 1+): Use Tor or VPN when connecting to relays


Testing Strategy

Unit Tests

Integration Tests

Edge Cases


Uses:

Used by:

Interacts with:


Status: Phase 0 - Implementation pending
Updated: 2026-06-25
Complexity: Low (WebSocket + library for NIP-04)
Research: See RS070 (implementation documentation strategy) —

🏠 Home ↑ Android App 📖 Glossary