XMTP Web3 Messenger Integration
Regular in-dApp chat—Firebase Realtime Database or Websocket with JWT auth. Problem: centralized message storage, dependence on your server, no protocol-level encryption. XMTP (Extensible Message Transport Protocol)—decentralized messaging where messages encrypt with wallet's end-to-end keys and store in decentralized node network. Ethereum address = user identifier, private key = encryption key.
How XMTP Works
On first use, user creates XMTP identity: signs message with wallet, deterministic identity key generated from signature. This key registers in XMTP network. Messages encrypt with recipient's public key via X3DH (Extended Triple Diffie-Hellman)—same protocol as Signal.
Messages store in XMTP nodes, not your server. User can open your integration, Coinbase Wallet, Converse, or any XMTP-compatible client—and see all messages.
Installation and Basic Integration
npm install @xmtp/browser-sdk
import { Client } from "@xmtp/browser-sdk"
// Initialize client
async function initXMTP(signer: WalletSigner) {
const client = await Client.create(signer, { env: "production" })
return client
}
// Check if address is registered in XMTP
async function canMessage(client: Client, address: string): Promise<boolean> {
return await client.canMessage(address)
}
// Create or open conversation
async function getOrCreateConversation(client: Client, recipientAddress: string) {
const conversation = await client.conversations.newConversation(recipientAddress)
return conversation
}
Nuance: Client.create requires wallet signature. First time—two signatures (identity creation), repeat runs—one. Communicate properly in UI or users get confused.
Integration with wagmi/viem
XMTP expects Signer object with signMessage method. Adapter for wagmi:
import { useWalletClient } from "wagmi"
import { Client } from "@xmtp/browser-sdk"
function useXMTPClient() {
const { data: walletClient } = useWalletClient()
const [xmtp, setXmtp] = useState<Client | null>(null)
const connect = async () => {
if (!walletClient) return
// Adapter: wagmi WalletClient -> XMTP Signer
const signer = {
getAddress: () => walletClient.account.address,
signMessage: (message: string) =>
walletClient.signMessage({ message })
}
const client = await Client.create(signer, {
env: "production",
// Persistent key storage in localStorage
persistConversations: true
})
setXmtp(client)
}
return { xmtp, connect }
}
Conversation List and Messages
function ConversationList({ client }: { client: Client }) {
const [conversations, setConversations] = useState<Conversation[]>([])
useEffect(() => {
const loadConversations = async () => {
const convos = await client.conversations.list()
setConversations(convos)
}
loadConversations()
// Listen for new conversations
const stream = client.conversations.stream()
const subscription = stream.then(s => {
;(async () => {
for await (const convo of s) {
setConversations(prev => [convo, ...prev])
}
})()
})
return () => { subscription.then(s => s.return?.()) }
}, [client])
return (
<ul>
{conversations.map(convo => (
<ConversationItem key={convo.peerAddress} conversation={convo} />
))}
</ul>
)
}
function MessageThread({ conversation }: { conversation: Conversation }) {
const [messages, setMessages] = useState<DecodedMessage[]>([])
useEffect(() => {
const loadMessages = async () => {
const msgs = await conversation.messages({ limit: 50 })
setMessages(msgs)
}
loadMessages()
// Realtime streaming of new messages
const startStream = async () => {
for await (const message of await conversation.streamMessages()) {
setMessages(prev => [...prev, message])
}
}
startStream()
}, [conversation])
const sendMessage = async (text: string) => {
await conversation.send(text)
}
return (
<div>
{messages.map(msg => (
<MessageBubble
key={msg.id}
text={msg.content}
isMine={msg.senderAddress === conversation.client.address}
timestamp={msg.sent}
/>
))}
</div>
)
}
Content Types: Rich Message Content
XMTP supports more than text. Content Types—standardized mechanism for arbitrary message types:
import { ContentTypeAttachment, AttachmentCodec } from "@xmtp/content-type-attachments"
import { ContentTypeReaction, ReactionCodec } from "@xmtp/content-type-reaction"
// Initialize with custom content types
const client = await Client.create(signer, {
env: "production",
codecs: [new AttachmentCodec(), new ReactionCodec()]
})
// Send file (up to 1MB, larger—via remote attachments)
await conversation.send({
filename: "contract.pdf",
mimeType: "application/pdf",
data: pdfBytes
}, { contentType: ContentTypeAttachment })
// React to message
await conversation.send({
reference: messageId,
action: "added",
content: "👍",
schema: "unicode"
}, { contentType: ContentTypeReaction })
Standard content types: text/plain, attachment, remote-attachment (files on IPFS/S3), reaction, reply (message replies), read-receipt. For custom needs—register own namespace.
Frames and Bots
XMTP supports Frames (like Farcaster Frames)—interactive cards in messages with buttons. Bot on XMTP—just Node.js process with XMTP client listening for incoming messages and replying:
const botClient = await Client.create(botSigner, { env: "production" })
for await (const message of await botClient.conversations.streamAllMessages()) {
if (message.senderAddress === botClient.address) continue // don't reply to self
const response = await generateBotResponse(message.content)
await message.conversation.send(response)
}
Used for transaction notifications, price alerts, customer support.
Development Timeline
Day 1-2: XMTP SDK integration, wallet adapter, basic conversation and message UI.
Day 3: Real-time streaming, message sending, address search.
Day 4-5: Advanced content types, notifications, mobile adaptation.
Basic messenger with text messages and real-time updates — 2-3 days. Full-featured chat with attachments, reactions, XMTP bot — 4-5 days.







