16 KiB
Decentralized Didactyl
The Question
Can you run Didactyl on multiple servers to gain censorship resistance, geographic distribution, and high availability?
Same Keys vs. Different Keys
Same Private Key on Multiple Servers
Running two or more Didactyl instances with the same nsec means they share a Nostr identity — same pubkey, same signature authority. This breaks almost immediately.
The core problem is state conflicts on replaceable events. Didactyl's identity is built on replaceable Nostr event kinds — kind 10002 (relay list), kind 10123 (skill adoption list), kind 31120 (soul), kind 31123/31124 (skills), kind 0 (profile). Replaceable events use created_at timestamps where the latest event wins. Two instances publishing the same replaceable kind within seconds creates a race where relays disagree on which version is canonical.
Beyond relay-level conflicts, each Didactyl process maintains significant in-memory state that is never shared: DM dedup caches, message fingerprint ring buffers, trigger cooldown timers, conversation history, and the self-skill cache. Two instances with the same key both subscribe to #p = their shared pubkey. The same admin DM arrives at both. Both process it. Both call the LLM. Both reply. The admin gets two possibly contradictory responses.
Making same-key work would require a coordination layer — leader election, distributed locks, or external shared state — all of which add complexity and defeat the decentralization goal.
Different Private Keys on Multiple Servers
Each instance is a distinct agent with its own Nostr identity. No state conflicts, no replaceable-event races, no shared in-memory caches to synchronize. Each agent independently subscribes to relays, processes DMs, calls its LLM, and replies.
This is architecturally clean and aligned with how Nostr works. The question becomes: how do multiple independent agents cooperate to serve a single admin?
How We Arrived at the Broadcast-Debounce Model
The first instinct with different keys is delegation — a primary agent receives the admin's DM and farms out subtasks to specialist agents. But this adds a single point of failure (the primary), requires an agent-to-agent trust protocol, and means the admin is still talking to one agent.
A simpler model: broadcast the same message to all agents, let them all respond independently, and debounce on the admin side. The admin sends the same DM to N agent pubkeys. Each agent processes it through its own LLM, its own tools, its own context. The admin collects replies and accepts the first one (or the best one). The others are discarded.
This requires no coordination protocol between agents. No leader election. No shared state. No new Nostr event kinds. The agents don't even need to know about each other. All complexity lives at the edge — in the admin's client.
The Broadcast-Debounce Architecture
flowchart TD
ADMIN[Admin Client<br/>with fan-out + debounce]
ADMIN -->|"DM: task"| A[Agent A<br/>Server 1 — Iceland<br/>anthropic/claude-sonnet]
ADMIN -->|"DM: task"| B[Agent B<br/>Server 2 — Brazil<br/>openai/gpt-4o]
ADMIN -->|"DM: task"| C[Agent C<br/>Server 3 — Singapore<br/>local Ollama]
A -->|"DM reply"| ADMIN
B -->|"DM reply"| ADMIN
C -->|"DM reply"| ADMIN
ADMIN -->|"Accept first reply,<br/>discard rest"| RESULT[Response shown to admin]
What Each Agent Needs
Each agent runs standard Didactyl with its own config:
- Own nsec/npub — distinct Nostr identity
- Same admin pubkey — all agents recognize the same admin
- Same soul — published as kind
31120from each agent's key, or adopted from a shared author - Same adopted skills — each agent's kind
10123references the same skill events - Own LLM config — can be different providers for diversity
- Own relay list — can overlap or be completely different for geographic distribution
No code changes to Didactyl are required. Each agent is a standard, unmodified instance.
What the Admin Client Needs
The admin side needs a thin coordination layer:
- Fan-out — when the admin sends a message, the client sends it as a DM to all N agent pubkeys
- Collection — the client listens for DM replies from all agent pubkeys
- Debounce — the client tags each outgoing message with a task identifier (hash of message + nonce). The first reply that corresponds to a pending task is accepted. Subsequent replies for the same task are logged but suppressed from display.
This could be implemented as:
- A modified Nostr client
- A thin proxy between the admin and the agents
- A Didactyl agent whose sole job is fan-out and collection
Properties
| Property | Benefit |
|---|---|
| No shared state | Each agent is fully independent — no coordination protocol needed |
| No code changes | Works with current Didactyl for read-only tasks |
| Censorship resistant | Kill any N-1 servers, the last one still works |
| Provider diverse | Different LLMs on each agent — different failure modes, different strengths |
| Geographically distributed | Different jurisdictions, different relay access |
| Graceful degradation | Losing agents means fewer responses, not failure |
| Complexity at the edge | Agents stay simple and sovereign; the admin client handles coordination |
The Write-Action Problem
For read-only tasks — questions, summaries, drafts — the model works perfectly. All agents respond, admin picks one, no side effects.
For write actions — "post a note about Bitcoin" — all three agents independently publish to Nostr. Three posts appear. The admin wanted one.
Solution: two-phase draft-then-confirm. Teach all agents via their soul or an adopted skill to never execute write actions directly. Instead, they draft the action and report what they would do. The admin reviews the drafts, picks one, and sends a confirmation DM to that specific agent. The other agents' drafts are simply never confirmed.
The skill instruction is straightforward:
When asked to publish, post, react, delete, or execute any action with side effects, draft the content and present it for approval. Do not execute until the admin explicitly confirms.
This requires no code changes — only a soul or skill update. It also happens to be good practice for any agent handling important actions.
Trigger Handling
Triggered skills (cron, nostr-subscription, webhook) present the same duplication issue as write actions — all agents fire the same trigger independently.
Options:
- Disable triggers on all but one agent — simple, but creates a single point of failure for triggered tasks
- Use the draft-then-confirm pattern for triggered actions — triggers draft and report to admin rather than acting directly
- Accept duplication for idempotent triggers — reactions, replaceable event updates, and monitoring alerts are harmless when duplicated
- Shard triggers across agents — Agent A handles cron triggers, Agent B handles nostr-subscription triggers, Agent C handles webhooks
Future: Agent Awareness
In the basic model, agents don't know about each other. A future enhancement could add awareness:
- Each agent publishes a heartbeat event (kind
30078with a timestamp) - Other agents (or the admin client) monitor heartbeats to detect which agents are alive
- The admin client adjusts fan-out based on which agents are responsive
This remains fully Nostr-native — heartbeats are just events on relays — and requires no direct agent-to-agent communication.
Beyond Nostr Posts: Software Development as the Task
The broadcast-debounce model above assumes the agents' primary job is Nostr social activity — posting notes, reacting, querying relays. But what if the task is software development and infrastructure management? For example: creating and maintaining a website, deployed across servers in different jurisdictions.
This changes the model fundamentally. The agents are no longer just talking — they are building things on their local machines.
What Didactyl Already Has
Each agent already has local system tools:
| Tool | Capability |
|---|---|
local_shell_exec |
Execute any shell command — git, npm, docker, systemctl, curl |
local_file_read |
Read source files from the working directory |
local_file_write |
Write/create source files in the working directory |
local_http_fetch |
Make HTTP requests — test endpoints, call APIs, download resources |
An agent with these tools can already: clone a repo, edit source code, run a build, start a dev server, deploy to production, check if a site is up, read logs, and fix issues. The LLM reasons about what to do; the tools are the hands.
The Shift: From Redundancy to Distribution
With Nostr posting, multiple agents doing the same task is redundancy — you want one result and discard the rest. With software development, multiple agents doing different parts of the same project is distribution — you want all of them to contribute.
flowchart TD
subgraph "Nostr Posting Model"
direction LR
N_ADMIN[Admin] -->|"Same task"| N_A[Agent A]
N_ADMIN -->|"Same task"| N_B[Agent B]
N_ADMIN -->|"Same task"| N_C[Agent C]
N_A -->|"Response 1"| N_DEDUP[Debounce:<br/>pick one]
N_B -->|"Response 2"| N_DEDUP
N_C -->|"Response 3"| N_DEDUP
end
subgraph "Software Dev Model"
direction LR
S_ADMIN[Admin] -->|"Build the site"| S_A[Agent A:<br/>Frontend]
S_ADMIN -->|"Build the site"| S_B[Agent B:<br/>Backend API]
S_ADMIN -->|"Build the site"| S_C[Agent C:<br/>Infrastructure]
S_A -->|"React app ready"| S_MERGE[Integration:<br/>combine all parts]
S_B -->|"API deployed"| S_MERGE
S_C -->|"Nginx + TLS configured"| S_MERGE
end
Architecture: Agents as Jurisdiction-Local Builders
Consider a website that needs to exist in multiple jurisdictions for censorship resistance. Each agent lives on a server in a different country and maintains a local copy of the site.
flowchart TD
ADMIN[Admin<br/>npub1admin...]
GIT[(Git Repo<br/>shared source of truth)]
subgraph "Iceland"
A[Agent A<br/>npub1aaa...]
A_FS[Local filesystem:<br/>/var/www/mysite]
A_SRV[Nginx serving<br/>mysite.is]
A --> A_FS --> A_SRV
end
subgraph "Brazil"
B[Agent B<br/>npub1bbb...]
B_FS[Local filesystem:<br/>/var/www/mysite]
B_SRV[Nginx serving<br/>mysite.br]
B --> B_FS --> B_SRV
end
subgraph "Singapore"
C[Agent C<br/>npub1ccc...]
C_FS[Local filesystem:<br/>/var/www/mysite]
C_SRV[Nginx serving<br/>mysite.sg]
C --> C_FS --> C_SRV
end
ADMIN -->|"DM: update the homepage"| A
ADMIN -->|"DM: update the homepage"| B
ADMIN -->|"DM: update the homepage"| C
A <-->|"git pull / push"| GIT
B <-->|"git pull / push"| GIT
C <-->|"git pull / push"| GIT
Git as the Coordination Layer
In the Nostr posting model, the write-action problem was solved with draft-then-confirm. For software development, the natural coordination layer is git.
- All agents share a git repository (hosted on Gitea, GitHub, a Nostr-based git relay, or even a bare repo on one of the servers)
- Each agent works on its local clone
- Before making changes, the agent pulls latest
- After making changes, the agent commits and pushes
- Conflicts are resolved by the LLM (or flagged to the admin)
Git gives you:
- Atomic changes — commits are all-or-nothing
- Conflict detection — if two agents edit the same file, git tells you
- History — every change is traceable to which agent made it
- Rollback — bad changes can be reverted
The agent already has local_shell_exec which can run git pull, git add, git commit, git push. No new tools needed.
Three Operational Modes
Depending on the task, the admin can use different patterns:
Mode 1: Broadcast-Identical (Same Task, All Servers)
"Pull latest and rebuild the site"
All agents do the same thing on their local server. This is the deployment/ops pattern — you want the same action everywhere. No debounce needed; all agents should execute.
Mode 2: Broadcast-Debounce (Same Task, One Result)
"Write a new About page"
All agents draft a version. Admin picks the best one. That agent commits and pushes. The other agents then pull the update in the next sync cycle. This is the creative/development pattern.
Mode 3: Directed (Specific Task, Specific Agent)
To Agent A only: "Debug why the Iceland server returns 502"
The admin sends a DM to one specific agent because the task is server-specific. This is the ops/debugging pattern.
The Sync Cycle
For the site to stay consistent across jurisdictions, agents need a periodic sync. This can be a cron-triggered skill:
You are a deployment sync agent. Every 15 minutes:
1. Run `git pull` in the project directory
2. If there are new changes, run the build command
3. Restart the web server if the build succeeded
4. DM the admin a brief status: what changed, build result, server status
5. If the pull fails due to conflicts, DM the admin with the conflict details and do not build
This skill already works with Didactyl's existing cron trigger type and local_shell_exec tool. Each agent runs it independently on its own server.
What Changes vs. the Nostr Model
| Aspect | Nostr Posting | Software Development |
|---|---|---|
| Primary tools | nostr_post, nostr_query |
local_shell_exec, local_file_write, local_file_read |
| Coordination | Debounce on admin side | Git repo as shared state |
| Write conflicts | Replaceable events (last-write-wins) | Git merge conflicts (explicit resolution) |
| Broadcast meaning | Redundancy — pick one response | Deployment — all servers execute |
| Agent specialization | All agents are identical | Agents can have server-specific skills |
| State | Nostr relays (stateless agents) | Local filesystem (stateful agents) |
| Failure mode | Agent down = fewer responses | Agent down = one jurisdiction offline |
The Stateful Agent Problem
This is the fundamental shift. Nostr-posting agents are essentially stateless — their identity and skills live on relays, and destroying the host doesn't kill the agent. Software development agents are deeply stateful — they have local files, running services, build artifacts, and server configurations that don't exist on Nostr.
Git mitigates this: if an agent's server dies, you spin up a new server, install Didactyl with the same agent keys, and git clone the repo. The agent recovers its working state from git. But the server-specific state (Nginx config, TLS certs, DNS, running processes) needs to be reconstructable too.
This suggests a infrastructure-as-code discipline where everything about the server's configuration is in the git repo:
- Nginx configs
- Docker compose files
- TLS certificate automation (Let's Encrypt)
- Deployment scripts
The agent's first-boot skill becomes: "Clone the repo, run the setup script, verify the site is serving."
Jurisdiction-Specific Considerations
Each agent can have server-specific skills that account for local differences:
| Agent | Jurisdiction | Specific Skills |
|---|---|---|
| Agent A | Iceland | Icelandic privacy law compliance, .is domain management |
| Agent B | Brazil | LGPD compliance checks, .br domain management |
| Agent C | Singapore | PDPA compliance, .sg domain management, Asia-Pacific CDN config |
These are adopted as private skills (kind 31124) specific to each agent, while the core development skills (kind 31123) are shared across all agents.
Summary
The broadcast-debounce model extends naturally to software development, but the coordination mechanism shifts from admin-side debounce to git as shared state. The three operational modes — broadcast-identical for deployment, broadcast-debounce for development, and directed for debugging — cover the full range of tasks. Didactyl's existing tools (local_shell_exec, local_file_read, local_file_write) and trigger types (cron for sync cycles) already support this without code changes. The main new requirement is disciplined infrastructure-as-code so that agent state is recoverable from git.