nostr_access

An Elixir library for querying Nostr relays with caching and deduplication.

Features

Installation

Add nostr_access to your list of dependencies in mix.exs:

def deps do
  [
    {:nostr_access, "~> 0.3.2"}
  ]
end

Quick Start

Command Line Interface

The nostr_access package includes a command-line tool that follows the nak tool syntax:

# Build the CLI tool
mix escript.build

# Generate a filter for kind 1 events
./nostr_access --bare -k 1 -l 10

# Query multiple relays
./nostr_access -k 1 -l 15 wss://relay.example.com wss://nostr-pub.wellorder.net

# Use multiple authors and kinds
./nostr_access --bare -a pubkey1 -a pubkey2 -k 1 -k 6

# With tags and time filters
./nostr_access --bare -k 1 -t e=event123 -s 1700000000 -u 1700003600

# Stream events in real-time
./nostr_access --stream -k 1 wss://relay.example.com

# Paginate through events
./nostr_access --paginate --paginate-global-limit 100 -k 1 wss://relay.example.com

# Publish a signed Nostr event to relays, require OK from at least 2
echo '{"id":"<id>","pubkey":"<hex>","created_at":1700000000,"kind":1,"tags":[],"content":"hi","sig":"<hex>"}' \
  | ./nostr_access --publish --min-ok 2 wss://relay1.com wss://relay2.com wss://relay3.com

Programmatic API

Fetching Events (Synchronous)

# Fetch text notes from a single relay
{:ok, events} = Nostr.Client.fetch(
  ["wss://relay.example.com"],
  %{kinds: [1]}
)

# Fetch events from multiple relays
{:ok, events} = Nostr.Client.fetch(
  ["wss://relay1.com", "wss://relay2.com"],
  %{authors: ["pubkey1", "pubkey2"], kinds: [1, 6]}
)

# With custom options
{:ok, events} = Nostr.Client.fetch(
  ["wss://relay.example.com"],
  %{kinds: [1]},
  idle_ms: 1000,
  overall_timeout: 60_000,
  cache?: false
)

Publishing Events

event = %{
  "id" => "<id>",
  "pubkey" => "<hex>",
  "created_at" => 1_700_000_000,
  "kind" => 1,
  "tags" => [],
  "content" => "hello",
  "sig" => "<hex>"
}

case Nostr.Client.publish(["wss://relay1.com", "wss://relay2.com"], event, min_ok: 2) do
  {:ok, result} -> IO.inspect(result, label: "publish result")
  {:error, {:min_ok_not_met, result}} -> IO.inspect(result, label: "not enough OKs")
end

Streaming Events (Asynchronous)

# Start a streaming query
{:ok, query_ref} = Nostr.Client.stream(
  ["wss://relay1.com", "wss://relay2.com"],
  %{kinds: [1]}
)

# Receive events
receive do
  {:nostr_event, ^query_ref, event} ->
    IO.puts("Received event: #{event["id"]}")
    
  {:nostr_eose, ^query_ref, true} ->
    IO.puts("All relays finished")
    
  {:nostr_eose, ^query_ref, false} ->
    IO.puts("One relay finished")
end

# Cancel a streaming query
Nostr.Client.cancel(query_ref)

Configuration

Configure nostr_access in your config/config.exs:

config :nostr_access,
  idle_ms: 500,                    # Inactivity window (ms)
  overall_timeout: 30_000,         # Hard stop timeout (ms)
  cache?: true,                    # Enable/disable caching
  dedup_strategy: Nostr.Dedup.Default  # Deduplication strategy

Relay Health Checks

Relay health is tracked automatically to improve relay selection. Each connection records successes and failures, and relays are scored in a short time window. Unhealthy relays are put into a temporary cooldown and are skipped for new queries and reconnects.

This is on by default and requires no API changes. You can tune or disable it via the :relay_health configuration:

config :nostr_access, :relay_health,
  enabled: true,            # Turn relay health checks on/off
  ttl_hours: 24,            # How long to keep relay state in cache
  fail_window_minutes: 5,   # Failure window for scoring
  cooldown_minutes: 10,     # Cooldown length after unhealthy relays
  fail_threshold: 3,        # Failures in window to trigger cooldown
  score_floor: 40,          # Minimum score to be considered usable
  success_inc: 5,           # Score increase per success
  failure_dec: 15,          # Score decrease per failure
  max_score: 100,           # Maximum score
  min_relays: 1,            # Minimum relays to try
  max_relays: 6             # Maximum relays to query at once

CLI Reference

The nostr_access command-line tool follows the nak tool syntax for compatibility:

Usage

nostr_access [options] [relay...]

Options

Filter Attributes

Output Options

Global Options

Examples

# Generate a filter for kind 1 events
./nostr_access --bare -k 1 -l 10

# Query multiple relays
./nostr_access -k 1 -l 15 wss://relay.example.com wss://nostr-pub.wellorder.net

# Use multiple authors and kinds
./nostr_access --bare -a pubkey1 -a pubkey2 -k 1 -k 6

# With tags and time filters
./nostr_access --bare -k 1 -t e=event123 -s 1700000000 -u 1700003600

# Stream events in real-time
./nostr_access --stream -k 1 wss://relay.example.com

# Paginate through events
./nostr_access --paginate --paginate-global-limit 100 -k 1 wss://relay.example.com

API Reference

Nostr.Client.fetch/3

Fetches events from relays synchronously.

@spec fetch([relay_uri], filter, Keyword.t()) :: {:ok, [event]} | {:error, term()}

Options:

Nostr.Client.stream/3

Starts a streaming query to relays.

@spec stream([relay_uri], filter, Keyword.t()) :: {:ok, query_ref} | {:error, term()}

Messages received:

Nostr.Client.cancel/1

Cancels a streaming query.

@spec cancel(query_ref) :: :ok | {:error, :not_found}

Filter Examples

# Text notes from specific authors
%{kinds: [1], authors: ["pubkey1", "pubkey2"]}

# Recent events
%{kinds: [1], since: System.system_time(:second) - 3600}

# Events with specific tags
%{kinds: [1], "#t" => ["nostr", "elixir"]}

# Addressable events
%{kinds: [30000], authors: ["pubkey"], "#d" => ["identifier"]}

# Limited results
%{kinds: [1], limit: 100}

Architecture

The library uses a supervision tree with the following components:

nostr_access.Application (Supervisor, :rest_for_one)
├─ Registry.NostrQueries              # {query_ref, pid}
├─ Nostr.Cache.Events   (Cachex, TTL 2 h)
├─ Nostr.Cache.Miss     (Cachex, TTL 10 min)
├─ Nostr.Telemetry.Supervisor
├─ DynamicSupervisor.QuerySup         # one Nostr.Query per request
└─ DynamicSupervisor.RelayPoolSup     # one per relay URI
    └─ Nostr.RelayPool (GenServer)
        └─ 0–3 Nostr.Connection (WebSockex)   # ≤10 subs each

Deduplication Strategy

The library follows NIP-01 rules for event deduplication:

Event Class Uniqueness Key
Addressable (kind 30000-39999) {kind, pubkey, d_tag}
Lists & Metadata (kind 0, 10000-19999) {kind, pubkey}
All other kinds event.id

You can implement custom deduplication strategies by implementing the Nostr.Dedup behaviour.

Testing

Run the test suite:

mix test

Run with coverage:

MIX_ENV=test mix test --cover

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests
  5. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments