Version 1.0 • Secure Asynchronous Messaging for the Decentralized Web
Built on Dap Identity and neue.cash Economics
The Inbox Protocol defines a secure, asynchronous messaging system designed for the decentralized web. It provides end‑to‑end encrypted communication built on cryptographic identity through the Dap naming system, with spam resistance achieved through economic signals via neue.cash micropayments.
This specification establishes the message formats, cryptographic operations, delivery mechanisms, and policy enforcement rules that enable trustless peer‑to‑peer messaging without reliance on centralized intermediaries.
The Inbox Protocol is designed to achieve the following objectives:
The following features are explicitly out of scope for version 1.0 of this specification:
The following terms are used throughout this specification:
Dap name: A human‑readable identifier registered on the Dap network that resolves to cryptographic keys and service endpoints.
Endpoint: An HTTPS URL where a recipient’s inbox server accepts incoming messages.
Envelope: The unencrypted metadata portion of a message containing routing and verification information.
Payload: The encrypted content portion of a message containing the actual message body and attachments.
Allowlist: A recipient‑maintained list of senders permitted to deliver messages without payment.
Blocklist: A recipient‑maintained list of senders whose messages are rejected or silently dropped.
Stranger fee: A neue.cash payment required from senders not on the recipient’s allowlist.
This specification uses the following conventions:
<algorithm>:<base64-encoded-key>blake3:...)Inbox addresses are derived from Dap names and support optional subaddressing for routing and policy purposes.
Primary address format: <name>
Subaddress format: <name>/<subaddress>
Examples of valid addresses:
grace — Primary address for the Dap name "grace"grace/business — Subaddress for business correspondenceada/spam — Subaddress that might apply different policy rulesSubaddresses are arbitrary strings chosen by the recipient. They allow a single identity to present multiple contact points with independent policies.
To send a message, the sender must resolve the recipient’s Dap name to obtain their inbox configuration. Resolution is performed through the Dap network and returns an InboxRecord containing:
Implementations SHOULD cache resolved records to reduce network overhead. A cache TTL of 24 hours is RECOMMENDED, with immediate refresh on delivery errors indicating key changes.
Subaddresses enable recipients to compartmentalize their communications. Each subaddress may define policy overrides that take precedence over the base policy.
When a message is addressed to name/subaddress, the recipient’s server:
InboxRecordpolicy.subaddresses mapUnknown subaddresses are delivered using the base policy unless the recipient has configured explicit rejection of unknown subaddresses.
InboxRecord StructureThe InboxRecord is stored in the Dap network and contains all information needed to send messages to an identity.
| Field | Type | Description |
|----------|-------------|----------------------------------------------------|
| endpoint | string | HTTPS URL where messages are delivered |
| keys | InboxKeys | Cryptographic keys for encryption and verification |
| policy | InboxPolicy | Delivery policy configuration |
InboxKeys StructureThe keys object contains the recipient’s current and historical cryptographic keys.
| Field | Type | Description |
|----------|--------|-----------------------------------------------------------|
| current | object | Active keypair: id, encrypt, verify, created |
| previous | array | Optional array of expired keys with expiration timestamps |
| revoked | array | Optional array of revoked key IDs |
Each key entry contains:
id: Unique identifier in format k<unix-timestamp>encrypt: X25519 public key for encryptionverify: Ed25519 public key for signature verificationcreated: Unix timestamp when the key was generatedexpires: (previous keys only) Unix timestamp after which the key should not be usedInboxPolicy StructureThe policy object defines the recipient’s message acceptance rules.
| Field | Type | Default | Description |
|---------------|---------|----------|-------------------------------------------------------|
| allowlistOnly | boolean | false | If true, reject messages from non‑allowlisted senders |
| blockBehavior | string | "reject" | "reject" returns error, "silent" drops quietly |
| blocklist | array | [] | Dap names whose messages are rejected |
| maxSize | integer | 1048576 | Maximum message size in bytes |
| strangerFee | string | "0" | neue.cash amount required from strangers |
| subaddresses | object | null | Map of subaddress to policy overrides |
A complete InboxRecord example:
{
"endpoint": "https://inbox.grace.dap/messages",
"keys": {
"current": {
"created": 1706812800000,
"encrypt": "x25519:MCowBQYDK2VuAyEA...",
"id": "k1706812800",
"verify": "ed25519:MCowBQYDK2VwAyEA..."
},
"previous": [{
"created": 1698969600000,
"encrypt": "x25519:MCowBQYDK2VuAyEA...",
"expires": 1714521600000,
"id": "k1698969600",
"verify": "ed25519:MCowBQYDK2VwAyEA..."
}],
"revoked": []
},
"policy": {
"allowlistOnly": false,
"blockBehavior": "reject",
"blocklist": ["spammer"],
"maxSize": 1048576,
"strangerFee": "0.001",
"subaddresses": {
"business": { "strangerFee": "0.01" }
}
}
}
The Inbox Protocol uses the following cryptographic primitives:
| Purpose | Algorithm | Reference |
|----------------------|--------------------|-------------------------|
| Digital Signatures | Ed25519 | RFC 8032 |
| Hashing | BLAKE3 | BLAKE3 Specification |
| Key Agreement | X25519 | RFC 7748 |
| Symmetric Encryption | XChaCha20‑Poly1305 | draft‑irtf‑cfrg‑xchacha |
These algorithms were selected for their security properties, performance characteristics, and broad implementation availability.
Public keys are encoded using the format:
<algorithm>:<base64-encoded-key>
Examples:
x25519:MCowBQYDK2VuAyEA7PgL... (encryption key)ed25519:MCowBQYDK2VwAyEAx9Nk... (verification key)Key identifiers follow the format:
k<unix-timestamp>
Where the timestamp is the Unix time (in seconds) when the key was generated. Key IDs MUST be unique within an identity’s key history.
Message encryption follows a hybrid encryption scheme:
inbox.v1.key-encapsulationThe encrypted output format is:
encrypted:<base64(ephemeral_pubkey || encrypted_symkey || ciphertext)>
Message signatures are computed over a canonical serialization of the envelope:
ed25519:<base64-encoded-signature>The canonical JSON ensures signature verification produces consistent results across implementations.
InboxMessage StructureA complete Inbox message consists of a version indicator, metadata envelope, and encrypted payload.
| Field | Type | Description |
|----------|---------------|-----------------------------------------------|
| envelope | InboxEnvelope | Unencrypted routing and verification metadata |
| payload | string | Encrypted message content |
| v | integer | Protocol version (currently 1) |
InboxEnvelope StructureThe envelope contains all information needed for routing, verification, and policy enforcement.
| Field | Type | Required | Description |
|------------|---------|----------|---------------------------------------------|
| from | string | Yes | Sender’s Dap name |
| hash | string | Yes | BLAKE3 hash of encrypted payload |
| keyId | string | Yes | ID of recipient’s key used for encryption |
| parent | string | No | Hash of parent message for threading |
| payment | object | No | neue.cash payment proof (amount, tx) |
| recipients | array | No | Other recipients’ Dap names (multi‑send) |
| signature | string | Yes | Sender’s Ed25519 signature |
| to | string | Yes | Recipient address (name or name/subaddress) |
| ts | integer | Yes | Unix timestamp in milliseconds |
InboxPayload StructureThe decrypted payload contains the actual message content.
| Field | Type | Required | Description |
|-------------|--------|----------|---------------------------------------------|
| attachments | array | No | Array of InboxAttachment references |
| body | array | Yes* | Array of InboxBody content blocks |
| editHistory | array | No | Previous version hashes for edits |
| editOf | string | No* | Hash of message being edited |
| emoji | string | No* | Emoji content for reactions |
| reactTo | string | No* | Hash of message being reacted to |
| subject | string | No | Message subject line |
| type | string | Yes | "message", "reaction", "edit", or "retract" |
* Required fields depend on payload type. Messages require body; reactions require reactTo and emoji; edits require editOf and body.
InboxBody StructureMessage body content supports multiple representations for client compatibility.
| Field | Type | Description |
|---------|--------|----------------------------------------------------------|
| content | string | The actual text content |
| type | string | MIME type: "text/plain", "text/html", or "text/markdown" |
A message body SHOULD include at least a text/plain representation for maximum compatibility. Additional representations (HTML, Markdown) may be included for rich display.
InboxAttachment StructureAttachments are stored externally and referenced by hash.
| Field | Type | Description |
|-------|---------|----------------------------------------------------|
| hash | string | BLAKE3 hash of attachment content |
| mime | string | MIME type of the attachment |
| name | string | Original filename |
| ref | string | URI where attachment can be retrieved |
| size | integer | File size in bytes |
The hash allows recipients to verify attachment integrity after download. Attachments are not counted against message size limits since they are stored externally.
To send a message to multiple recipients, the sender uses a fan‑out model:
Each message has its own envelope (different to address, keyId, signature) but shares the same encrypted payload content. This approach does not create a persistent group identity—each recipient sees the message independently.
Recipient visibility: When sending to multiple recipients, the sender MUST include the recipients field in the envelope listing all other recipients’ Dap names (excluding the current to address). This allows recipients to see who else received the message and reply‑all if desired.
Example envelope for a message sent to ada, grace, and turing (as received by grace):
{
"from": "hopper",
"hash": "blake3:Ynx7...",
"keyId": "k1706812800",
"recipients": ["ada", "turing"],
"signature": "ed25519:Mx9a...",
"to": "grace",
"ts": 1707000000000
}
The recipients field is omitted for single‑recipient messages.
Attachments are stored externally and referenced by URI in the ref field. The protocol does not mandate a specific storage mechanism—implementations may use any URI scheme appropriate for their deployment.
Retrieval process:
ref URI from the attachment metadatahash field in the attachment metadataAuthentication: When attachments are stored on authenticated endpoints, the sender SHOULD provide retrieval credentials through a secure channel (e.g., within the encrypted payload). The mechanism for credential exchange is implementation‑defined.
Recommended approach: Senders may host attachments on their own infrastructure with time‑limited signed URLs, or use a shared storage service agreed upon by communicating parties.
Before sending a message, the sender must discover the recipient’s inbox endpoint:
InboxRecordTTL: 24 hours)The complete message sending process:
InboxRecordPOST the complete InboxMessage to the recipient’s endpointThe recipient’s endpoint MUST implement the following HTTP endpoint:
POST /messages
Request body: A complete InboxMessage as JSON
Content type: application/json
The endpoint processes the message and returns an InboxDeliveryResponse indicating acceptance, queuing, or rejection.
Recipients MUST validate incoming messages in the following order:
keyId is current, in previous (not expired), and not revokedmaxSizeThis order ensures efficient rejection of invalid messages before expensive operations.
The endpoint returns one of three response types:
Accepted:
{ "id": "msg:V1StGXR8_Z5jdHi6B-myT", "status": "accepted" }
Queued (for delayed processing):
{ "id": "msg:V1StGXR8_Z5jdHi6B-myT", "reason": "rate limited", "status": "queued" }
Rejected:
{ "error": "PAYMENT_REQUIRED", "message": "Stranger fee of 0.001 required", "status": "rejected" }
Upon accepting a message, the recipient’s endpoint generates a unique message ID for tracking and reference purposes.
Message IDs use the following format:
msg:<nanoid>
Where <nanoid> is a 21‑character identifier generated using the Nano ID algorithm with the default alphabet (A-Za-z0-9_-).
Example: msg:V1StGXR8_Z5jdHi6B-myT
The message ID is returned in the delivery response and may be used for:
Message IDs are opaque to senders and SHOULD NOT be used for cryptographic purposes. The canonical identifier for a message remains its content hash.
Message timestamps are validated against the following bounds:
MAX_CLOCK_DRIFT: 5 minutes into the futureMAX_AGE: 7 days into the pastMessages with timestamps outside this window are rejected with TIMESTAMP_INVALID. This prevents replay attacks using old messages while accommodating reasonable clock skew between systems.
Recipients MUST maintain a record of seen message hashes to prevent duplicate delivery:
MAX_AGE (7 days) to bound storage requirementsThe combination of hash and timestamp in the signature prevents attackers from modifying timestamps on captured messages.
The sender’s signature covers both the timestamp and the payload hash, creating a binding that prevents manipulation:
An attacker who captures a message cannot replay it after the MAX_AGE window expires, and cannot modify the timestamp because it would invalidate the signature.
Recipients SHOULD rotate their keys periodically (recommended: every 6-12 months). The rotation process:
The overlap period (typically 30 days) ensures messages encrypted with the old key can still be decrypted during the transition.
If a private key is compromised, the recipient MUST revoke it immediately:
Messages encrypted with a revoked key are rejected with KEY_REVOKED. Senders receiving this error MUST refresh the recipient’s record and retry with the current key.
Senders SHOULD cache recipient key information to reduce resolution overhead:
InboxRecord after successful resolutionTTL: 24 hoursOn receiving a key‑related error, senders should implement the following retry behavior:
| Error | Action |
|-------------|--------------------------------------------------|
| KEY_EXPIRED | Refresh recipient record, retry with current key |
| KEY_REVOKED | Refresh recipient record, retry with current key |
| KEY_UNKNOWN | Refresh recipient record, retry with current key |
After refreshing, the sender should retry the message once. Repeated failures indicate a persistent problem and should be reported to the user.
Recipients may maintain an allowlist of trusted senders. Allowlist behavior:
allowlistOnly is false: All senders accepted (subject to fee/blocklist)allowlistOnly is true: Only allowlisted senders acceptedThe allowlist API (authenticated):
GET /allowlist — Retrieve current allowlist entriesPOST /allowlist — Add a Dap name to the allowlistDELETE /allowlist — Remove a Dap name from the allowlistAuthentication for these management endpoints is implementation‑defined and outside the scope of this specification.
Recipients may block specific senders. Blocklist behavior depends on blockBehavior:
BLOCKED error to senderSilent blocking prevents senders from confirming they are blocked. The blocklist API follows the same pattern as allowlists (GET/POST/DELETE /blocklist).
Authentication for these management endpoints is implementation‑defined and outside the scope of this specification.
The stranger fee mechanism provides economic spam resistance:
strangerFee > 0, sender must include paymentenvelope.payment: { amount, tx }The payment amount MUST be at least equal to the strangerFee specified in the policy. Subaddresses may override the fee (e.g., higher fee for business inquiries).
The maxSize policy field specifies the maximum total size of a message in bytes:
Messages exceeding the size limit are rejected with SIZE_EXCEEDED.
Messages may reference a parent message by including the parent’s hash in the envelope:
"parent": "blake3:Ynx7..."
Parent references form a directed acyclic graph (DAG), similar to a Merkle tree. A message may only have one direct parent, but the graph allows branching when multiple parties reply to the same message.
Clients reconstruct conversation threads locally:
No server‑side thread state is required—threads are implicit in the message graph.
Reactions are lightweight responses that reference another message:
{
"emoji": "👍",
"reactTo": "blake3:Ynx7...",
"type": "reaction"
}
The reactTo field contains the hash of the message being reacted to. Multiple reactions from the same sender replace previous reactions (last one wins).
Edits allow senders to modify previously sent messages:
{
"body": [{ "content": "Updated content", "type": "text/plain" }],
"editHistory": ["blake3:Ynx7..."],
"editOf": "blake3:Ynx7...",
"type": "edit"
}
The editHistory array contains hashes of all previous versions, allowing clients to show edit history if desired. Only the original sender may edit a message.
Retractions signal that the sender wishes to withdraw a message:
{
"editOf": "blake3:Ynx7...",
"type": "retract"
}
Recipients MAY honor retractions by hiding the original message, but are not required to delete it. The retraction serves as a signal of sender intent, not a guarantee of removal.
The following error codes are defined for message rejection:
| Code | Description |
|-------------------|------------------------------------------------------------|
| BLOCKED | Sender is on recipient’s blocklist |
| DUPLICATE | Message hash has been seen before |
| KEY_EXPIRED | Encryption key found in previous array but past expiration |
| KEY_REVOKED | Encryption key is in revoked list |
| KEY_UNKNOWN | Encryption key ID not recognized |
| PAYMENT_INVALID | Payment proof failed validation |
| PAYMENT_REQUIRED | Stranger fee required but not provided |
| POLICY_VIOLATION | Generic policy failure (unspecified reason) |
| SIGNATURE_INVALID | Sender signature verification failed |
| SIZE_EXCEEDED | Message exceeds `maxSize` limit |
| TIMESTAMP_INVALID | Timestamp outside acceptable window |
Error responses follow this structure:
{
"error": "<code>",
"message": "<optional human-readable detail>",
"status": "rejected"
}
The message field is optional and provides additional context for debugging. Implementations SHOULD NOT expose internal details that could aid attackers.
Errors are classified as retryable or terminal:
Retryable after refresh:
KEY_EXPIRED, KEY_REVOKED, KEY_UNKNOWN — Refresh recipient record and retryPAYMENT_REQUIRED — Obtain payment and retryTerminal (do not retry):
BLOCKED — Sender is permanently blockedDUPLICATE — Message was already deliveredSIGNATURE_INVALID — Indicates implementation bug or attackSIZE_EXCEEDED — Reduce message size before retryingTIMESTAMP_INVALID — Adjust system clock or accept failureThe Inbox Protocol is designed to protect against the following adversaries:
Passive network observers: Attackers who can observe network traffic but cannot modify it. They may attempt to learn message contents or metadata.
Active MITM attackers: Attackers who can intercept and modify network traffic. They may attempt to impersonate senders or recipients.
Compromised endpoints: Recipient servers that may be malicious or compromised. They have access to metadata but should not be able to read encrypted content.
Malicious senders: Attackers who attempt to send spam, phishing, or abusive content to recipients.
The protocol provides the following protections:
| Threat | Mitigation |
|-------------------|-----------------------------------------------------|
| Passive observers | End-to-end encryption (XChaCha20-Poly1305) |
| MITM attacks | Signatures + Dap resolution for sender verification |
| Key compromise | Key rotation + revocation mechanisms |
| Spam/abuse | Allowlists + stranger fees + blocklists |
Version 1.0 of the protocol has the following known limitations:
Implementations SHOULD follow these security practices:
The v field in InboxMessage indicates the protocol version. Version handling rules:
New payload types may be introduced beyond the core set (edit, message, reaction, retract):
Fields prefixed with x- are reserved for experimental and extension use:
x- fields in the envelope may be used for transport‑level extensionsx- fields in the payload may be used for application‑level extensionsx- fieldsThe following specifications are required for implementation:
The following specifications informed the design of this protocol:
The following TypeScript definitions provide a normative reference for implementers:
interface InboxRecord {
endpoint: string;
keys: InboxKeys;
policy: InboxPolicy;
}
interface InboxKeys {
current: {
created: number;
encrypt: string;
id: string;
verify: string;
};
previous?: Array<{
created: number;
encrypt: string;
expires: number;
id: string;
verify: string;
}>;
revoked?: string[];
}
interface InboxPolicy {
allowlistOnly: boolean;
blockBehavior?: "reject" | "silent";
blocklist?: string[];
maxSize: number;
strangerFee: string;
subaddresses?: Record>;
}
interface InboxMessage {
envelope: InboxEnvelope;
payload: string;
v: 1;
}
interface InboxEnvelope {
from: string;
hash: string;
keyId: string;
parent?: string;
payment?: {
amount: string;
tx: string;
};
recipients?: string[];
signature: string;
to: string;
ts: number;
}
interface InboxPayload {
attachments?: InboxAttachment[];
body: InboxBody[];
editHistory?: string[];
editOf?: string;
emoji?: string;
reactTo?: string;
subject?: string;
type: "edit" | "message" | "reaction" | "retract";
}
interface InboxBody {
content: string;
type: "text/html" | "text/markdown" | "text/plain";
}
interface InboxAttachment {
hash: string;
mime: string;
name: string;
ref: string;
size: number;
}
type InboxDeliveryResponse =
| { id: string; status: "accepted" }
| { id: string; reason: string; status: "queued" }
| { error: InboxError; message?: string; status: "rejected" };
type InboxError =
| "BLOCKED"
| "DUPLICATE"
| "KEY_EXPIRED"
| "KEY_REVOKED"
| "KEY_UNKNOWN"
| "PAYMENT_INVALID"
| "PAYMENT_REQUIRED"
| "POLICY_VIOLATION"
| "SIGNATURE_INVALID"
| "SIZE_EXCEEDED"
| "TIMESTAMP_INVALID";
{
"envelope": {
"from": "ada",
"hash": "blake3:Ynx7K9mB2vP...",
"keyId": "k1706812800",
"signature": "ed25519:Mx9aQ2kL7nR...",
"to": "grace",
"ts": 1707000000000
},
"payload": "encrypted:Qm9x4WvR8mN...",
"v": 1
}
{
"envelope": {
"from": "ada",
"hash": "blake3:7FabX3nK2wQ...",
"keyId": "k1706812800",
"parent": "blake3:Ynx7K9mB2vP...",
"payment": {
"amount": "0.01",
"tx": "nc:tx:8QmzR4kL9vB..."
},
"signature": "ed25519:Kp2bM7nR4wX...",
"to": "grace/business",
"ts": 1707000060000
},
"payload": "encrypted:Rm4yK8vN2qP...",
"v": 1
}
{
"envelope": {
"from": "hopper",
"hash": "blake3:Pq8rN2mK4vX...",
"keyId": "k1706812800",
"recipients": ["ada", "turing"],
"signature": "ed25519:Wx7bN4kR2mQ...",
"to": "grace",
"ts": 1707000120000
},
"payload": "encrypted:Kn5xR8vM2pL...",
"v": 1
}
{
"attachments": [
{
"hash": "blake3:9BncX4mR2kL...",
"mime": "application/pdf",
"name": "agenda.pdf",
"ref": "https://files.hopper.dap/a/9BncX4mR2kL?token=eyJhbG...",
"size": 24576
}
],
"body": [
{
"content": "Are we still on for 3pm?",
"type": "text/plain"
}
],
"subject": "Meeting tomorrow",
"type": "message"
}
POST /messagesDeliver a message to the recipient.
Request:
POST /messages HTTP/1.1
Content-Type: application/json
<InboxMessage JSON>
Response (accepted):
HTTP/1.1 200 OK
Content-Type: application/json
{ "id": "msg:V1StGXR8_Z5jdHi6B-myT", "status": "accepted" }
Response (rejected):
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "PAYMENT_REQUIRED",
"message": "Stranger fee of 0.001 required",
"status": "rejected"
}
GET /allowlist
Retrieve the recipient’s allowlist entries.
POST /allowlist
Add a name to the allowlist.
{ "name": "ada" }
DELETE /allowlist
Remove a name from the allowlist.
{ "name": "ada" }
The blocklist API follows the same pattern as allowlist management:
GET /blocklist — Retrieve blocklist entriesPOST /blocklist — Add a name to the blocklistDELETE /blocklist — Remove a name from the blocklistFor signature generation and verification, envelope fields MUST be serialized in canonical form to ensure consistent results across implementations.
The following envelope fields are included in the signature (when present):
from — Sender’s Dap namehash — BLAKE3 hash of encrypted payloadkeyId — Recipient’s key IDparent — Parent message hash (if present)payment — Payment object (if present)recipients — Other recipients array (if present)to — Recipient addressts — TimestampGiven an envelope with all fields present:
Canonical form:
{"from":"ada","hash":"blake3:Ynx7...","keyId":"k1706812800","parent":"blake3:Abc1...","payment":{"amount":"0.01","tx":"nc:tx:8Qmz..."},"recipients":["grace","turing"],"to":"hopper","ts":1707000000000}
Note: The above is shown on one line. The actual canonical form contains no whitespace or line breaks.