nostr_access
An Elixir library for querying Nostr relays with caching and deduplication.
Features
- Multi-relay queries: Query multiple Nostr relays simultaneously
- Automatic deduplication: Follows NIP-01 rules for event deduplication
- In-memory caching: Configurable TTL for events (2h) and misses (10min)
- Connection pooling: Up to 3 connections per relay with ≤10 subscriptions each
- Dual API: Both synchronous (
fetch/3) and asynchronous (stream/3) interfaces - Automatic timeouts: Idle timeout (500ms) and overall timeout (30s) handling
Installation
Add nostr_access to your list of dependencies in mix.exs:
def deps do
[
{:nostr_access, "~> 0.3.2"}
]
endQuick 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.comProgrammatic 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")
endStreaming 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 strategyRelay 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 onceCLI Reference
The nostr_access command-line tool follows the nak tool syntax for compatibility:
Usage
nostr_access [options] [relay...]Options
Filter Attributes
--author, -a- Only accept events from these authors (pubkey as hex)--id, -i- Only accept events with these ids (hex)--kind, -k- Only accept events with these kind numbers--limit, -l- Only accept up to this number of events--search- NIP-50 search query (relay support required)--since, -s- Only accept events newer than this (unix timestamp)--tag, -t- Takes a tag like-t e=<id>, only accept events with these tags--until, -u- Only accept events older than this (unix timestamp)-d- Shortcut for--tag d=<value>-e- Shortcut for--tag e=<value>-p- Shortcut for--tag p=<value>
Output Options
--bare- Print just the filter, not enveloped in a["REQ", ...]array--ids-only- Fetch just a list of event IDs--stream- Keep subscription open, print events as they arrive--paginate- Make multiple REQs decreasing 'until' until conditions met--paginate-global-limit- Global limit for pagination--paginate-interval- Time between pagination queries (e.g., "5s", "1m")
Global Options
--help, -h- Show help--quiet, -q- Suppress logs and info messages--verbose, -v- Print more detailed information--version- Print version
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.comAPI Reference
Nostr.Client.fetch/3
Fetches events from relays synchronously.
@spec fetch([relay_uri], filter, Keyword.t()) :: {:ok, [event]} | {:error, term()}Options:
:idle_ms- Inactivity window in milliseconds (default: 500):overall_timeout- Hard stop timeout in milliseconds (default: 30_000):cache?- Enable/disable caching (default: true):dedup_strategy- Deduplication strategy module (default: Nostr.Dedup.Default)
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_event, query_ref, event}- When a new event arrives{:nostr_eose, query_ref, done?}- When a relay sends EOSE
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 eachDeduplication 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 testRun with coverage:
MIX_ENV=test mix test --coverContributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- Built for the Nostr protocol (NIP-01)
- Uses WebSockex for WebSocket connections
- Uses Cachex for in-memory caching