Skip to main content

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

FieldTypeRequiredDescription
toPublicKeyYesRecipient wallet address
messageTypeMessageTypeYesType of content
contentstringYesMessage body
threadIdnumberNoRandom u32 if omitted
targetInboxInbox | InboxClientNoRecipient's inbox (load it first to apply payment rules correctly)
paymentSendMsgPaymentParams | DisabledPaymentNoPayment override
skipInboxArchivalIxbooleanNoSkip 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?",
});
FieldTypeRequiredDescription
messageTypeMessageTypeYesContent type
contentstringYesMessage body
msgSeqnumberNoAuto-incremented from last seq if omitted
paymentSendMsgPaymentParamsNoAttach a token payment to this message
skipInboxArchivalIxbooleanNoSkip 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

FieldTypeRequiredDescription
amountBNYesToken amount
mintPublicKeyYesToken mint address
fromTokenAccountPublicKeyNoSender's token account (auto-derived as ATA if omitted)
to{ type: "ata"; owner?: PublicKey } | { type: "raw"; address: PublicKey }NoRecipient token account (defaults to the recipient's ATA)
tokenProgramPublicKeyNoAuto-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.