@mulmobridge/nostr
v0.1.1
Published
Nostr bridge for MulmoBridge — encrypted DMs over any Nostr relay
Readme
@mulmobridge/nostr
Experimental — please test and report issues.
Nostr encrypted-DM bridge for MulmoClaude. Connects to any list of Nostr relays over WebSocket, handles NIP-04 encrypted direct messages, and replies as a signed kind=4 event. Outbound-only — no public URL needed.
Setup
1. Generate a bot key
A brand-new Nostr identity is just a secret key. One-liner:
node -e "const { generateSecretKey, getPublicKey, nip19 } = require('nostr-tools'); const sk = generateSecretKey(); console.log('NOSTR_PRIVATE_KEY=' + Buffer.from(sk).toString('hex')); console.log('npub: ' + nip19.npubEncode(getPublicKey(sk)));"Or use any Nostr client (Damus / Amethyst / Iris / Primal) to register and export the secret key (nsec1…).
2. Pick relays
Public, free relays that accept everyone:
wss://relay.damus.iowss://nos.lolwss://relay.snort.socialwss://nostr.winewss://relay.nostr.band
Start with 2–3. More relays = better reach but more network traffic.
3. Run the bridge
NOSTR_PRIVATE_KEY=your-hex-or-nsec \
NOSTR_RELAYS=wss://relay.damus.io,wss://nos.lol \
npx @mulmobridge/nostrSend a Nostr DM to the bot's npub from any Nostr client — you'll get a reply.
Environment variables
| Variable | Required | Default | Description |
|--------------------------|----------|---------|-------------|
| NOSTR_PRIVATE_KEY | yes | — | 64-char hex or nsec1… bech32 bot secret key |
| NOSTR_RELAYS | yes | — | CSV of wss:// relay URLs |
| NOSTR_ALLOWED_PUBKEYS | no | (all) | CSV of hex pubkeys allowed to DM the bot (lower-case). Empty = everyone |
| NOSTR_CURSOR_FILE | no | ~/.mulmoclaude/nostr-cursor.json | Path for the persisted last-seen event timestamp. Set to an absolute path if you run multiple bots on the same machine |
| MULMOCLAUDE_AUTH_TOKEN | no | auto | MulmoClaude bearer token override |
| MULMOCLAUDE_API_URL | no | http://localhost:3001 | MulmoClaude server URL |
How it works
- Bridge derives the bot's pubkey from the secret key and opens WebSocket subscriptions to every relay in
NOSTR_RELAYS. - Filter:
kinds=[4]+#p=<botPubkey>+since=<cursor>. The cursor is thecreated_atof the last event we've committed to processing, persisted toNOSTR_CURSOR_FILEso restarts don't lose DMs delivered while the bridge was offline. On cold start (no cursor file) we fall back tonow-60sto avoid replaying ancient history. - Every 5 minutes we reopen the subscription on every relay.
nostr-tools'SimplePooldoes not auto-resume subscriptions when a relay drops the WebSocket, so without this we would silently stop receiving after the first relay hiccup. Duplicate deliveries across the reopen boundary are filtered by event-id dedup. - For each inbound event, we verify (
nostr-toolsdoes it), decrypt with NIP-04 ECDH + AES-CBC, check the sender against the allowlist, and forward the plaintext to MulmoClaude keyed bysender pubkey (hex). - Replies are encrypted back with the sender's pubkey, signed as a fresh
kind=4event, and broadcast to all relays. Any relay accepting it = successful delivery (clients will see the message).
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| No events delivered | Your relays don't replicate inbound kind=4 events | Add a high-availability relay like wss://relay.damus.io |
| Decrypt failed | Sender used NIP-44 (newer spec) or non-standard encryption | NIP-44 support is deferred to v0.2 — for now tell the user to send via a NIP-04-compatible client |
| Reply never shows up in the sender's client | All your relays rejected the event (spam filter / rate-limit) | Add more relays; most clients read from many in parallel |
Security notes
- The secret key is the bot's entire identity. Store it in a secret manager (not plain env / shell history).
- Nostr relays see the ciphertext of every DM — the plaintext is only readable by the sender and recipient. Metadata (who talks to whom, when, how much) is public.
- NIP-04 is the legacy standard. NIP-44 is newer with better cryptography but isn't universally deployed yet. This bridge does NIP-04 only in v0.1.0.
- Without
NOSTR_ALLOWED_PUBKEYS, any Nostr user who DMs the bot pubkey can converse with your MulmoClaude. Use allowlisting for personal agents. - The bot will also see its own echoes if relays replay events — we filter on
evt.pubkey === ourPubkeyso they're ignored.
Ecosystem
Part of the @mulmobridge/* package family.
Shared libraries:
@mulmobridge/client— socket.io client library used by every bridge below@mulmobridge/protocol— wire types and constants@mulmobridge/chat-service— server-side relay + session store@mulmobridge/relay— Cloudflare Workers webhook proxy@mulmobridge/mock-server— mock server for local bridge development
Bridges (one npm package per platform):
@mulmobridge/bluesky— Bluesky DMs over atproto@mulmobridge/chatwork— Chatwork (Japanese business chat)@mulmobridge/cli— interactive terminal bridge@mulmobridge/discord— Discord bot via Gateway@mulmobridge/email— IMAP poll + SMTP reply, threading preserved@mulmobridge/google-chat— Google Chat via MulmoBridge relay@mulmobridge/irc— IRC (Libera, Freenode, custom)@mulmobridge/line— LINE Messaging API via MulmoBridge relay@mulmobridge/line-works— LINE Works (enterprise LINE)@mulmobridge/mastodon— Mastodon DMs + mentions@mulmobridge/matrix— Matrix / Element@mulmobridge/mattermost— Mattermost@mulmobridge/messenger— Facebook Messenger via MulmoBridge relay@mulmobridge/nostr— Nostr NIP-04 encrypted DMs ← this package@mulmobridge/rocketchat— Rocket.Chat@mulmobridge/signal— Signal via signal-cli-rest-api@mulmobridge/slack— Slack Socket Mode@mulmobridge/teams— Microsoft Teams via Bot Framework@mulmobridge/telegram— Telegram bot@mulmobridge/twilio-sms— SMS via Twilio Programmable Messaging@mulmobridge/viber— Viber Public Account bots@mulmobridge/webhook— generic HTTP webhook bridge@mulmobridge/whatsapp— WhatsApp Cloud API via MulmoBridge relay@mulmobridge/xmpp— XMPP / Jabber@mulmobridge/zulip— Zulip
