Pacto direct messages are end-to-end encrypted using the NIP-59 Gift Wrap envelope (kind 1059) following the NIP-17 private messaging model. Every DM — text, file, reaction, or typing indicator — is sealed inside a Gift Wrap addressed to the recipient’s public key. Relays see only the opaque outer envelope; sender identity, recipient identity, and message content are invisible to the network. The conversation identifier on both sides is always the other user’s npub (Documentation Index
Fetch the complete documentation index at: https://mintlify.com/covenant-gov/pacto-app/llms.txt
Use this file to discover all available pages before exploring further.
npub1…).
Wire format
Each DM travels as a kind 1059 outer envelope (Gift Wrap) that wraps a signed NIP-17-style rumor. The rumor is never published directly to a relay — it exists only after the recipient unwraps the Gift Wrap locally.| Rumor kind | Purpose |
|---|---|
| 14 | Text message |
| 15 | File attachment |
| 7 | Reaction (emoji response to a message) |
| 30078 | Typing indicator |
For the full Nostr kind table used across Pacto — including kind 443 (MlsWelcome) and kind 444 (MlsGroupMessage) — see /reference/nostr-kinds.
Conversation identity
The chat identifier for a DM is the other user’s npub (npub1…). This is the value passed to chatId when loading messages and to receiver when sending. It is what the backend emits on the message_new event as chat_id.
| Context | Value |
|---|---|
chatId in get_message_views | Peer’s npub1… |
receiver in message command | Peer’s npub1… |
event.payload.chat_id in message_new | Peer’s npub1… |
group_id that never starts with npub1.
App flow
Loading a DM thread
Pass the peer’s npub aschatId to retrieve the message history from the local database and in-memory state.
Sending a text DM
file_message and voice_message commands respectively, with the same receiver: peerNpub pattern.
Listening for incoming DMs
Sync and event processing
Historical DM sync is driven by thefetch_messages Tauri command, which queries TRUSTED_RELAYS for Gift Wrap events (kind 1059) addressed to the local account since the last known cursor. Each event is passed through handle_event:
- Unwrap — the Gift Wrap is decrypted locally.
- Branch — if the inner event is kind 443 (MlsWelcome), it is forwarded to the MLS engine and emits
mls_invite_received. All other inner kinds are DM rumors. - Process rumor —
process_rumorstores the message withConversationType::DirectMessageand emitsmessage_newwithchat_idset to the sender’s npub.
wrapper_event_id so re-fetching the same relay history is safe.
WalletBar DMs
The WalletBar (the in-thread wallet UI) uses structured DMs to coordinate wallet actions between participants. These are delivered over the same Gift Wrap path but carry a JSON payload in the rumor’scontent field rather than plain text.
| Structured DM schema | Purpose |
|---|---|
wallet_tx_request | Request a token payment from the peer |
wallet_tx_announcement | Announce a completed on-chain transaction |
Rumor types allowed inside a Gift Wrap
| Kind | Name | Notes |
|---|---|---|
| 14 | Text message | Plain text or markdown body; supports reply tag for threading |
| 15 | File | Attachment with url, m (MIME), optional size |
| 7 | Reaction | Emoji or + / -; e tag references target message |
| 30078 | Typing indicator | Ephemeral; cleared when message is sent |
| 443 | MlsWelcome | Squad/channel invite; not a DM rumor — forwarded to MLS engine |
Privacy guarantees
Pacto’s Gift Wrap implementation provides several layers of metadata protection beyond standard NIP-04 encryption:Relay opacity
Relays receive only kind 1059 events. The inner rumor kind, sender identity, and message content are not visible at the relay layer.
Sender/recipient hiding
The Gift Wrap’s outer
pubkey is an ephemeral throwaway key, not the actual sender’s npub. Recipient is hidden in the encrypted payload.TRUSTED_RELAYS only
All Gift Wraps — DMs and MLS Welcomes — are published only to the app’s curated
TRUSTED_RELAYS set, not arbitrary relay lists.Local-only decryption
Decryption happens entirely on the user’s device in the Rust backend. The Svelte frontend never handles raw nsec material.