Blockchain-Based Chat Development
"Chat on the blockchain" is a concept that requires immediate clarification. Storing every message on-chain on Ethereum mainnet costs $2-10 per message at normal gas prices. That's not a chat, it's an expensive public registry. The real task is to design a system where the blockchain is used where it's needed: identity, key management, payment channels, proof of existence — while messages themselves are transmitted through a cryptographically secured, decentralized transport.
Solution space: from on-chain to hybrid
Fully on-chain: only for specific cases
Storing messages on-chain makes sense only in very narrow scenarios:
- Governance proposals and discussions (Snapshot, Tally) — immutability matters
- Dispute resolution in arbitration protocols — evidence must be tamper-proof
- Critical announcements for DAOs — provable public visibility
For these cases: Ethereum events (event MessagePosted(address indexed sender, bytes32 indexed channelId, string content)). Calldata for storing text is cheaper than storage. On Ethereum mainnet, 1KB of calldata costs approximately 16,000 gas, or $0.5-2. On Arbitrum or Base — 10-50x cheaper.
P2P with on-chain identity: XMTP
XMTP (Extensible Message Transport Protocol) is a production-ready protocol for Web3 messaging. It's the de facto standard for decentralized chat in 2024. Used in Coinbase Wallet, Converse, and numerous dApps.
XMTP architecture:
- Identity is based on the Ethereum address. No separate account needed.
- Messages are encrypted end-to-end using X3DH (Extended Triple Diffie-Hellman) — the same protocol as Signal.
- Transport is a decentralized P2P network of XMTP nodes.
- On-chain: only key information during the user's first registration.
import { Client } from '@xmtp/xmtp-js';
import { ethers } from 'ethers';
// Initialize client — sign XMTP keys with wallet
const signer = await provider.getSigner();
const xmtp = await Client.create(signer, { env: 'production' });
// Start a conversation with an address
const conversation = await xmtp.conversations.newConversation(
'0xRecipientAddress'
);
// Send a message
await conversation.send('Hello from dApp!');
// Receive messages in real-time
for await (const message of await conversation.streamMessages()) {
console.log(`${message.senderAddress}: ${message.content}`);
}
XMTP supports more than just text — structured content types: transaction notifications, NFT attachments, read receipts. This is especially important for DeFi context: "Sent you 100 USDC" with an embedded transaction preview.
Group chats: XMTP MLS
XMTP v3 (2024) added groups based on MLS (Messaging Layer Security, RFC 9420) — a cryptographic protocol for group encryption with forward secrecy and post-compromise security. A group is a set of participants, each with their own keys; removing from a group prevents reading future messages.
import { Client } from '@xmtp/xmtp-js';
// Create a group
const group = await xmtp.conversations.newGroup([
'0xAddress1',
'0xAddress2',
'0xAddress3'
]);
await group.send('Hello everyone!');
// Manage members
await group.addMembers(['0xNewMember']);
await group.removeMembers(['0xOldMember']); // forward secrecy: keys rotate
Waku: decentralized transport without identity layer
Waku (Status protocol, now a standalone project) is a P2P transport for messaging without centralized servers. Suitable when you only need messaging without Web3 identity.
Waku uses libp2p and gossipsub. Messages have a TTL and are not stored permanently. For persistence — combine with the Waku Store protocol (offline messages) or external storage.
import { createLightNode, waitForRemotePeer } from '@waku/sdk';
import { createEncoder, createDecoder } from '@waku/sdk';
const waku = await createLightNode({ defaultBootstrap: true });
await waitForRemotePeer(waku);
const contentTopic = '/my-chat/1/messages/proto';
const encoder = createEncoder({ contentTopic });
const decoder = createDecoder(contentTopic);
// Subscribe to messages
await waku.filter.subscribe([decoder], (message) => {
if (message.payload) {
const decoded = ChatMessage.decode(message.payload); // Protobuf
console.log(decoded.text);
}
});
// Send
await waku.lightPush.send(encoder, {
payload: ChatMessage.encode({ text: 'hello', sender: address }).finish()
});
Waku is used by Status, Railgun, and several other Web3 projects.
Token-gated chats
Restricting access to a chat based on on-chain conditions is a common pattern for DAOs and NFT collections.
Checking on the backend (if there is one)
// Token-gating check middleware
async function checkTokenGate(userAddress: string, channelId: string): Promise<boolean> {
const gateConfig = await getChannelGate(channelId);
const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
if (gateConfig.type === 'ERC20_MINIMUM') {
const balance = await client.readContract({
address: gateConfig.tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress as `0x${string}`]
});
return balance >= gateConfig.minimumAmount;
}
if (gateConfig.type === 'NFT_HOLDER') {
const balance = await client.readContract({
address: gateConfig.contractAddress,
abi: erc721Abi,
functionName: 'balanceOf',
args: [userAddress as `0x${string}`]
});
return balance > 0n;
}
return false;
}
Serverless token-gate via XMTP
XMTP doesn't have built-in token-gating. But you can implement it at the application level: when joining a group, the caller signs an attestation of their on-chain status. Existing members verify through an Ethereum provider before accepting addMembers.
For more complex scenarios — Sign-In With Ethereum (EIP-4361) + Lit Protocol conditions: only a wallet with the required NFT can receive the channel decryption key.
Payment channels in chat
For micro-payments for messages (pay-per-message models, tip systems, spam prevention) — integrate a payment layer:
Superfluid streams: the sender opens a money flow to the recipient, which remains active while the conversation continues. Closing the conversation closes the flow. Implemented through the Superfluid SDK + UI hooks.
Inline ETH transfers: when sending a message — optional button "Add a tip". Creates an XMTP message of special content type transaction-reference + a parallel on-chain transaction.
Message history storage and privacy
Decentralized transports (XMTP, Waku) don't guarantee permanent storage of old messages. For archives:
- Ceramic Network: append-only streams with cryptographic authorship guarantees
- Arweave: permanent storage, more expensive, but permanent. Used for critical communications
- Self-hosted: users store their own messages locally (IndexedDB), synchronize through IPFS
Privacy mode: Railgun integration for anonymous messages — sender and receiver are encrypted through zk-proofs. Niche, but existing use case.
Frontend architecture
Chat is one of the most state management-demanding UI components. For Web3 chat:
Real-time messaging: WebSocket or SSE for XMTP relay, or polling (inefficient). XMTP JS SDK has a built-in streamMessages() async iterator.
Message persistence: TanStack Query with infinite scroll for history. Optimistic updates for just-sent messages — a message appears immediately, status changes from pending to sent upon confirmation.
Content types: rendering markdown, embedded NFT previews (via opensea/simplehash API), transaction previews for embedded tx links.
function ChatMessage({ message }: { message: DecodedMessage }) {
if (message.contentType?.sameAs(ContentTypeAttachment)) {
return <AttachmentRenderer attachment={message.content} />;
}
if (message.contentType?.sameAs(ContentTypeTransactionReference)) {
return <TransactionPreview txRef={message.content} />;
}
// Text message
return <ReactMarkdown>{message.content}</ReactMarkdown>;
}
Development stack
| Component | Technology |
|---|---|
| Messaging protocol | XMTP v3 |
| P2P transport (alternative) | Waku SDK |
| Identity | Ethereum + EIP-4361 |
| Token-gating | viem + custom logic |
| Payment streams | Superfluid SDK |
| Frontend | Next.js / React + wagmi |
| State management | Zustand + TanStack Query |
| Storage (archive) | Ceramic or Arweave |
Timeline estimates
Basic 1-on-1 chat with XMTP, wallet-based identity, and token-gating for one contract — 1-2 weeks. Group chats (XMTP MLS), multiple token-gate conditions, history persistence, embedded transactions, and mobile adaptation — 4-6 weeks. Full-featured platform with custom P2P transport, payment channels, admin tools, and multi-chain support — 2-3 months.







