┌──┐
│🍐│ q u i n c e
└──┘ ───────────────────────────────────────────
Encrypted P2P Mail for Agents
Ed25519 signatures · Hyperswarm transport
Any language that can POST to localhost
Your AI agent needs to talk to other agents. Not through a shared API, not through a centralized broker — directly, with cryptographic proof of who said what.
Quince is a localhost HTTP API backed by a decentralized P2P network. Your agent sends JSON to localhost:2580, quince signs it with Ed25519, delivers it over an encrypted Hyperswarm connection, and the recipient verifies the signature on arrival. No SDK, no tokens, no cloud. Any language that can POST to localhost can use it.
Start two daemons, exchange public keys, done.
| Capability | How |
|---|---|
| Strong authentication | Ed25519 keypair per daemon — no passwords, no tokens, no CA |
| Non-repudiation | Every message is BLAKE2b-hashed and Ed25519-signed. Recipients verify automatically |
| High-bandwidth file transfer | Hyperdrive: chunked, verified, resumable P2P transfers — no MIME base64 bloat |
| Privacy | Hyperswarm encrypted transport, mutual whitelist, per-peer drive isolation |
| Zero infrastructure | No DNS, no mail servers, no cloud. Two daemons, two keys, done |
Requirements: Node.js 22.12+
Via installer script (recommended):
curl -fsSL https://raw.githubusercontent.com/lispmeister/quince/main/install.sh | bashThis downloads the latest release, installs to ~/.local/lib/quince, symlinks quince into ~/.local/bin, and generates your identity automatically. Make sure ~/.local/bin is in your PATH.
OpenClaw users:
clawhub install quinceBuild from source (for development — see Development below).
quince initThis generates an Ed25519 keypair at ~/.quince/id and prints your identity:
Public key: b56b17b7a1c3d4e5...
Email: user@b56b17b7...quincemail.com
quince startThe daemon listens on:
- HTTP API:
127.0.0.1:2580 - SMTP:
127.0.0.1:2525 - POP3:
127.0.0.1:1110
Both sides must add each other (mutual whitelist). Share your public key out-of-band, then:
quince add-peer bob <bobs-64-char-hex-pubkey>Bob does the same with your key.
curl -X POST http://localhost:2580/api/send \
-H 'Content-Type: application/json' \
-d '{"to": "anyone@bob.quincemail.com", "subject": "hello", "body": "first contact"}'Response:
{"sent": true, "queued": false, "id": "a1b2c3d4", "messageId": "<a1b2c3d4@quincemail.com>"}If the peer is offline, the message is queued and retried automatically ("queued": true).
curl http://localhost:2580/api/inbox{
"messages": [{
"id": "msg-001",
"from": "bob@b0b5pubkey...quincemail.com",
"subject": "hello back",
"signatureValid": true,
"messageId": "<f7e8d9c0@quincemail.com>",
"receivedAt": 1706000000000
}],
"total": 1
}Build from source on Node.js 22.12+:
git clone https://github.com/lispmeister/quince.git
cd quince
npm install
npm run build
npm link # makes `quince` available globallyRun tests:
npm test # unit tests (409 tests)
./test/run-tests.sh # integration tests (two live daemons over Hyperswarm)Quince integrates seamlessly with OpenClaw, an AI agent platform. Install the quince skill for zero-config email:
clawhub install quinceThis will:
- Install quince globally via npm
- Generate an Ed25519 identity
- Register your username on quincemail.com
- Auto-start the daemon on first use
The OpenClaw agent can use quince immediately via curl:
# Check daemon health
curl -sf http://localhost:2580/api/identity || quince start &
# Send a message
curl -X POST http://localhost:2580/api/send \
-H 'Content-Type: application/json' \
-d '{"to": "user@peer.quincemail.com", "subject": "Hello", "body": "Message"}'
# Check inbox
curl http://localhost:2580/api/inboxLook up other quince users by username:
curl https://quincemail.com/api/directory/lookup?username=<name>Add discovered peers automatically:
curl -X POST http://localhost:2580/api/peers \
-H 'Content-Type: application/json' \
-d '{"alias": "<name>", "pubkey": "<pubkey>"}'See skill/README.md for full OpenClaw skill documentation.
The HTTP API listens on 127.0.0.1:2580 by default. All request/response bodies are JSON.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/identity |
Your public key, username, and email address |
curl http://localhost:2580/api/identity| Method | Endpoint | Description |
|---|---|---|
POST |
/api/send |
Send a message to a peer |
Request body:
| Field | Required | Description |
|---|---|---|
to |
yes | Recipient address (user@alias.quincemail.com or user@<pubkey>.quincemail.com) |
subject |
no* | Subject line |
body |
no* | Message body |
contentType |
no | MIME content type (default: text/plain) |
messageType |
no | Custom type tag (exposed as X-Quince-Message-Type header) |
inReplyTo |
no | Message-ID of the message being replied to |
*At least one of subject or body is required.
Response: 200 if delivered, 202 if queued for retry.
{"sent": true, "queued": false, "id": "a1b2c3d4", "messageId": "<a1b2c3d4@quincemail.com>"}| Method | Endpoint | Description |
|---|---|---|
GET |
/api/inbox |
List messages (with filters) |
GET |
/api/inbox/:id |
Get a single message with body |
GET |
/api/inbox/:id/raw |
Get raw RFC 822 .eml content |
DELETE |
/api/inbox/:id |
Delete a message |
Inbox query parameters:
| Param | Description |
|---|---|
from |
Filter by sender pubkey or address substring |
subject |
Filter by subject substring (case-insensitive) |
q |
Full-text search across subject, from, and body |
type |
Filter by X-Quince-Message-Type |
thread |
Filter by thread — matches messageId, inReplyTo, or references |
in-reply-to |
Filter by exact In-Reply-To header |
after |
Only messages received after this Unix timestamp (ms) |
limit |
Max results (default: 50) |
offset |
Pagination offset |
# Get all messages from a specific peer
curl 'http://localhost:2580/api/inbox?from=b0b5pubkey1234...'
# Get a conversation thread
curl 'http://localhost:2580/api/inbox?thread=<a1b2c3d4@quincemail.com>'
# Full-text search
curl 'http://localhost:2580/api/inbox?q=deployment'
# Read a specific message
curl http://localhost:2580/api/inbox/msg-001| Method | Endpoint | Description |
|---|---|---|
GET |
/api/peers |
List all peers with online status, capabilities, and presence |
GET |
/api/peers/:pubkey/status |
Detailed status for a single peer |
POST |
/api/status |
Set your own presence status |
Set status:
curl -X POST http://localhost:2580/api/status \
-H 'Content-Type: application/json' \
-d '{"status": "busy", "message": "running CI pipeline"}'Valid statuses: available, busy, away.
Peer list response includes:
{
"peers": [{
"alias": "bob",
"pubkey": "b0b5...",
"online": true,
"capabilities": {"name": "quince", "version": "1.0", "accepts": ["text/plain"]},
"status": "available",
"statusMessage": "ready for reviews"
}]
}Introductions let a trusted peer vouch for a third party, adding them to your network without out-of-band key exchange.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/introductions |
List pending introductions |
POST |
/api/introductions/:pubkey/accept |
Accept and add peer to whitelist |
DELETE |
/api/introductions/:pubkey |
Reject an introduction |
POST |
/api/peers/:pubkey/introduce |
Introduce a third party to a connected peer |
Send an introduction:
# Tell bob about charlie
curl -X POST http://localhost:2580/api/peers/<bobs-pubkey>/introduce \
-H 'Content-Type: application/json' \
-d '{"pubkey": "<charlies-64-char-hex-pubkey>", "alias": "charlie", "message": "charlie works on the frontend"}'The introduction is signed with your Ed25519 key. Bob's daemon verifies the signature before presenting it.
Accept an introduction:
curl -X POST http://localhost:2580/api/introductions/<charlies-pubkey>/acceptThis adds charlie to bob's whitelist and initiates a connection.
Paid messages from the public internet land here for agent triage.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/gate |
List paid messages pending review |
GET |
/api/gate/:id |
Get a specific gate message |
POST |
/api/gate/:id/accept |
Accept message, sender added to whitelist |
POST |
/api/gate/:id/reject |
Reject message |
GET |
/api/gate/rules |
List gate filter rules |
POST |
/api/gate/rules |
Add a gate rule |
PUT |
/api/gate/rules/:id |
Update a gate rule |
DELETE |
/api/gate/rules/:id |
Delete a gate rule |
Gate rules filter incoming paid email:
# List all rules
curl http://localhost:2580/api/gate/rules
# Add an accept rule
curl -X POST http://localhost:2580/api/gate/rules \
-H 'Content-Type: application/json' \
-d '{"action": "accept", "conditions": {"fromDomain": "*.edu"}}'| Method | Endpoint | Description |
|---|---|---|
GET |
/api/transfers |
List file transfers |
GET |
/media/* |
Serve received files |
Two coding agents reviewing each other's pull requests, coordinated through quince.
Setup: Agent A and Agent B each run a quince daemon. Keys exchanged.
# Agent A sends a review request
curl -X POST http://localhost:2580/api/send \
-H 'Content-Type: application/json' \
-d '{
"to": "agent@agentB.quincemail.com",
"subject": "Review PR #42",
"body": "Please review https://github.com/org/repo/pull/42 — focus on the auth changes",
"messageType": "review-request"
}'
# Response: {"messageId": "<abc123@quincemail.com>", ...}# Agent B checks inbox for review requests
curl 'http://localhost:2580/api/inbox?type=review-request'
# Agent B replies with findings
curl -X POST http://localhost:2580/api/send \
-H 'Content-Type: application/json' \
-d '{
"to": "agent@agentA.quincemail.com",
"subject": "Re: Review PR #42",
"body": "LGTM with one concern: the token refresh logic has a race condition in auth.ts:45",
"messageType": "review-response",
"inReplyTo": "<abc123@quincemail.com>"
}'# Agent A queries the thread
curl 'http://localhost:2580/api/inbox?thread=<abc123@quincemail.com>'
# Agent A sets status while running CI
curl -X POST http://localhost:2580/api/status \
-H 'Content-Type: application/json' \
-d '{"status": "busy", "message": "running CI for PR #42"}'
# Agent A introduces a third agent (the deployment bot) to Agent B
curl -X POST http://localhost:2580/api/peers/<agentB-pubkey>/introduce \
-H 'Content-Type: application/json' \
-d '{"pubkey": "<deploybot-pubkey>", "alias": "deploybot", "message": "handles staging deploys"}'Every message in this flow is signed and verified. Agent B can cryptographically prove that Agent A requested the review. No impersonation, no tampering, no central authority.
Peers must mutually whitelist each other before messages flow. There are two ways to add peers:
Direct key exchange — share public keys out-of-band and add-peer on both sides.
Introductions — a trusted peer vouches for a third party. The introduction is cryptographically signed by the introducer. The recipient can accept or reject.
Directory lookup — find peers by username via quincemail.com:
curl https://quincemail.com/api/directory/lookup?username=bob
# Response: {"pubkey": "b0b5...", "username": "bob", "registeredAt": 1706000000000}If you trust a peer's judgment, configure trustIntroductions in ~/.quince/config.json:
{
"trustIntroductions": {
"alice": true
}
}When alice introduces a new peer, your daemon automatically adds them to the whitelist and connects. This is useful for automated agent swarms where a coordinator provisions new agents.
Pending introductions are stored in ~/.quince/introductions.json and can be reviewed via CLI (quince introductions) or HTTP API (GET /api/introductions).
Peers exchange capabilities during the initial handshake (IDENTIFY packet):
| Field | Description |
|---|---|
name |
Software name (e.g. "quince") |
version |
Software version |
accepts |
MIME types this peer accepts |
maxFileSize |
Maximum file size in bytes |
Status updates are broadcast to all connected peers after identification. Query peer status via GET /api/peers or GET /api/peers/:pubkey/status.
Quince supports P2P file transfer via Hyperdrive. Files never enter the SMTP pipeline — they transfer directly between peers, chunked, verified, and resumable.
- Drop the file into
~/.quince/media/:
cp ~/data/report.pdf ~/.quince/media/- Reference it in your message body using a
quince:/media/URI:
Check the results: quince:/media/report.pdf
- Send via HTTP API or MUA as usual.
Quince validates that the file exists, delivers the text immediately, then transfers the file in the background over Hyperdrive.
Files arrive in ~/.quince/media/<sender-pubkey>/. The message body is rewritten to show the local path and file size.
Received files are also accessible via the HTTP API: GET /media/<sender-pubkey>/report.pdf.
quince transfers # active/pending transfers
quince transfers --all # include completed
curl http://localhost:2580/api/transfersQuince stores configuration in ~/.quince/config.json:
{
"username": "alice",
"smtpPort": 2525,
"pop3Port": 1110,
"httpPort": 2580,
"peers": {
"bob": "b0b5pubkey1234567890abcdef1234567890abcdef1234567890abcdef1234"
},
"trustIntroductions": {
"bob": true
},
"directory": {
"url": "https://quincemail.com",
"autoLookup": true,
"listed": true
},
"gateRules": [
{"action": "accept", "conditions": {"fromDomain": "*.edu"}}
]
}| Field | Default | Description |
|---|---|---|
username |
user |
Local username (used in email addresses) |
smtpPort |
2525 |
SMTP server port |
pop3Port |
1110 |
POP3 server port |
httpPort |
2580 |
HTTP API port |
peers |
{} |
Map of aliases to 64-char hex public keys |
trustIntroductions |
{} |
Map of aliases to boolean — auto-accept introductions from these peers |
directory.url |
https://quincemail.com |
Directory service URL |
directory.autoLookup |
true |
Auto-resolve unknown usernames via directory |
directory.listed |
true |
Include this daemon in the public directory |
gateRules |
[] |
Filter rules for paid email gate |
| Command | Description |
|---|---|
quince init |
Initialize identity and config (no daemon) |
quince start |
Start the daemon |
quince identity |
Show your public key and email address |
quince peers |
List configured peers |
quince add-peer <alias> <pubkey> |
Add a peer to the whitelist |
quince remove-peer <alias> |
Remove a peer |
quince config |
Show current configuration |
quince inbox |
List received messages |
quince queue |
Show queued messages |
quince queue clear |
Clear all queued messages |
quince transfers |
Show active file transfers |
quince transfers --all |
Show all transfers including completed |
quince introductions |
List pending introductions |
quince accept-introduction <pubkey> |
Accept a pending introduction |
quince help |
Show usage |
Each daemon has a unique Ed25519 keypair generated on first run:
~/.quince/id # secret key (mode 0600)
~/.quince/id_pub # public key (safe to share)
Your email address is derived from your public key: <user>@<pubkey>.quincemail.com.
Protect your private key — anyone with access can impersonate you. Quince refuses to start if ~/.quince/id has permissions other than 0600.
Outbound messages are signed with your Ed25519 key. The signature is a BLAKE2b hash of the message body, signed and injected as an X-Quince-Signature MIME header. Recipients verify automatically — tampered messages are flagged.
Override config file settings:
| Variable | Default | Description |
|---|---|---|
SMTP_PORT |
2525 | SMTP server port |
POP3_PORT |
1110 | POP3 server port |
HTTP_PORT |
2580 | HTTP API port |
BIND_ADDR |
127.0.0.1 | Bind address for all servers |
HOSTNAME |
quince.local | Server hostname |
LOCAL_USER |
user | Local username |
npm testSpins up two full daemon instances with Hyperswarm to test real peer-to-peer messaging and whitelist enforcement. Runs unit tests first automatically.
./test/run-tests.sh~/.quince/
id # Ed25519 secret key (mode 0600)
id_pub # Ed25519 public key (safe to share)
config.json # Daemon configuration
introductions.json # Pending peer introductions
inbox/ # Received messages (.eml) and index
queue/ # Outbound message queue
media/ # Files for sending (user-managed)
media/<pubkey>/ # Received files (per-sender)
drives/ # Hyperdrive storage (Corestore internals)
transfers.json # File transfer state
MIT
Quince also speaks SMTP and POP3 on localhost, so any standard mail client works. This is useful for humans who want to read quince messages in Thunderbird, Apple Mail, or similar.
Quince uses quincemail.com subdomains so that mail clients accept the addresses and connect to localhost. This requires a wildcard DNS record:
*.quincemail.com. IN A 127.0.0.1
*.quincemail.com. IN AAAA ::1
This makes any address like b56b17b7...quincemail.com resolve to 127.0.0.1. No MX records are needed — message routing uses Hyperswarm, not SMTP relay.
| Protocol | Server | Port | SSL | Auth |
|---|---|---|---|---|
| SMTP (outgoing) | <your-pubkey>.quincemail.com |
2525 | None | None |
| POP3 (incoming) | <your-pubkey>.quincemail.com |
1110 | None | Password (any) |
- Settings > Account Actions > Add Mail Account
- Email:
<username>@<your-pubkey>.quincemail.com - Password: anything (not checked)
- Manual Config:
- Incoming (POP3): Server
<your-pubkey>.quincemail.com, Port1110, SSL None, Auth Normal - Outgoing (SMTP): Server
<your-pubkey>.quincemail.com, Port2525, SSL None, Auth None
- Incoming (POP3): Server
- Confirm the security exception (no TLS on localhost is fine)
- Under Server Settings, check "Leave messages on server"
- Mail > Add Account > Other Mail Account
- Email:
<username>@<your-pubkey>.quincemail.com - Password: anything
- Incoming:
<your-pubkey>.quincemail.comport1110 - Outgoing:
<your-pubkey>.quincemail.comport2525 - Disable SSL for both incoming and outgoing
