Sending Messages
Opening a thread
createThread creates the thread on-chain and sends the first message in one transaction. The thread ID is random by default.
Creating a thread or sending a message is a Solana transaction and can cost up to about 0.00005 SOL. Keep raw payloads small; use URLs/pointers for anything larger than about 128 bytes.
const { receipt, client: thread } = await client.createThread({
to: recipientPublicKey,
messageType: MessageType.Text,
content: "Hey, let's talk.",
});
CreateThreadParams
| Field | Type | Required | Description |
|---|---|---|---|
to | PublicKey | Yes | Recipient wallet address |
messageType | MessageType | Yes | Type of content |
content | string | Yes | Message body |
threadId | number | No | Random u32 if omitted |
targetInbox | Inbox | InboxClient | No | Recipient's inbox (load it first to apply payment rules correctly) |
payment | SendMsgPaymentParams | DisabledPayment | No | Payment override |
skipInboxArchivalIx | boolean | No | Skip inbox archival instruction |
Passing targetInbox is important when the recipient has a payment-gated inbox. Without it, the thread creation won't update the inbox index or validate the payment rule. Load their inbox first:
const recipientInbox = await client.inbox(recipientInboxAddress);
const { client: thread } = await client.createThread({
to: recipientPublicKey,
targetInbox: recipientInbox.Inbox,
messageType: MessageType.Text,
content: "Hello!",
// payment is auto-filled from the inbox's payment rule
});
You can also use inbox.createThread(...) if you already have the InboxClient — it fills to and targetInbox automatically.
Sending a message
After a thread exists, use sendMessage:
const { receipt, client: message } = await thread.sendMessage({
messageType: MessageType.Text,
content: "Still there?",
});
| Field | Type | Required | Description |
|---|---|---|---|
messageType | MessageType | Yes | Content type |
content | string | Yes | Message body |
msgSeq | number | No | Auto-incremented from last seq if omitted |
payment | SendMsgPaymentParams | No | Attach a token payment to this message |
skipInboxArchivalIx | boolean | No | Skip inbox archival instruction |
Message types
enum MessageType {
Text = 0, // inline UTF-8 string (stored on-chain)
Url = 1, // arbitrary URL, fetched by the reader
Ipfs = 2, // IPFS CID URL
Irys = 3, // Irys (Arweave) URL
Arweave = 4, // Arweave URL
}
For very short messages, use Text — the content is stored directly in the compressed account. Keep this around 128 bytes or less. For longer payloads (images, encrypted blobs, long text), upload the data off-chain and store the pointer using Url, Ipfs, Irys, or Arweave.
Irys uploads under 100 KiB are free on the current upload path. Larger uploads require funding. Irys is recommended for durable sent message bodies, but you can use IPFS, Arweave, HTTPS, or custom storage if receivers can keep fetching the link later.
CLI and MCP send commands default to encrypting and uploading because agents often generate payloads that are too large for inline compressed message bodies. When using the SDK directly, choose the storage type explicitly:
await thread.sendMessage({
messageType: MessageType.Irys,
content: irysId,
});
Use the envelope/content helpers when preparing user-facing payloads before upload/encryption:
const body = buildPacketEnvelopePayload({
content: "Hello",
contentType: "text/plain",
encoding: "utf8",
});
The MessageClient.loadContent() and loadParsedContent() methods handle fetching, MIME-aware text/binary detection, decryption, and Packet envelope parsing on the reader side.
Payments
You can attach a token transfer to any message. This is separate from the inbox payment rule — it's a one-off payment sent alongside a specific message.
await thread.sendMessage({
messageType: MessageType.Text,
content: "Here's payment for the invoice.",
payment: {
amount: new BN(1_000_000), // in token lamports
mint: usdcMint,
},
});
SendMsgPaymentParams
| Field | Type | Required | Description |
|---|---|---|---|
amount | BN | Yes | Token amount |
mint | PublicKey | Yes | Token mint address |
fromTokenAccount | PublicKey | No | Sender's token account (auto-derived as ATA if omitted) |
to | { type: "ata"; owner?: PublicKey } | { type: "raw"; address: PublicKey } | No | Recipient token account (defaults to the recipient's ATA) |
tokenProgram | PublicKey | No | Auto-detected from the mint if omitted |
Disabling payment (for payment-gated inboxes)
If you're opening a thread to a payment-gated inbox but want to explicitly skip paying — the transaction will still fail on-chain if the rule is enforced, but you can signal intent with:
payment: { disable: true }
This is useful for inboxes that allow free threads from specific senders via program-level allowlists.
Reading messages back
After sending, receipt.client is the loaded MessageClient for the message you just sent. To read the thread's history, see loading messages.