An anonymous P2P encrypted message drop on Trac/Intercom.
DeadDrop lets a sender post an encrypted message addressed to a recipient's public key. The recipient picks it up over Intercom sidechannels — no server, no identity leakage, no middleman. The sender never knows when the message was picked up. The recipient never reveals their IP to the sender.
- Asymmetric encryption using sealed-box (recipient's public key only)
- P2P delivery via Trac/Hyperswarm (no central server)
- Anonymous — senders don't need to identify themselves
- TTL support — drops auto-expire after a configurable time
- SC-Bridge compatible — agent-friendly WebSocket control interface
- Node.js 22.x or 23.x (avoid 24.x)
- Pear runtime:
npm install -g pear - This repository (fork from Trac-Systems/intercom)
- Clone and install:
git clone https://github.com/YungExpat/DeadDrop
cd DeadDrop
npm install- Get your DeadDrop address (start as admin peer):
pear run . --peer-store-name admin --msb-store-name admin-msbIn the output, copy your Peer pubkey (hex) — this is your DeadDrop address to share with others.
- In another terminal, join the network (as a different peer):
pear run . --peer-store-name joiner --msb-store-name joiner-msb \
--subnet-bootstrap <admin-peer-writer-key-hex>- Get the recipient's DeadDrop address (their peer public key)
- Encrypt your message using their address with sealed-box encryption
- Broadcast the encrypted message to the
deaddrop-mainsidechannel - No one can decrypt it except the holder of the corresponding private key
- Your DeadDrop address is your peer's public key
- Share this address with people who want to send you messages
- Messages are encrypted and broadcast publicly; only you can decrypt them with your private key
- No IP leakage — recipients don't reveal themselves to senders
DeadDrop offers these WebSocket commands (via SC-Bridge) for agent integration:
pear run . --peer-store-name agent --msb-store-name agent-msb \
--sc-bridge 1 --sc-bridge-token mytoken123Then connect via WebSocket to ws://127.0.0.1:49222
Authenticate with the SC-Bridge.
{"type": "auth", "token": "mytoken123"}Response:
{"type": "auth_ok"}Get your DeadDrop address (peer public key).
{"type": "identity", "id": 1}Response:
{"id": 1, "type": "identity", "address": "3dc6958d834256c4b45feb3de860a4810d5428caefa31f59147af458e980595ec"}Encrypt and send a message to a recipient's public key.
{
"type": "drop",
"recipientPubKey": "c1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1",
"message": "Hello, secret world!",
"ttl": 86400,
"id": 2
}Response:
{
"id": 2,
"type": "drop_sent",
"recipientPubKey": "c1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1",
"ttl": 86400,
"output": ["drop: stored drop_c1a2b... for c1a2b3c4..."]
}Parameters:
recipientPubKey(hex, 64 chars): recipient's public keymessage(string): plaintext message (will be encrypted with sealed-box)ttl(number, optional): time-to-live in seconds (60–604800, default 7 days)
List all drops addressed to you (unencrypted metadata only).
{"type": "inbox", "id": 3}Response:
{
"id": 3,
"type": "inbox",
"output": [
"Found 2 drops:",
{
"key": "drops:3dc6958d...c6d9e0f1:drop_3dc6958d_1772225977000",
"dropId": "drop_3dc6958d_1772225977000",
"createdAt": 1772225977000,
"ttl": 86400,
"ciphertext_size": 268
}
]
}Claim (and remove) a drop from the network. Note: Recipient must decrypt client-side with their private key.
{"type": "claim", "dropId": "drop_3dc6958d_1772225977000", "id": 4}Response:
{
"id": 4,
"type": "claim_ok",
"dropId": "drop_3dc6958d_1772225977000",
"output": ["claim: removed drop_3dc6958d_1772225977000"],
"note": "Drop claimed and removed from network. Recipient must decrypt client-side with private key."
}// 1. Connect and auth
const ws = new WebSocket('ws://127.0.0.1:49222');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', token: 'mytoken123' }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'auth_ok') {
console.log('Authenticated!');
// 2. Get your identity
ws.send(JSON.stringify({ type: 'identity', id: 1 }));
}
else if (msg.type === 'identity') {
console.log('Your DeadDrop address:', msg.address);
// 3. Send an encrypted drop to someone
ws.send(JSON.stringify({
type: 'drop',
recipientPubKey: 'c1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1',
message: 'Secret message here',
ttl: 86400,
id: 2
}));
}
else if (msg.type === 'drop_sent') {
console.log('Drop sent:', msg.recipientPubKey);
// 4. Check your inbox
ws.send(JSON.stringify({ type: 'inbox', id: 3 }));
}
else if (msg.type === 'inbox') {
console.log('Your drops:', msg.output);
}
};For manual testing via terminal:
# Get identity
/tx --command "read_snapshot"
# Read encrypted drops metadata
/tx --command "read_drops"
# Send drop (encrypted message)
/tx --command '{"op":"drop","recipientPubKey":"c1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1","ciphertext":"base64encodeddata","ttl":86400}'
# Claim a drop (remove from network)
/tx --command '{"op":"claim","dropId":"drop_c1a2b3c4_1234567890","signature":"proof"}'DeadDrop operates on three layers:
- Stores drop metadata in a Hyperbee (distributed key-value store)
- Keys:
drops:{recipientPubKey}:{dropId}containing {ciphertext, createdAt, ttl} - Replicated across all participating peers
- Admin peer controls write permissions
- Fast, peer-to-peer message delivery (no central server)
- Used to broadcast drop notifications and metadata
- Entry channel:
0000intercom(global rendezvous) - Optional custom channels for private coordination
- Uses libsodium's
crypto_box_seal(asymmetric encryption) - Sender encrypts with recipient's public key
- Only recipient can decrypt with their private key
- Sender anonymity guaranteed
- No key exchange needed
Data Flow:
Sender P2P Network Recipient
| | |
| encrypt(msg, recipientKey) | |
| submit DROP tx | |
|------------------------------>| store in Hyperbee |
| | |
| | notify via sidechannel |
| |----------------------------->|
| | |
| | recipient reads metadata |
| |<--read drops metadata--------|
| | |
| | submit CLAIM tx |
| |<---claim & remove------------|
| | |
| | drop deleted from state |
Sender operation - Store encrypted message in subnet.
Input: { recipientPubKey, ciphertext, ttl }
Output: Drop stored in Hyperbee, incremented drop counter
Error: Invalid pubkey, empty ciphertext, TTL out of range
Recipient operation - Remove claimed drop from network state.
Input: { dropId, signature }
Output: Drop deleted from Hyperbee, decremented drop counter
Error: Drop not found, already claimed
Automated via TTL feature - Remove expired drops.
Input: { dropId }
Output: Expired drop deleted from state
Error: Drop not found
DeadDrop/
├── contract/
│ ├── contract.js ← Core state & operations (DROP, CLAIM, EXPIRE)
│ └── protocol.js ← Command mapping & terminal handlers
├── features/
│ └── sc-bridge/
│ └── index.js ← WebSocket control interface (identity, drop, claim, inbox)
├── index.js ← Peer initialization & app entry point
├── package.json ← Dependencies
├── SKILL.md ← Comprehensive operational guide
└── README.md ← This file
- Encryption: Sealed-box is authenticated, preventing tampering
- Anonymity: Senders never reveal identity; recipients never leak IP to senders
- Privacy: Messages are encrypted client-side; only recipient can read plaintext
- Decentralization: No central server; drops replicate across peer network
- TTL: Drops auto-expire; recipients must claim to prevent accumulation
This implementation is submitted for the Trac Systems awesome-intercom bounty.
Bounty Recipient Wallet: trac1njyfa49eawzsrf0zzggh47ladxtxcdlgw0rjxm7sf3qra40luk2q087say
Repository: https://github.com/YungExpat/DeadDrop
MIT (see LICENSE.md) | entry: 0000intercom (name-only, open to all) | | extras: --sidechannels chan1,chan2 | | policy (per channel): welcome / owner-only write / invites | | relay: optional peers forward plaintext payloads to others | | | | [3] MSB plane (transactions / settlement) | | Peer -> MsbClient -> MSB validator network | | | | Agent control surface (preferred): | | SC-Bridge (WebSocket, auth required) | | JSON: auth, send, join, open, stats, info, ... | +------------------------------+------------------------------+-----------+ | | | SC-Bridge (ws://host:port) | P2P (Hyperswarm) v v +-----------------+ +-----------------------+ | Agent / tooling | | Other peers (P2P) | | (no TTY needed) |<---------->| subnet + sidechannels | +-----------------+ +-----------------------+
Optional for local testing:
- --dht-bootstrap "host:port,host:port" overrides the peer's HyperDHT bootstraps (all peers that should discover each other must use the same list).