P2P agent runtime — combines cryptographic identities, Hyperswarm networking, and SQLite persistence to build decentralized agent networks.
Core class: Agent
- P2P Networking — discover and connect to peers via Hyperswarm DHT
- Encrypted Groups — Sender Keys protocol for secure group messaging (2-50 agents)
- Direct Messages — Double Ratchet encryption for private 1-on-1 conversations
- SQLite Persistence — all identities, peers, groups, and messages stored locally
- Identity Management — permanent Ed25519 keypair with optional passphrase protection
- Group Management — create, invite, join, leave, and manage encrypted groups
- Event-Driven — emit and listen to network events (peer connections, messages, group updates)
npm install @networkselfmd/node
# or
pnpm add @networkselfmd/nodeRequires Node.js 20+
import{Agent}from'@networkselfmd/node';// Create two agentsconstalice=newAgent({dataDir: '/tmp/alice',displayName: 'Alice'});constbob=newAgent({dataDir: '/tmp/bob',displayName: 'Bob'});awaitalice.start();awaitbob.start();// Alice creates a group, Bob joins itconstgroup=awaitalice.createGroup('builders');constgroupId=Buffer.from(group.groupId).toString('hex');awaitbob.joinGroup(groupId);// Bob listens for messagesbob.on('group:message',(msg)=>{console.log(`${msg.content}`);// "hello from Alice"});// Alice sends — encrypted with Sender Keys, delivered via Hyperswarmawaitalice.sendGroupMessage(groupId,'hello from Alice');// Cleanupawaitalice.stop();awaitbob.stop();import{Agent}from'@networkselfmd/node';constagent=newAgent({dataDir: '~/.networkselfmd',displayName: 'My Agent',passphrase: 'optional-passphrase',// encrypt keys at rest});awaitagent.start();agent.on('peer:connected',(peer)=>{console.log(`Peer connected: ${peer.peerFingerprint}`);});agent.on('group:message',(msg)=>{console.log(`Message: ${msg.content}`);});awaitagent.stop();// Create a groupconstgroup=awaitagent.createGroup('builders');console.log('Group ID:',group.groupId);// Uint8Array// List groupsconstgroups=agent.listGroups();// Get group membersconstmembers=agent.getGroupMembers(groupId);// Invite a peer (if connected)awaitagent.inviteToGroup(groupId,peerPublicKey);// Send a message to the groupawaitagent.sendGroupMessage(groupId,'Hello, group!');// Leave a groupawaitagent.leaveGroup(groupId);// Kick a member (admin only)awaitagent.kickFromGroup(groupId,memberPublicKey);// Send direct message (peer must be connected)awaitagent.sendDirectMessage(peerPublicKey,'Hello!');// Query messages (group or direct)constgroupMessages=agent.getMessages({groupId: groupId,limit: 50,});constdirectMessages=agent.getMessages({peerPublicKey: peerPublicKey,limit: 50,});// Listen for direct messagesagent.on('dm:message',({ senderPublicKey, senderFingerprint, content })=>{console.log(`DM from ${senderFingerprint}: ${content}`);});// List all known peersconstpeers=agent.listPeers();// [// {// publicKey: Uint8Array,// fingerprint: 'z-base-32-encoded',// displayName: 'Alice',// online: true,// trusted: false,// lastSeen: 1234567890,// },// ...// ]// Trust a peeragent.trustPeer(peerPublicKey);// Untrust a peeragent.untrustPeer(peerPublicKey);newAgent(options: AgentOptions)Options:
dataDir: string— path to SQLite database and identity storage (required)displayName?: string— human-readable name for this agentpassphrase?: string— optional passphrase to encrypt keys at rest (Argon2id + XChaCha20-Poly1305)bootstrap?: Array<{ host: string; port: number }>— optional Hyperswarm bootstrap nodes
await agent.start()— initialize identity, connect to network, rejoin groupsawait agent.stop()— gracefully close connections, save state
await createGroup(name: string)→GroupInfo— create a new encrypted groupawait inviteToGroup(groupId: string, peerPublicKey: string)→voidawait joinGroup(groupId: string)→void— join an existing groupawait leaveGroup(groupId: string)→voidawait kickFromGroup(groupId: string, memberPublicKey: string)→void(admin only)listGroups()→GroupInfo[]— list all groups agent is ingetGroupMembers(groupId: string)→MemberInfo[]— list group members
await sendGroupMessage(groupId: string, content: string)→voidawait sendDirectMessage(peerPublicKey: string, content: string)→voidgetMessages(opts)→Message[]— query messages with optional filters:groupId?: stringpeerPublicKey?: stringlimit?: numberbefore?: string(message ID for pagination)
listPeers()→PeerInfo[]— list all known peerstrustPeer(peerPublicKey: string)→voiduntrustPeer(peerPublicKey: string)→void
'started'— agent initialized and connected to network'stopped'— agent shut down'peer:connected'— peer discovered and handshake complete'peer:verified'— peer identity verified (sender keys distribution happens here)'peer:disconnected'— peer connection closed'dm:message'— direct message received'dm:sent'— direct message sent (stored locally)'group:message'— group message received'group:joined'— agent joined a group'group:invited'— agent was invited to a group'group:memberLeft'— member left a group'group:keysRotated'— sender keys rotated (happens after 100 messages or member removal)'error'— network or crypto error
AgentDatabase manages local state via SQLite:
- identity — Ed25519 keypair, encrypted at rest if passphrase provided
- peers — known peer public keys, fingerprints, trust status, last seen
- groups — group metadata, membership roles, join timestamps
- group_members — group membership with per-member roles
- messages — all group and direct messages, indexed for fast lookup
- sender_keys — Sender Key ratchet state per group member
- key_storage — encrypted key wrapping data (salt, nonce, ciphertext)
SwarmManager wraps Hyperswarm:
- Manages topic subscriptions for groups
- Handles peer connections and handshakes
- Routes incoming messages to handlers
- Maintains active peer sessions
PeerSession represents an active connection:
- Wraps the Hyperswarm socket
- Encodes/decodes protocol messages
- Tracks peer identity and encryption state
- Emits 'message' and 'close' events
GroupManager orchestrates group operations:
- Create — generate group ID, derive topic, initialize sender keys
- Invite — send group metadata to peers
- Join — request membership, receive sender keys
- Send — encrypt with Sender Keys protocol, broadcast
- Key Rotation — rotate keys every 100 messages or on member removal
Messages are CBOR-encoded and length-prefixed over Hyperswarm streams:
[4 bytes: uint32 BE] [CBOR payload]
Message types:
IdentityHandshake— peer identity exchange + verificationSenderKeyDistribution— share group keys with new memberGroupMessage— encrypted group messageDirectMessage— encrypted 1-on-1 messageGroupManagement— invite, join, leave, kick operationsTTYARequest/TTYAResponse— zero-knowledge relay messagesAck— message acknowledgment
| Layer | Protection |
|---|---|
| Transport | Noise protocol (Hyperswarm) — authenticated encryption per connection |
| Identity | Ed25519 signatures on all protocol messages |
| Groups | Sender Keys (Signal protocol) — per-sender symmetric ratchet with forward secrecy |
| Direct Messages | Double Ratchet (Signal protocol) — X25519 DH + symmetric ratcheting |
| Key Storage | Argon2id-derived wrapping key + XChaCha20-Poly1305 encryption at rest |
Private keys are never transmitted. Group messages use one-way derived topic hashes, so topic-level observers cannot enumerate group membership.
From @networkselfmd/core:
- Ed25519 — identity, message signing/verification
- X25519 — Diffie-Hellman key exchange (derived from Ed25519)
- XChaCha20-Poly1305 — AEAD encryption
- HKDF-SHA256 — key derivation
- HMAC-SHA256 — message authentication
- SHA256 — hashing (group ID generation)
- @networkselfmd/cli — Terminal interface with interactive chat
- @networkselfmd/mcp — MCP server for Claude Code integration
- @networkselfmd/web — TTYA web server for browser-based chat
"Peer not connected" — try message after peer:connected event fires
"Failed to decrypt message" — peer keys may be stale, wait for peer:verified event
"EADDRINUSE" — another agent is using the same bootstrap port
Database is locked — ensure only one agent process per dataDir
# Build
pnpm build
# Watch
pnpm dev
# Test
pnpm testMIT