solana-messenger-sdk 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/GUIDE.md ADDED
@@ -0,0 +1,628 @@
1
+ # Integration Guide — Building on Solana Messenger
2
+
3
+ This guide walks you through every operation the protocol supports, with code examples and explanations of when and why you'd use each one.
4
+
5
+ **Program:** `msg1SxLsvf1ZL374noHwUWcVYjPsNSNwKb3xphg6Lxf` (mainnet & devnet)
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Setup](#setup)
10
+ 2. [Identity & Registration](#identity--registration)
11
+ 3. [Direct Messages](#direct-messages)
12
+ 4. [Servers](#servers)
13
+ 5. [Channels](#channels)
14
+ 6. [Discovery](#discovery)
15
+ 7. [Key Management](#key-management)
16
+ 8. [Account Deserialization](#account-deserialization)
17
+ 9. [Common Patterns](#common-patterns)
18
+ 10. [Architecture Reference](#architecture-reference)
19
+
20
+ ---
21
+
22
+ ## Setup
23
+
24
+ ```typescript
25
+ import { SolanaMessenger, ChannelType } from "solana-messenger-sdk";
26
+ import { readFileSync } from "fs";
27
+
28
+ // Option 1: Self-custody (you have a keypair file)
29
+ const keypair = new Uint8Array(JSON.parse(readFileSync("~/.config/solana/id.json", "utf-8")));
30
+ const messenger = new SolanaMessenger({
31
+ apiKey: "YOUR_HELIUS_API_KEY",
32
+ network: "mainnet", // or "devnet"
33
+ keypair,
34
+ });
35
+
36
+ // Option 2: Custodial wallet (Privy, Turnkey, etc.)
37
+ const messenger = new SolanaMessenger({
38
+ apiKey: "YOUR_HELIUS_API_KEY",
39
+ walletAddress: "YourCustodialWalletAddress",
40
+ signer: async (unsignedTx, recentBlockhash, feePayer) => {
41
+ return await yourCustodialProvider.signTransaction(unsignedTx);
42
+ },
43
+ });
44
+
45
+ // Always call init() first — generates encryption key, registers on-chain
46
+ const { encryptionAddress, status } = await messenger.init();
47
+ // status: "registered" | "already_registered" | "updated"
48
+ ```
49
+
50
+ **Why two keys?** Your wallet key (A) signs transactions and can be custodial. Your encryption key (B) is generated locally and never leaves your machine. This means Privy/Turnkey can sign transactions for you but can never read your messages.
51
+
52
+ ---
53
+
54
+ ## Identity & Registration
55
+
56
+ Every user needs an on-chain encryption key registry before they can send or receive messages.
57
+
58
+ ### Register (automatic via init)
59
+
60
+ ```typescript
61
+ await messenger.init(); // handles registration automatically
62
+ ```
63
+
64
+ `init()` does three things:
65
+ 1. Generates a local encryption keypair (stored at `~/.solana-messenger/keys/<address>.json`)
66
+ 2. Registers the public key on-chain at PDA `["messenger", walletAddress]`
67
+ 3. Loads platform config (fee vault, protocol fee)
68
+
69
+ ### Set spam fee
70
+
71
+ **When:** You want senders to pay you a fee to send DMs (spam deterrent).
72
+
73
+ ```typescript
74
+ await messenger.setMinFee(10000); // 10,000 lamports (~$0.001) per DM to you
75
+ ```
76
+
77
+ Senders automatically pay this fee when calling `send()`. It goes directly to your wallet.
78
+
79
+ ### Look up someone's encryption key
80
+
81
+ **When:** You want to verify someone is registered, or you're building custom encryption.
82
+
83
+ ```typescript
84
+ const encKey = await messenger.lookupEncryptionKey("SomeWalletAddress");
85
+ if (!encKey) console.log("User not registered — can't message them");
86
+ ```
87
+
88
+ ### Deregister
89
+
90
+ **When:** You're done with the protocol and want to reclaim rent (~0.001 SOL).
91
+
92
+ ```typescript
93
+ await messenger.deregister();
94
+ ```
95
+
96
+ ⚠️ After deregistering, nobody can send you messages until you re-register.
97
+
98
+ ---
99
+
100
+ ## Direct Messages
101
+
102
+ Wallet-to-wallet encrypted messages. Permanent, protocol-level, nobody can revoke them.
103
+
104
+ ### Send a DM
105
+
106
+ ```typescript
107
+ const signatures = await messenger.send("RecipientAddress", "Hey, what's up?");
108
+ // Returns array of tx signatures (multiple if message was chunked)
109
+ ```
110
+
111
+ The SDK:
112
+ 1. Looks up recipient's encryption key from the registry
113
+ 2. Performs X25519 Diffie-Hellman key exchange
114
+ 3. Encrypts with NaCl box (XSalsa20-Poly1305)
115
+ 4. Auto-chunks messages > 661 bytes
116
+ 5. Pays protocol fee + recipient min_fee automatically
117
+
118
+ ### Read DMs
119
+
120
+ ```typescript
121
+ const messages = await messenger.read({ limit: 20, since: 1710000000 });
122
+ for (const msg of messages) {
123
+ console.log(`[${new Date(msg.timestamp * 1000).toISOString()}] ${msg.sender}: ${msg.text}`);
124
+ }
125
+ ```
126
+
127
+ ### Listen for DMs in real-time
128
+
129
+ ```typescript
130
+ const unsubscribe = await messenger.listen((msg) => {
131
+ console.log(`New DM from ${msg.sender}: ${msg.text}`);
132
+ });
133
+
134
+ // Later: stop listening
135
+ unsubscribe();
136
+ ```
137
+
138
+ Uses WebSocket subscription — ~400ms latency.
139
+
140
+ ---
141
+
142
+ ## Servers
143
+
144
+ Servers are workspaces (like Discord servers or Slack workspaces). They contain channels and members with roles.
145
+
146
+ ### Create a server
147
+
148
+ **When:** You want to create a workspace for a team, community, or organization.
149
+
150
+ ```typescript
151
+ const { serverId, serverPda, signature } = await messenger.createServer("My Team");
152
+ ```
153
+
154
+ What happens:
155
+ 1. Generates a random server ID (keypair pubkey)
156
+ 2. Creates Server PDA at `["server", serverId]`
157
+ 3. Creates your ServerMember PDA at `["server_member", serverId, yourWallet]` with role=Owner
158
+ 4. Generates the first server encryption key (version 0) and wraps it for you
159
+
160
+ You're now the **Owner** — full control over the server.
161
+
162
+ ### Invite a member
163
+
164
+ **When:** You want to add someone to your server. You must be Admin or Owner.
165
+
166
+ ```typescript
167
+ // You need to know the current server keys to wrap them for the new member
168
+ const myMember = await messenger.getServerMember(serverId);
169
+ // Unwrap your server keys first
170
+ const serverKeys = myMember.keyWraps.map(kw => {
171
+ const key = messenger.unwrapKey(kw.wrappedKey, kw.nonce, adminEncryptionPubkey);
172
+ if (!key) throw new Error("Failed to unwrap key");
173
+ return key;
174
+ });
175
+
176
+ await messenger.inviteToServer(serverId, "NewMemberWallet", serverKeys);
177
+ ```
178
+
179
+ What happens:
180
+ 1. Verifies you're Admin/Owner (checks your ServerMember PDA)
181
+ 2. Creates a ServerMember for the new member
182
+ 3. Wraps ALL historical server key versions for them (so they can read full history)
183
+ 4. New member gets role=Member
184
+
185
+ ### Remove a member
186
+
187
+ **When:** Someone needs to be kicked. Admin/Owner only. **Rotates the server key** so the removed member can't read future messages.
188
+
189
+ ```typescript
190
+ // Get current member list
191
+ const members = await messenger.getServerMembers(serverId);
192
+ const remaining = members
193
+ .filter(m => m.account.member !== "RemovedMemberWallet")
194
+ .map(m => m.account.member);
195
+
196
+ await messenger.removeServerMember(serverId, "RemovedMemberWallet", remaining);
197
+ ```
198
+
199
+ What happens:
200
+ 1. Closes the removed member's ServerMember PDA (reclaims rent)
201
+ 2. Generates a new server key (version N+1)
202
+ 3. Wraps the new key for all remaining members
203
+ 4. Removed member still has old keys cached locally (can read old messages) but can't decrypt anything new
204
+
205
+ ### Leave a server
206
+
207
+ **When:** You want to leave voluntarily. Also rotates the key (forward secrecy).
208
+
209
+ ```typescript
210
+ const members = await messenger.getServerMembers(serverId);
211
+ const remaining = members
212
+ .filter(m => m.account.member !== myAddress)
213
+ .map(m => m.account.member);
214
+
215
+ await messenger.leaveServer(serverId, remaining);
216
+ ```
217
+
218
+ ⚠️ The server **owner** cannot leave — transfer ownership first (not yet in SDK, use low-level builders).
219
+
220
+ ### Update server name
221
+
222
+ ```typescript
223
+ await messenger.updateServer(serverId, "New Server Name");
224
+ ```
225
+
226
+ ### Close a server
227
+
228
+ **When:** You're done with the server entirely. Owner only. Reclaims all rent.
229
+
230
+ ```typescript
231
+ const members = await messenger.getServerMembers(serverId);
232
+ const memberAddresses = members.map(m => m.account.member);
233
+ await messenger.closeServer(serverId, memberAddresses);
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Channels
239
+
240
+ Channels are where messages are sent. Two types:
241
+
242
+ | Type | Key Source | Members | Use Case |
243
+ |------|-----------|---------|----------|
244
+ | **Public** | Server key | All server members | #general, #announcements |
245
+ | **Private** | Own channel key | Invite-only | DM groups, private discussions |
246
+
247
+ Channels can be **server channels** (belong to a server) or **standalone** (group DMs without a server).
248
+
249
+ ### Create a public channel (in a server)
250
+
251
+ **When:** You want a channel visible to all server members. Messages encrypted with the server key.
252
+
253
+ ```typescript
254
+ const { channelId, channelPda } = await messenger.createChannel({
255
+ serverId: serverId,
256
+ channelType: ChannelType.Public,
257
+ members: [], // public channels don't need a member list
258
+ });
259
+ ```
260
+
261
+ ### Create a private channel (in a server)
262
+
263
+ **When:** You want an invite-only channel within a server. Has its own encryption key.
264
+
265
+ ```typescript
266
+ const { channelId, channelPda } = await messenger.createChannel({
267
+ serverId: serverId,
268
+ channelType: ChannelType.Private,
269
+ members: [myAddress, member1, member2],
270
+ });
271
+ ```
272
+
273
+ ### Create a standalone group DM (no server)
274
+
275
+ **When:** You want a group chat without the overhead of a full server.
276
+
277
+ ```typescript
278
+ const { channelId, channelPda } = await messenger.createChannel({
279
+ members: [myAddress, friend1, friend2], // you must be in the list
280
+ });
281
+ ```
282
+
283
+ ### Send a message to a channel
284
+
285
+ **When:** Sending to any channel (public or private).
286
+
287
+ ```typescript
288
+ // For PUBLIC channels: use the server key
289
+ const myServerMember = await messenger.getServerMember(serverId);
290
+ const serverKey = messenger.unwrapKey(
291
+ myServerMember.keyWraps[keyVersion].wrappedKey,
292
+ myServerMember.keyWraps[keyVersion].nonce,
293
+ wrapperEncryptionPubkey,
294
+ );
295
+ const senderServerMemberPda = await deriveServerMemberPda(serverId, myAddress, programId);
296
+
297
+ await messenger.sendChannelMessage({
298
+ channelPda,
299
+ message: "Hello everyone!",
300
+ channelKey: serverKey,
301
+ keyVersion: myServerMember.keyWraps.length - 1, // latest version
302
+ senderServerMemberPda, // REQUIRED for public channels
303
+ });
304
+
305
+ // For PRIVATE channels: use the channel key
306
+ const myChannelMember = await messenger.getChannelMember(channelId);
307
+ const channelKey = messenger.unwrapKey(
308
+ myChannelMember.keyWraps[0].wrappedKey,
309
+ myChannelMember.keyWraps[0].nonce,
310
+ wrapperEncryptionPubkey,
311
+ );
312
+
313
+ await messenger.sendChannelMessage({
314
+ channelPda,
315
+ message: "Private discussion",
316
+ channelKey: channelKey,
317
+ keyVersion: 0,
318
+ // no senderServerMemberPda needed for private channels
319
+ });
320
+ ```
321
+
322
+ **Key difference:** Public channels require `senderServerMemberPda` — the program verifies you're a server member before accepting the message.
323
+
324
+ ### Read channel messages
325
+
326
+ ```typescript
327
+ const messages = await messenger.readChannelMessages(channelId, channelKey, {
328
+ limit: 50,
329
+ since: 1710000000, // unix timestamp
330
+ });
331
+
332
+ for (const msg of messages) {
333
+ console.log(`[${msg.sender}] ${msg.text} (key v${msg.keyVersion})`);
334
+ }
335
+ ```
336
+
337
+ ### Listen for channel messages in real-time
338
+
339
+ ```typescript
340
+ const unsubscribe = await messenger.listenChannel(channelPda, channelKey, (msg) => {
341
+ console.log(`${msg.sender}: ${msg.text}`);
342
+ });
343
+ ```
344
+
345
+ ### Add members to a private channel
346
+
347
+ **When:** Inviting new people to an existing private channel. Creator only.
348
+
349
+ ```typescript
350
+ // New members get ALL historical key versions (Slack-style full history)
351
+ await messenger.addChannelMembers(channelPda, channelId, ["NewMember"], [allChannelKeys]);
352
+ ```
353
+
354
+ ### Remove members from a private channel
355
+
356
+ **When:** Kicking someone. Rotates the channel key. Creator only.
357
+
358
+ ```typescript
359
+ const { newChannelPda } = await messenger.removeChannelMembers(
360
+ channelId,
361
+ currentVersion, // current channel version (from channel account)
362
+ ["RemovedMember"], // who to remove
363
+ ["Remaining1", "Remaining2"], // who stays
364
+ );
365
+ // ⚠️ Channel PDA changes! Old PDA is closed, new one at version+1
366
+ ```
367
+
368
+ ### Close a channel
369
+
370
+ ```typescript
371
+ await messenger.closeChannel(channelPda, channelId, memberAddresses);
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Discovery
377
+
378
+ **The key promise:** With just your wallet key + encryption key, you can discover everything you're part of — no backend needed.
379
+
380
+ ### Find all my servers
381
+
382
+ ```typescript
383
+ const servers = await messenger.getMyServers();
384
+ for (const { server, account } of servers) {
385
+ const roleName = ["Member", "Admin", "Owner"][account.role];
386
+ console.log(`${server.name} — ${roleName} (joined ${new Date(account.joinedAt * 1000).toISOString()})`);
387
+ }
388
+ ```
389
+
390
+ ### Find all my private channels
391
+
392
+ ```typescript
393
+ const channels = await messenger.getMyChannels();
394
+ for (const { channel, account } of channels) {
395
+ if (channel) {
396
+ const type = channel.channelType === 0 ? "Private" : "Public";
397
+ console.log(`Channel ${channel.channelId} (${type}, ${channel.members.length} members)`);
398
+ }
399
+ }
400
+ ```
401
+
402
+ ### Find all channels in a server
403
+
404
+ ```typescript
405
+ const channels = await messenger.getServerChannels(serverId);
406
+ for (const { account } of channels) {
407
+ const type = account.channelType === 0 ? "Private" : "Public";
408
+ console.log(`${account.channelId} — ${type}, v${account.version}`);
409
+ }
410
+ ```
411
+
412
+ ### Find all members of a server
413
+
414
+ ```typescript
415
+ const members = await messenger.getServerMembers(serverId);
416
+ for (const { account } of members) {
417
+ const roleName = ["Member", "Admin", "Owner"][account.role];
418
+ console.log(`${account.member} — ${roleName}`);
419
+ }
420
+ ```
421
+
422
+ ### Get a specific server/channel/member
423
+
424
+ ```typescript
425
+ const server = await messenger.getServer(serverId);
426
+ const channel = await messenger.getChannel(channelId, version);
427
+ const myServerMember = await messenger.getServerMember(serverId);
428
+ const myChannelMember = await messenger.getChannelMember(channelId);
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Key Management
434
+
435
+ The encryption model uses **shared symmetric keys** wrapped per-member.
436
+
437
+ ### How it works
438
+
439
+ 1. **Server key**: One symmetric key per server version. Stored wrapped in each ServerMember's `keyWraps`.
440
+ 2. **Channel key**: One symmetric key per channel version (private channels only). Stored in ChannelMember's `keyWraps`.
441
+ 3. **Key rotation**: When a member is removed (or leaves), a NEW key is generated and wrapped for all remaining members. The old key still exists in their records (for reading old messages).
442
+ 4. **`keyVersion`**: When sending a message, you specify which key version you used. Readers use this to find the right key wrap to decrypt.
443
+
444
+ ### Unwrap a key
445
+
446
+ ```typescript
447
+ // Get your ServerMember or ChannelMember account
448
+ const myMember = await messenger.getServerMember(serverId);
449
+
450
+ // Unwrap the latest key
451
+ const latestWrap = myMember.keyWraps[myMember.keyWraps.length - 1];
452
+ const key = messenger.unwrapKey(
453
+ latestWrap.wrappedKey,
454
+ latestWrap.nonce,
455
+ wrapperEncryptionPubkey, // encryption pubkey of whoever wrapped it (admin/creator)
456
+ );
457
+ ```
458
+
459
+ ### Who wrapped the key?
460
+
461
+ The wrapper is whoever called `inviteToServer`, `createServer`, `removeServerMember`, or `leaveServer`. You need their **encryption public key** (from the EncryptionRegistry) to unwrap.
462
+
463
+ For key wraps created by different people at different times, you may need to try multiple encryption pubkeys. In practice, most servers have one admin who does all invites.
464
+
465
+ ### Key version timeline example
466
+
467
+ ```
468
+ v0: Server created with key K0 (wrapped for owner)
469
+ → invite member B (K0 wrapped for B)
470
+ → invite member C (K0 wrapped for C)
471
+ v1: Member C removed → new key K1 (wrapped for owner + B)
472
+ → C can still read v0 messages, not v1+
473
+ v2: Member B leaves → new key K2 (wrapped for owner only)
474
+ → B can still read v0-v1 messages, not v2+
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Account Deserialization
480
+
481
+ For custom integrations, you can deserialize raw account data directly:
482
+
483
+ ```typescript
484
+ import {
485
+ deserializeServer,
486
+ deserializeServerMember,
487
+ deserializeChannel,
488
+ deserializeChannelMember,
489
+ deserializeEncryptionRegistry,
490
+ deserializePlatformConfig,
491
+ ACCOUNT_DISCRIMINATORS,
492
+ } from "solana-messenger-sdk";
493
+
494
+ // From raw RPC data
495
+ const accountInfo = await rpc.getAccountInfo(address(pda), { encoding: "base64" }).send();
496
+ const data = Buffer.from(accountInfo.value.data[0], "base64");
497
+ const server = deserializeServer(new Uint8Array(data));
498
+ ```
499
+
500
+ ### Account layouts
501
+
502
+ | Account | PDA Seeds | Key Fields |
503
+ |---------|-----------|------------|
504
+ | `Server` | `["server", serverId]` | serverId, owner, name |
505
+ | `ServerMember` | `["server_member", serverId, member]` | serverId, member, role, keyWraps |
506
+ | `Channel` | `["channel", channelId, versionBytes]` | channelId, serverId?, creator, version, channelType, members |
507
+ | `ChannelMember` | `["channel_member", channelId, member]` | channelId, member, keyWraps |
508
+ | `EncryptionRegistry` | `["messenger", owner]` | owner, encryptionKey, minFee |
509
+ | `PlatformConfig` | `["config"]` | authority, feeVault, protocolFee |
510
+
511
+ ---
512
+
513
+ ## Common Patterns
514
+
515
+ ### Pattern: "Show me everything" (first login / new device)
516
+
517
+ ```typescript
518
+ await messenger.init();
519
+
520
+ // 1. Find all servers
521
+ const servers = await messenger.getMyServers();
522
+
523
+ // 2. For each server, find channels
524
+ for (const { server, account } of servers) {
525
+ const channels = await messenger.getServerChannels(server.serverId);
526
+ console.log(`Server: ${server.name} (${channels.length} channels)`);
527
+ }
528
+
529
+ // 3. Find standalone group DMs
530
+ const myChannels = await messenger.getMyChannels();
531
+ const groupDMs = myChannels.filter(c => c.channel && !c.channel.serverId);
532
+
533
+ // 4. DM history
534
+ const dms = await messenger.read({ limit: 50 });
535
+ ```
536
+
537
+ ### Pattern: Full server setup
538
+
539
+ ```typescript
540
+ // 1. Create server
541
+ const { serverId } = await messenger.createServer("Acme Corp");
542
+
543
+ // 2. Create channels
544
+ await messenger.createChannel({ serverId, channelType: ChannelType.Public, members: [] }); // #general
545
+ await messenger.createChannel({ serverId, channelType: ChannelType.Private, members: [myAddr, cofounderAddr] }); // #founders
546
+
547
+ // 3. Invite team
548
+ const myMember = await messenger.getServerMember(serverId);
549
+ const serverKey = messenger.unwrapKey(myMember.keyWraps[0].wrappedKey, myMember.keyWraps[0].nonce, myEncPubkey);
550
+ await messenger.inviteToServer(serverId, "TeamMember1", [serverKey]);
551
+ await messenger.inviteToServer(serverId, "TeamMember2", [serverKey]);
552
+ ```
553
+
554
+ ### Pattern: Offboarding a member
555
+
556
+ ```typescript
557
+ // 1. Remove from server (rotates server key → public channels secured)
558
+ const members = await messenger.getServerMembers(serverId);
559
+ const remaining = members.filter(m => m.account.member !== removedAddr).map(m => m.account.member);
560
+ await messenger.removeServerMember(serverId, removedAddr, remaining);
561
+
562
+ // 2. Remove from private channels (must do separately!)
563
+ const channels = await messenger.getServerChannels(serverId);
564
+ for (const { account: ch } of channels) {
565
+ if (ch.channelType === 0 && ch.members.includes(removedAddr)) {
566
+ const chRemaining = ch.members.filter(m => m !== removedAddr);
567
+ await messenger.removeChannelMembers(ch.channelId, ch.version, [removedAddr], chRemaining);
568
+ }
569
+ }
570
+ ```
571
+
572
+ ⚠️ **Important:** Removing from a server does NOT automatically remove from private channels. You must do both.
573
+
574
+ ---
575
+
576
+ ## Architecture Reference
577
+
578
+ ### Encryption
579
+
580
+ | Message Type | Algorithm | Key Source |
581
+ |---|---|---|
582
+ | DM | NaCl box (X25519-XSalsa20-Poly1305) | Diffie-Hellman shared secret |
583
+ | Public channel | NaCl secretbox (XSalsa20-Poly1305) | Server key (from ServerMember.keyWraps) |
584
+ | Private channel | NaCl secretbox (XSalsa20-Poly1305) | Channel key (from ChannelMember.keyWraps) |
585
+
586
+ ### Roles
587
+
588
+ | Value | Role | Can do |
589
+ |---|---|---|
590
+ | 0 | Member | Send messages |
591
+ | 1 | Admin | Invite/remove members |
592
+ | 2 | Owner | Everything + cannot be removed |
593
+
594
+ ### Events (on-chain)
595
+
596
+ Messages are emitted as events (not stored in accounts):
597
+
598
+ ```typescript
599
+ // DM event
600
+ { sender, recipient, ciphertext, nonce, timestamp }
601
+
602
+ // Channel message event
603
+ { sender, channelId, version, keyVersion, ciphertext, nonce, timestamp }
604
+ ```
605
+
606
+ ### Security model
607
+
608
+ - **Forward secrecy**: Key rotation on removal AND voluntary leave
609
+ - **Full history**: New members get all historical keys
610
+ - **Public channel auth**: Sender's ServerMember PDA verified on-chain
611
+ - **Server channel auth**: Creator's ServerMember PDA required for channel creation
612
+ - **Admin verification**: PDA derivation check prevents spoofing
613
+ - **On-chain immutability**: Messages are permanent events — protocol cannot enforce deletion
614
+ - **Custody separation**: Signing key (A) ≠ encryption key (B) — custodial wallets can't read messages
615
+
616
+ ### Costs
617
+
618
+ | Action | Cost |
619
+ |--------|------|
620
+ | Send DM | ~5000 lamports + protocol fee + recipient min_fee |
621
+ | Send channel message | ~5000 lamports + protocol fee |
622
+ | Register | ~0.001 SOL rent (reclaimable) |
623
+ | Create server | ~0.001 SOL rent |
624
+ | Create channel | ~0.06 SOL rent (reclaimable) |
625
+ | ServerMember | ~0.002 SOL rent (reclaimable) |
626
+ | ChannelMember | ~0.002 SOL rent (reclaimable) |
627
+
628
+ All rent is reclaimable when accounts are closed.