Didactyl — Nostr Subscriptions

Nostr subscription management, runtime filters, and context subscriptions for Didactyl agents.

Didactyl — Nostr Subscriptions

Overview

Didactyl maintains persistent websocket subscriptions to Nostr relays for the lifetime of the process. Subscriptions are opened during startup and are never closed — the relay pool keeps them alive, automatically reconnecting and resubscribing when relays drop.

All subscriptions are created via nostr_relay_pool_subscribe() from nostr_core_lib and are sent to every relay in the configured relay list.

Startup Sequence

The subscriptions are opened in a specific order during main() startup. The diagram below shows the full sequence:

flowchart TD
    A[nostr_handler_init] --> B[Connect to all relays]
    B --> C[nostr_handler_reconcile_startup_events]
    C --> D[Publish startup events to relays]
    D --> E[trigger_manager_init]
    E --> F[trigger_manager_load_from_startup_events]
    F --> G["Subscribe: Admin Context"]
    G --> H["Subscribe: Self Skills"]
    H --> I[Send startup DM to admin]
    I --> J["Subscribe: DMs"]
    J --> K[Enter main poll loop]
    K --> L["Poll: nostr_handler_poll + trigger_manager_poll + http_api_poll"]

    style F fill:#2a7,stroke:#333,color:#fff
    style G fill:#27a,stroke:#333,color:#fff
    style H fill:#27a,stroke:#333,color:#fff
    style J fill:#27a,stroke:#333,color:#fff

Subscription Categories

1. Admin Context Subscription

Function: nostr_handler_subscribe_admin_context() in src/nostr_handler.c When: During startup, before self-skill subscription Condition: Only if admin_context.enabled is true in config

This creates up to two persistent subscriptions for the admin’s pubkey:

Profile Subscription

Field Value
Kinds 0 (profile), 3 (contacts), 10002 (relay list) — each configurable
Authors Admin pubkey
Limit 32
Callback on_admin_context_event
Dedup Enabled
Close on EOSE No

Tracks the admin’s profile metadata, contact list (WoT), and relay preferences. Used to build agent context about who the admin is.

Kind 1 Notes Subscription

Field Value
Kinds 1
Authors Admin pubkey
Limit Configurable via kind_1_limit, default 10, max 256
Callback on_admin_context_event
Dedup Enabled
Close on EOSE No
Condition Only if admin_context.track_kind_1 is true

Tracks the admin’s recent public notes. Used for agent context and as the event source for triggered skills that watch admin posts.

2. Self-Skill Subscription

Function: nostr_handler_subscribe_self_skills() in src/nostr_handler.c When: During startup, after admin context subscription

Field Value
Kinds 31123 (public skill), 31124 (private skill), 10123 (adoption list)
Authors Agent’s own pubkey
Limit 300
Callback on_self_skill_event
EOSE Callback on_self_skill_eose
Dedup Disabled (handles dedup internally via cache upsert)
Close on EOSE No

This is the core skill awareness subscription. It serves three purposes:

  1. Cache population — Every arriving event is stored in the in-memory self-skill cache via self_skill_cache_upsert_event_locked(), making skills available for LLM tool calls and context building.

  2. Live trigger registration — When a kind 31123 or 31124 event arrives with trigger=nostr-subscription and a valid filter tag, register_trigger_from_self_skill_event() immediately calls trigger_manager_add() to create a persistent trigger subscription. This means skills published from any client are automatically activated without restart.

  3. Deferred bulk load — After EOSE, the on_self_skill_eose callback fires trigger_manager_load_from_skills() as a one-time bulk scan of the adoption list. This catches any skills that were already cached before the per-event path was wired up.

3. DM Subscriptions

Function: nostr_handler_subscribe_dms() in src/nostr_handler.c When: During startup, after self-skill subscription and startup DM Required: Yes — startup fails if DM subscription cannot be created

Creates one or two subscriptions depending on the configured DM protocol:

NIP-04 DM Subscription

Field Value
Kinds 4
#p Agent’s own pubkey
Since Process start time
Limit 100
Callback on_event (routes to agent_on_message)
Dedup Disabled (handled by dm_id_seen_or_remember)
Close on EOSE No
Condition dm_protocol is nip04 or both

NIP-17 DM Subscription

Field Value
Kinds 1059 (gift wrap)
#p Agent’s own pubkey
Since Process start time
Limit 400
Callback on_event (unwraps gift wrap, routes to agent_on_message)
Dedup Disabled (handled by dm_id_seen_or_remember)
Close on EOSE No
Condition dm_protocol is nip17 or both

4. Trigger Subscriptions

Function: register_trigger_subscription_locked() in src/trigger_manager.c When: Dynamically, whenever a trigger is registered via trigger_manager_add() Created by: nostr_handler_subscribe_with_filter() wrapper

Each active trigger gets its own persistent subscription based on the skill’s filter tag:

Field Value
Filter Parsed from the skill’s filter tag JSON
Since From filter, or defaults to now - 30s
Limit From filter, or defaults to 200
Callback on_trigger_subscription_event
Dedup Enabled
Close on EOSE No

When an event matches the filter, maybe_fire_trigger_locked() checks cooldown and dedup, then executes the trigger action (LLM or template).

Trigger subscriptions are created at three points:

  • Startup config scantrigger_manager_load_from_startup_events() parses startup_events[] from config for skills with trigger tags
  • Live self-skill eventregister_trigger_from_self_skill_event() in the self-skill subscription callback
  • EOSE bulk loadtrigger_manager_load_from_skills() after self-skill EOSE
  • Runtime tool callskill_create tool with trigger parameters

Subscription Parameters

All subscriptions share these common pool parameters:

Parameter Value Meaning
close_on_eose 0 Subscription stays open after initial EOSE
result_mode NOSTR_POOL_EOSE_FULL_SET EOSE fires after all relays respond or timeout
relay_timeout_seconds 30 Per-relay timeout for initial response
eose_timeout_seconds 120 Overall EOSE timeout across all relays

Subscription Lifecycle

flowchart LR
    INIT["Process Start"] --> CONNECT["Connect Relays"]
    CONNECT --> SUB["Open Subscriptions"]
    SUB --> LIVE["Live Event Stream"]
    LIVE --> |"Relay disconnects"| RECON["Auto-Reconnect"]
    RECON --> |"Relay reconnects"| RESUB["Auto-Resubscribe"]
    RESUB --> LIVE
    LIVE --> |"SIGINT/SIGTERM"| SHUT["Shutdown"]
    SHUT --> CLOSE["Close All + Cleanup"]

Subscriptions are never manually closed during normal operation. The relay pool handles reconnection and resubscription transparently. On shutdown, trigger_manager_cleanup() closes trigger subscriptions and nostr_handler_cleanup() destroys the pool.

Summary Table

Subscription Kinds Target Persistent Created At
Admin Profile 0, 3, 10002 Admin pubkey Yes Startup
Admin Notes 1 Admin pubkey Yes Startup
Self Skills 31123, 31124, 10123 Own pubkey Yes Startup
DMs NIP-04 4 Own pubkey (#p) Yes Startup
DMs NIP-17 1059 Own pubkey (#p) Yes Startup
Trigger N Per skill filter Varies Yes Dynamic

Related Documentation

  • Skills — Skill event format and trigger tags
  • Toolsskill_create tool with trigger parameters
  • APItrigger_list and trigger_status endpoints
Write a comment
No comments yet.