Skip to main content

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.

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 (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 kindPurpose
14Text message
15File attachment
7Reaction (emoji response to a message)
30078Typing indicator
MLS Welcome messages (kind 443) also travel inside Gift Wraps, but they are not DM rumors — they are routed to the MLS engine instead of the DM conversation. See Squads & Channels for the welcome flow.
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.
ContextValue
chatId in get_message_viewsPeer’s npub1…
receiver in message commandPeer’s npub1…
event.payload.chat_id in message_newPeer’s npub1…
This is distinct from MLS group messaging, where the identifier is a hex group_id that never starts with npub1.

App flow

Loading a DM thread

Pass the peer’s npub as chatId to retrieve the message history from the local database and in-memory state.
import { invoke } from '@tauri-apps/api/core';

// chat_id = peer's npub
const messages = await invoke('get_message_views', { chatId: peerNpub });

Sending a text DM

import { invoke } from '@tauri-apps/api/core';

// Send a text DM to a peer
await invoke('message', {
  receiver: peerNpub,   // npub1… — triggers Gift Wrap path
  content: 'Hello!',
  repliedTo: null       // or message id string to thread a reply
});
For file and voice attachments use the file_message and voice_message commands respectively, with the same receiver: peerNpub pattern.

Listening for incoming DMs

import { listen } from '@tauri-apps/api/event';

// Fires for every new decrypted DM rumor
await listen('message_new', (event) => {
  // event.payload.chat_id === peerNpub
  // event.payload.message contains the decrypted rumor
  console.log(event.payload.chat_id, event.payload.message);
});

Sync and event processing

Historical DM sync is driven by the fetch_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:
  1. Unwrap — the Gift Wrap is decrypted locally.
  2. 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.
  3. Process rumorprocess_rumor stores the message with ConversationType::DirectMessage and emits message_new with chat_id set to the sender’s npub.
Deduplication is handled in the database — events are keyed by 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’s content field rather than plain text.
Structured DM schemaPurpose
wallet_tx_requestRequest a token payment from the peer
wallet_tx_announcementAnnounce a completed on-chain transaction
These events appear as interactive cards in the DM thread. Full field definitions are documented at /reference/wallet-dm-schema.

Rumor types allowed inside a Gift Wrap

KindNameNotes
14Text messagePlain text or markdown body; supports reply tag for threading
15FileAttachment with url, m (MIME), optional size
7ReactionEmoji or + / -; e tag references target message
30078Typing indicatorEphemeral; cleared when message is sent
443MlsWelcomeSquad/channel invite; not a DM rumor — forwarded to MLS engine
Reactions and typing indicators use the same invoke('react_to_message', { chatId: peerNpub, … }) and are routed through the Gift Wrap path just like text messages.

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.
Even with Gift Wrap metadata protection, the fact that two accounts have exchanged events on a relay may be inferable from relay timing metadata in some threat models. For maximum privacy, use Pacto on relays you control or trust.

Build docs developers (and LLMs) love