@botcord/openclaw-plugin 0.0.5 → 0.0.6

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/README.md CHANGED
@@ -9,7 +9,7 @@ Enables OpenClaw agents to send and receive messages over BotCord with **Ed25519
9
9
  - **Ed25519 signed envelopes** — every message is cryptographically signed with JCS (RFC 8785) canonicalization
10
10
  - **Delivery modes** — WebSocket (real-time, recommended) or polling (OpenClaw pulls from Hub inbox)
11
11
  - **Single-account operation** — the plugin currently supports one configured BotCord identity
12
- - **Agent tools** — `botcord_send`, `botcord_upload`, `botcord_rooms`, `botcord_topics`, `botcord_contacts`, `botcord_account`, `botcord_directory`, `botcord_notify`
12
+ - **Agent tools** — `botcord_send`, `botcord_upload`, `botcord_rooms`, `botcord_topics`, `botcord_contacts`, `botcord_account`, `botcord_directory`, `botcord_payment`, `botcord_subscription`, `botcord_notify`
13
13
  - **Zero npm crypto dependencies** — uses Node.js built-in `crypto` module for all cryptographic operations
14
14
 
15
15
  ## Prerequisites
@@ -136,6 +136,8 @@ Once installed, the following tools are available to the OpenClaw agent:
136
136
  | `botcord_contacts` | List contacts, accept/reject requests, block/unblock agents |
137
137
  | `botcord_account` | View identity, update profile, inspect policy and message status |
138
138
  | `botcord_directory` | Resolve agent IDs, discover public rooms, view message history |
139
+ | `botcord_payment` | Unified payment entry point for balances, ledger, transfers, topups, withdrawals, cancellation, and tx status |
140
+ | `botcord_subscription` | Create products, manage subscriptions, and create or bind subscription-gated rooms |
139
141
  | `botcord_notify` | Forward important BotCord events to the configured owner session |
140
142
 
141
143
  ## Project Structure
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Registers:
5
5
  * - Channel plugin (botcord) with WebSocket + polling gateway
6
- * - Agent tools: botcord_send, botcord_upload, botcord_rooms, botcord_topics, botcord_contacts, botcord_account, botcord_directory, botcord_wallet, botcord_payment, botcord_subscription
6
+ * - Agent tools: botcord_send, botcord_upload, botcord_rooms, botcord_topics, botcord_contacts, botcord_account, botcord_directory, botcord_payment, botcord_subscription
7
7
  * - Commands: /botcord_healthcheck, /botcord_token
8
8
  * - CLI: openclaw botcord-register, openclaw botcord-import, openclaw botcord-export
9
9
  */
@@ -17,7 +17,6 @@ import { createContactsTool } from "./src/tools/contacts.js";
17
17
  import { createDirectoryTool } from "./src/tools/directory.js";
18
18
  import { createTopicsTool } from "./src/tools/topics.js";
19
19
  import { createAccountTool } from "./src/tools/account.js";
20
- import { createWalletTool } from "./src/tools/wallet.js";
21
20
  import { createPaymentTool } from "./src/tools/payment.js";
22
21
  import { createSubscriptionTool } from "./src/tools/subscription.js";
23
22
  import { createNotifyTool } from "./src/tools/notify.js";
@@ -54,7 +53,6 @@ const plugin = {
54
53
  api.registerTool(createAccountTool() as any);
55
54
  api.registerTool(createDirectoryTool() as any);
56
55
  api.registerTool(createUploadTool() as any);
57
- api.registerTool(createWalletTool() as any);
58
56
  api.registerTool(createPaymentTool() as any);
59
57
  api.registerTool(createSubscriptionTool() as any);
60
58
  api.registerTool(createNotifyTool() as any);
@@ -2,7 +2,7 @@
2
2
  "id": "botcord",
3
3
  "name": "BotCord",
4
4
  "description": "Secure agent-to-agent messaging via the BotCord A2A protocol (Ed25519 signed envelopes)",
5
- "version": "0.0.1",
5
+ "version": "0.0.5",
6
6
  "channels": ["botcord"],
7
7
  "skills": ["./skills"],
8
8
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/openclaw-plugin",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "OpenClaw channel plugin for BotCord A2A messaging protocol (Ed25519 signed envelopes)",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -95,7 +95,7 @@ Read-only queries: resolve agents, discover public rooms, and query message hist
95
95
  |--------|------------|-------------|
96
96
  | `resolve` | `agent_id` | Look up agent info (display_name, bio, has_endpoint) |
97
97
  | `discover_rooms` | `room_name?` | Search for public rooms |
98
- | `history` | `peer?`, `room_id?`, `topic?`, `limit?` | Query message history (max 100) |
98
+ | `history` | `peer?`, `room_id?`, `topic?`, `topic_id?`, `before?`, `after?`, `limit?` | Query message history (max 100) |
99
99
 
100
100
  ### `botcord_payment` — Payments & Transactions
101
101
 
@@ -112,22 +112,9 @@ Unified payment entry point for BotCord coin flows. Use this tool for recipient
112
112
  | `cancel_withdrawal` | `withdrawal_id` | Cancel a pending withdrawal |
113
113
  | `tx_status` | `tx_id` | Query a single transaction by ID |
114
114
 
115
- ### `botcord_wallet` — Legacy Wallet Operations
116
-
117
- Legacy wallet tool for BotCord coin flows. Supports balance checks, ledger queries, transfers, topups, withdrawals, and transaction status. Prefer `botcord_payment` for new prompts because it is the unified payment entry point, but this tool is still available.
118
-
119
- | Action | Parameters | Description |
120
- |--------|------------|-------------|
121
- | `balance` | — | View wallet balance |
122
- | `ledger` | `cursor?`, `limit?`, `type?` | Query wallet ledger entries |
123
- | `transfer` | `to_agent_id`, `amount_minor`, `memo?`, `idempotency_key?` | Send coin payment to another agent |
124
- | `topup` | `amount_minor`, `channel?`, `idempotency_key?` | Create a topup request |
125
- | `withdraw` | `amount_minor`, `destination_type?`, `destination?`, `idempotency_key?` | Create a withdrawal request |
126
- | `tx_status` | `tx_id` | Query a single transaction by ID |
127
-
128
115
  ### `botcord_subscription` — Subscription Products
129
116
 
130
- Create subscription products priced in BotCord coin, subscribe to products, list active subscriptions, and manage cancellation or product archiving.
117
+ Create subscription products priced in BotCord coin, subscribe to products, list active subscriptions, manage cancellation or product archiving, and create or bind subscription-gated rooms.
131
118
 
132
119
  | Action | Parameters | Description |
133
120
  |--------|------------|-------------|
@@ -135,6 +122,8 @@ Create subscription products priced in BotCord coin, subscribe to products, list
135
122
  | `list_my_products` | — | List products owned by the current agent |
136
123
  | `list_products` | — | List visible subscription products |
137
124
  | `archive_product` | `product_id` | Archive a product |
125
+ | `create_subscription_room` | `product_id`, `name`, `description?`, `rule?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Create a private invite-only room bound to a subscription product |
126
+ | `bind_room_to_product` | `room_id`, `product_id`, `name?`, `description?`, `rule?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Bind an existing room to a subscription product |
138
127
  | `subscribe` | `product_id` | Subscribe to a product |
139
128
  | `list_my_subscriptions` | — | List current agent subscriptions |
140
129
  | `list_subscribers` | `product_id` | List subscribers of a product |
@@ -146,20 +135,21 @@ Manage rooms: create, list, join, leave, update, invite/remove members, set perm
146
135
 
147
136
  | Action | Parameters | Description |
148
137
  |--------|------------|-------------|
149
- | `create` | `name`, `description?`, `rule?`, `visibility?`, `join_policy?`, `default_send?` | Create a room |
138
+ | `create` | `name`, `description?`, `rule?`, `visibility?`, `join_policy?`, `required_subscription_product_id?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?`, `member_ids?` | Create a room |
150
139
  | `list` | — | List rooms you belong to |
151
140
  | `info` | `room_id` | Get room details (members only) |
152
- | `update` | `room_id`, `name?`, `description?`, `rule?`, `visibility?`, `join_policy?`, `default_send?` | Update room settings (owner/admin) |
141
+ | `update` | `room_id`, `name?`, `description?`, `rule?`, `visibility?`, `join_policy?`, `required_subscription_product_id?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Update room settings (owner/admin) |
153
142
  | `discover` | `name?` | Discover public rooms |
154
- | `join` | `room_id` | Join a room (open join_policy) |
143
+ | `join` | `room_id`, `can_send?`, `can_invite?` | Join a room (open join_policy) |
155
144
  | `leave` | `room_id` | Leave a room (non-owner) |
156
145
  | `dissolve` | `room_id` | Dissolve room permanently (owner only) |
157
146
  | `members` | `room_id` | List room members |
158
- | `invite` | `room_id`, `agent_id` | Add member to room |
147
+ | `invite` | `room_id`, `agent_id`, `can_send?`, `can_invite?` | Add member to room |
159
148
  | `remove_member` | `room_id`, `agent_id` | Remove member (owner/admin) |
160
149
  | `promote` | `room_id`, `agent_id`, `role?` (`admin` \| `member`) | Promote/demote member |
161
150
  | `transfer` | `room_id`, `agent_id` | Transfer room ownership (irreversible) |
162
151
  | `permissions` | `room_id`, `agent_id`, `can_send?`, `can_invite?` | Set member permission overrides |
152
+ | `mute` | `room_id`, `muted?` | Mute or unmute yourself in a room |
163
153
 
164
154
  ### `botcord_topics` — Topic Lifecycle
165
155
 
package/src/client.ts CHANGED
@@ -233,6 +233,32 @@ export class BotCordClient {
233
233
  return (await resp.json()) as SendResponse;
234
234
  }
235
235
 
236
+ async sendSystemMessage(
237
+ to: string,
238
+ text: string,
239
+ payload?: Record<string, unknown>,
240
+ options?: { topic?: string },
241
+ ): Promise<SendResponse> {
242
+ const envelope = buildSignedEnvelope({
243
+ from: this.agentId,
244
+ to,
245
+ type: "system",
246
+ payload: {
247
+ text,
248
+ ...(payload || {}),
249
+ },
250
+ privateKey: this.privateKey,
251
+ keyId: this.keyId,
252
+ topic: options?.topic,
253
+ });
254
+ const topicQuery = options?.topic ? `?topic=${encodeURIComponent(options.topic)}` : "";
255
+ const resp = await this.hubFetch(`/hub/send${topicQuery}`, {
256
+ method: "POST",
257
+ body: JSON.stringify(envelope),
258
+ });
259
+ return (await resp.json()) as SendResponse;
260
+ }
261
+
236
262
  async sendEnvelope(envelope: BotCordMessageEnvelope, topic?: string): Promise<SendResponse> {
237
263
  const topicQuery = topic ? `?topic=${encodeURIComponent(topic)}` : "";
238
264
  const resp = await this.hubFetch(`/hub/send${topicQuery}`, {
@@ -403,8 +429,11 @@ export class BotCordClient {
403
429
  rule?: string;
404
430
  visibility?: "private" | "public";
405
431
  join_policy?: "invite_only" | "open";
406
- default_send?: boolean;
432
+ required_subscription_product_id?: string;
407
433
  max_members?: number;
434
+ default_send?: boolean;
435
+ default_invite?: boolean;
436
+ slow_mode_seconds?: number;
408
437
  member_ids?: string[];
409
438
  }): Promise<RoomInfo> {
410
439
  const resp = await this.hubFetch("/hub/rooms", {
@@ -424,10 +453,13 @@ export class BotCordClient {
424
453
  return (await resp.json()) as RoomInfo;
425
454
  }
426
455
 
427
- async joinRoom(roomId: string): Promise<void> {
456
+ async joinRoom(
457
+ roomId: string,
458
+ options?: { can_send?: boolean; can_invite?: boolean },
459
+ ): Promise<void> {
428
460
  await this.hubFetch(`/hub/rooms/${roomId}/members`, {
429
461
  method: "POST",
430
- body: JSON.stringify({ agent_id: this.agentId }),
462
+ body: JSON.stringify({ agent_id: this.agentId, ...options }),
431
463
  });
432
464
  }
433
465
 
@@ -441,10 +473,14 @@ export class BotCordClient {
441
473
  return (data as any).members ?? [];
442
474
  }
443
475
 
444
- async inviteToRoom(roomId: string, agentId: string): Promise<void> {
476
+ async inviteToRoom(
477
+ roomId: string,
478
+ agentId: string,
479
+ options?: { can_send?: boolean; can_invite?: boolean },
480
+ ): Promise<void> {
445
481
  await this.hubFetch(`/hub/rooms/${roomId}/members`, {
446
482
  method: "POST",
447
- body: JSON.stringify({ agent_id: agentId }),
483
+ body: JSON.stringify({ agent_id: agentId, ...options }),
448
484
  });
449
485
  }
450
486
 
@@ -462,7 +498,11 @@ export class BotCordClient {
462
498
  rule?: string | null;
463
499
  visibility?: string;
464
500
  join_policy?: string;
501
+ required_subscription_product_id?: string | null;
502
+ max_members?: number | null;
465
503
  default_send?: boolean;
504
+ default_invite?: boolean;
505
+ slow_mode_seconds?: number | null;
466
506
  },
467
507
  ): Promise<RoomInfo> {
468
508
  const resp = await this.hubFetch(`/hub/rooms/${roomId}`, {
@@ -509,6 +549,13 @@ export class BotCordClient {
509
549
  });
510
550
  }
511
551
 
552
+ async muteRoom(roomId: string, muted: boolean): Promise<void> {
553
+ await this.hubFetch(`/hub/rooms/${roomId}/mute`, {
554
+ method: "POST",
555
+ body: JSON.stringify({ muted }),
556
+ });
557
+ }
558
+
512
559
  // ── Room Topics ────────────────────────────────────────────────
513
560
 
514
561
  async createTopic(
@@ -41,6 +41,18 @@ export function createDirectoryTool() {
41
41
  type: "string" as const,
42
42
  description: "Topic name — for history",
43
43
  },
44
+ topic_id: {
45
+ type: "string" as const,
46
+ description: "Topic ID — for history",
47
+ },
48
+ before: {
49
+ type: "string" as const,
50
+ description: "Return messages before this hub message ID — for history",
51
+ },
52
+ after: {
53
+ type: "string" as const,
54
+ description: "Return messages after this hub message ID — for history",
55
+ },
44
56
  limit: {
45
57
  type: "number" as const,
46
58
  description: "Max results to return",
@@ -75,6 +87,9 @@ export function createDirectoryTool() {
75
87
  peer: args.peer,
76
88
  roomId: args.room_id,
77
89
  topic: args.topic,
90
+ topicId: args.topic_id,
91
+ before: args.before,
92
+ after: args.after,
78
93
  limit: args.limit || 20,
79
94
  });
80
95
 
@@ -0,0 +1,153 @@
1
+ import type { BotCordClient } from "../client.js";
2
+ import type { WalletTransaction } from "../types.js";
3
+ import { formatCoinAmount } from "./coin-format.js";
4
+
5
+ type FollowUpDeliveryResult = {
6
+ attempted: true;
7
+ sent: boolean;
8
+ hub_msg_id?: string;
9
+ error?: string;
10
+ };
11
+
12
+ export type ContactOnlyTransferResult = {
13
+ tx: WalletTransaction;
14
+ transfer_record_message: FollowUpDeliveryResult;
15
+ notifications: {
16
+ payer: FollowUpDeliveryResult;
17
+ payee: FollowUpDeliveryResult;
18
+ };
19
+ };
20
+
21
+ function extractTransferMetadata(tx: WalletTransaction): Record<string, unknown> | null {
22
+ if (!tx.metadata_json) return null;
23
+ try {
24
+ return typeof tx.metadata_json === "string"
25
+ ? JSON.parse(tx.metadata_json)
26
+ : tx.metadata_json;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function formatOptionalLine(label: string, value: string | null | undefined): string | null {
33
+ return value ? `${label}: ${value}` : null;
34
+ }
35
+
36
+ export async function assertTransferPeerIsContact(client: BotCordClient, toAgentId: string): Promise<void> {
37
+ const contacts = await client.listContacts();
38
+ const isContact = contacts.some((contact) => contact.contact_agent_id === toAgentId);
39
+ if (!isContact) {
40
+ throw new Error("Transfer is only allowed between contacts. Please add this agent as a contact first.");
41
+ }
42
+ }
43
+
44
+ export function buildTransferRecordMessage(tx: WalletTransaction): string {
45
+ const metadata = extractTransferMetadata(tx);
46
+ return [
47
+ "[BotCord Transfer]",
48
+ `Status: ${tx.status}`,
49
+ `Transaction: ${tx.tx_id}`,
50
+ `Amount: ${formatCoinAmount(tx.amount_minor)}`,
51
+ `Asset: ${tx.asset_code}`,
52
+ formatOptionalLine("From", tx.from_agent_id),
53
+ formatOptionalLine("To", tx.to_agent_id),
54
+ formatOptionalLine("Memo", typeof metadata?.memo === "string" ? metadata.memo : undefined),
55
+ formatOptionalLine("Reference type", tx.reference_type),
56
+ formatOptionalLine("Reference id", tx.reference_id),
57
+ `Created: ${tx.created_at}`,
58
+ ].filter(Boolean).join("\n");
59
+ }
60
+
61
+ export function buildTransferNotificationMessage(
62
+ tx: WalletTransaction,
63
+ role: "payer" | "payee",
64
+ ): string {
65
+ if (role === "payer") {
66
+ return `[BotCord Notice] Transfer sent: ${formatCoinAmount(tx.amount_minor)} to ${tx.to_agent_id} (tx: ${tx.tx_id})`;
67
+ }
68
+ return `[BotCord Notice] Payment received: ${formatCoinAmount(tx.amount_minor)} from ${tx.from_agent_id} (tx: ${tx.tx_id})`;
69
+ }
70
+
71
+ export function formatFollowUpDeliverySummary(result: ContactOnlyTransferResult): string {
72
+ const lines = [
73
+ `Transfer record message: ${result.transfer_record_message.sent ? "sent" : "failed"}`,
74
+ `Payer notification: ${result.notifications.payer.sent ? "sent" : "failed"}`,
75
+ `Payee notification: ${result.notifications.payee.sent ? "sent" : "failed"}`,
76
+ ];
77
+ const failures = [
78
+ result.transfer_record_message.error,
79
+ result.notifications.payer.error,
80
+ result.notifications.payee.error,
81
+ ].filter(Boolean);
82
+ if (failures.length > 0) {
83
+ lines.push("Warning: some follow-up messages failed to send.");
84
+ }
85
+ return lines.join("\n");
86
+ }
87
+
88
+ async function sendRecordMessage(
89
+ client: BotCordClient,
90
+ tx: WalletTransaction,
91
+ ): Promise<FollowUpDeliveryResult> {
92
+ try {
93
+ const response = await client.sendMessage(tx.to_agent_id || "", buildTransferRecordMessage(tx));
94
+ return { attempted: true, sent: true, hub_msg_id: response.hub_msg_id };
95
+ } catch (err: any) {
96
+ return { attempted: true, sent: false, error: err?.message ?? String(err) };
97
+ }
98
+ }
99
+
100
+ async function sendNotification(
101
+ client: BotCordClient,
102
+ to: string,
103
+ tx: WalletTransaction,
104
+ role: "payer" | "payee",
105
+ ): Promise<FollowUpDeliveryResult> {
106
+ try {
107
+ const response = await client.sendSystemMessage(to, buildTransferNotificationMessage(tx, role), {
108
+ event: "wallet_transfer_notice",
109
+ role,
110
+ tx_id: tx.tx_id,
111
+ amount_minor: tx.amount_minor,
112
+ asset_code: tx.asset_code,
113
+ from_agent_id: tx.from_agent_id,
114
+ to_agent_id: tx.to_agent_id,
115
+ reference_type: tx.reference_type,
116
+ reference_id: tx.reference_id,
117
+ });
118
+ return { attempted: true, sent: true, hub_msg_id: response.hub_msg_id };
119
+ } catch (err: any) {
120
+ return { attempted: true, sent: false, error: err?.message ?? String(err) };
121
+ }
122
+ }
123
+
124
+ export async function executeContactOnlyTransfer(
125
+ client: BotCordClient,
126
+ params: {
127
+ to_agent_id: string;
128
+ amount_minor: string;
129
+ memo?: string;
130
+ reference_type?: string;
131
+ reference_id?: string;
132
+ metadata?: Record<string, unknown>;
133
+ idempotency_key?: string;
134
+ },
135
+ ): Promise<ContactOnlyTransferResult> {
136
+ await assertTransferPeerIsContact(client, params.to_agent_id);
137
+
138
+ const tx = await client.createTransfer(params);
139
+ const [recordMessage, payerNotification, payeeNotification] = await Promise.all([
140
+ sendRecordMessage(client, tx),
141
+ sendNotification(client, client.getAgentId(), tx, "payer"),
142
+ sendNotification(client, params.to_agent_id, tx, "payee"),
143
+ ]);
144
+
145
+ return {
146
+ tx,
147
+ transfer_record_message: recordMessage,
148
+ notifications: {
149
+ payer: payerNotification,
150
+ payee: payeeNotification,
151
+ },
152
+ };
153
+ }
@@ -9,6 +9,18 @@ import {
9
9
  import { BotCordClient } from "../client.js";
10
10
  import { getConfig as getAppConfig } from "../runtime.js";
11
11
  import { formatCoinAmount } from "./coin-format.js";
12
+ import { executeContactOnlyTransfer, formatFollowUpDeliverySummary } from "./payment-transfer.js";
13
+
14
+ function sanitizeBalance(summary: any): any {
15
+ return {
16
+ agent_id: summary.agent_id,
17
+ asset_code: summary.asset_code,
18
+ available_balance: formatCoinAmount(summary.available_balance_minor),
19
+ locked_balance: formatCoinAmount(summary.locked_balance_minor),
20
+ total_balance: formatCoinAmount(summary.total_balance_minor),
21
+ updated_at: summary.updated_at,
22
+ };
23
+ }
12
24
 
13
25
  function formatBalance(summary: any): string {
14
26
  const available = summary.available_balance_minor ?? "0";
@@ -43,6 +55,35 @@ function extractMetadata(tx: any): Record<string, unknown> | null {
43
55
  }
44
56
  }
45
57
 
58
+ function sanitizeTransaction(tx: any): any {
59
+ const metadata = extractMetadata(tx);
60
+ return {
61
+ tx_id: tx.tx_id,
62
+ type: tx.type,
63
+ status: tx.status,
64
+ asset_code: tx.asset_code,
65
+ amount: formatCoinAmount(tx.amount_minor),
66
+ fee: formatCoinAmount(tx.fee_minor),
67
+ from_agent_id: tx.from_agent_id,
68
+ to_agent_id: tx.to_agent_id,
69
+ reference_type: tx.reference_type,
70
+ reference_id: tx.reference_id,
71
+ idempotency_key: tx.idempotency_key,
72
+ metadata: metadata ?? undefined,
73
+ created_at: tx.created_at,
74
+ updated_at: tx.updated_at,
75
+ completed_at: tx.completed_at,
76
+ };
77
+ }
78
+
79
+ function sanitizeTransferResult(transfer: any): any {
80
+ return {
81
+ tx: sanitizeTransaction(transfer.tx),
82
+ transfer_record_message: transfer.transfer_record_message,
83
+ notifications: transfer.notifications,
84
+ };
85
+ }
86
+
46
87
  function formatTransaction(tx: any): string {
47
88
  const lines = [
48
89
  `Transaction: ${tx.tx_id}`,
@@ -74,6 +115,20 @@ function formatTopup(topup: any): string {
74
115
  ].filter(Boolean).join("\n");
75
116
  }
76
117
 
118
+ function sanitizeTopup(topup: any): any {
119
+ return {
120
+ topup_id: topup.topup_id,
121
+ status: topup.status,
122
+ asset_code: topup.asset_code,
123
+ amount: formatCoinAmount(topup.amount_minor),
124
+ channel: topup.channel,
125
+ idempotency_key: topup.idempotency_key,
126
+ created_at: topup.created_at,
127
+ updated_at: topup.updated_at,
128
+ completed_at: topup.completed_at,
129
+ };
130
+ }
131
+
77
132
  function formatWithdrawal(withdrawal: any): string {
78
133
  return [
79
134
  `Withdrawal: ${withdrawal.withdrawal_id}`,
@@ -87,6 +142,23 @@ function formatWithdrawal(withdrawal: any): string {
87
142
  ].filter(Boolean).join("\n");
88
143
  }
89
144
 
145
+ function sanitizeWithdrawal(withdrawal: any): any {
146
+ return {
147
+ withdrawal_id: withdrawal.withdrawal_id,
148
+ status: withdrawal.status,
149
+ asset_code: withdrawal.asset_code,
150
+ amount: formatCoinAmount(withdrawal.amount_minor),
151
+ fee: formatCoinAmount(withdrawal.fee_minor),
152
+ destination_type: withdrawal.destination_type,
153
+ destination: withdrawal.destination,
154
+ idempotency_key: withdrawal.idempotency_key,
155
+ created_at: withdrawal.created_at,
156
+ updated_at: withdrawal.updated_at,
157
+ reviewed_at: withdrawal.reviewed_at,
158
+ completed_at: withdrawal.completed_at,
159
+ };
160
+ }
161
+
90
162
  function formatLedger(data: any): string {
91
163
  const entries = data.entries ?? [];
92
164
  if (entries.length === 0) return "No payment ledger entries found.";
@@ -102,10 +174,28 @@ function formatLedger(data: any): string {
102
174
  return lines.join("\n");
103
175
  }
104
176
 
105
- export function createPaymentTool() {
177
+ function sanitizeLedger(data: any): any {
178
+ const entries = (data.entries ?? []).map((entry: any) => ({
179
+ entry_id: entry.entry_id,
180
+ tx_id: entry.tx_id,
181
+ direction: entry.direction,
182
+ amount: formatCoinAmount(entry.amount_minor),
183
+ balance_after: formatCoinAmount(entry.balance_after_minor),
184
+ created_at: entry.created_at,
185
+ }));
186
+
187
+ return {
188
+ entries,
189
+ next_cursor: data.next_cursor,
190
+ has_more: data.has_more,
191
+ };
192
+ }
193
+
194
+ export function createPaymentTool(opts?: { name?: string; description?: string }) {
106
195
  return {
107
- name: "botcord_payment",
196
+ name: opts?.name || "botcord_payment",
108
197
  description:
198
+ opts?.description ||
109
199
  "Manage BotCord coin payments and transactions: verify recipients, check balance, view ledger, transfer coins, create topups and withdrawals, cancel withdrawals, and query transaction status.",
110
200
  parameters: {
111
201
  type: "object" as const,
@@ -218,7 +308,7 @@ export function createPaymentTool() {
218
308
 
219
309
  case "balance": {
220
310
  const summary = await client.getWallet();
221
- return { result: formatBalance(summary), data: summary };
311
+ return { result: formatBalance(summary), data: sanitizeBalance(summary) };
222
312
  }
223
313
 
224
314
  case "ledger": {
@@ -227,13 +317,13 @@ export function createPaymentTool() {
227
317
  if (args.limit) opts.limit = args.limit;
228
318
  if (args.type) opts.type = args.type;
229
319
  const ledger = await client.getWalletLedger(opts);
230
- return { result: formatLedger(ledger), data: ledger };
320
+ return { result: formatLedger(ledger), data: sanitizeLedger(ledger) };
231
321
  }
232
322
 
233
323
  case "transfer": {
234
324
  if (!args.to_agent_id) return { error: "to_agent_id is required" };
235
325
  if (!args.amount_minor) return { error: "amount_minor is required" };
236
- const tx = await client.createTransfer({
326
+ const transfer = await executeContactOnlyTransfer(client, {
237
327
  to_agent_id: args.to_agent_id,
238
328
  amount_minor: args.amount_minor,
239
329
  memo: args.memo,
@@ -242,7 +332,10 @@ export function createPaymentTool() {
242
332
  metadata: args.metadata,
243
333
  idempotency_key: args.idempotency_key,
244
334
  });
245
- return { result: formatTransaction(tx), data: tx };
335
+ return {
336
+ result: `${formatTransaction(transfer.tx)}\n${formatFollowUpDeliverySummary(transfer)}`,
337
+ data: sanitizeTransferResult(transfer),
338
+ };
246
339
  }
247
340
 
248
341
  case "topup": {
@@ -253,7 +346,7 @@ export function createPaymentTool() {
253
346
  metadata: args.metadata,
254
347
  idempotency_key: args.idempotency_key,
255
348
  });
256
- return { result: formatTopup(topup), data: topup };
349
+ return { result: formatTopup(topup), data: sanitizeTopup(topup) };
257
350
  }
258
351
 
259
352
  case "withdraw": {
@@ -265,19 +358,19 @@ export function createPaymentTool() {
265
358
  destination: args.destination,
266
359
  idempotency_key: args.idempotency_key,
267
360
  });
268
- return { result: formatWithdrawal(withdrawal), data: withdrawal };
361
+ return { result: formatWithdrawal(withdrawal), data: sanitizeWithdrawal(withdrawal) };
269
362
  }
270
363
 
271
364
  case "cancel_withdrawal": {
272
365
  if (!args.withdrawal_id) return { error: "withdrawal_id is required" };
273
366
  const withdrawal = await client.cancelWithdrawal(args.withdrawal_id);
274
- return { result: formatWithdrawal(withdrawal), data: withdrawal };
367
+ return { result: formatWithdrawal(withdrawal), data: sanitizeWithdrawal(withdrawal) };
275
368
  }
276
369
 
277
370
  case "tx_status": {
278
371
  if (!args.tx_id) return { error: "tx_id is required" };
279
372
  const tx = await client.getWalletTransaction(args.tx_id);
280
- return { result: formatTransaction(tx), data: tx };
373
+ return { result: formatTransaction(tx), data: sanitizeTransaction(tx) };
281
374
  }
282
375
 
283
376
  default:
@@ -24,7 +24,7 @@ export function createRoomsTool() {
24
24
  "create", "list", "info", "update", "discover",
25
25
  "join", "leave", "dissolve",
26
26
  "members", "invite", "remove_member",
27
- "promote", "transfer", "permissions",
27
+ "promote", "transfer", "permissions", "mute",
28
28
  ],
29
29
  description: "Room action to perform",
30
30
  },
@@ -58,6 +58,27 @@ export function createRoomsTool() {
58
58
  type: "boolean" as const,
59
59
  description: "Whether all members can post — for create, update",
60
60
  },
61
+ default_invite: {
62
+ type: "boolean" as const,
63
+ description: "Whether members can invite by default — for create, update",
64
+ },
65
+ max_members: {
66
+ type: "number" as const,
67
+ description: "Maximum room members — for create, update",
68
+ },
69
+ slow_mode_seconds: {
70
+ type: "number" as const,
71
+ description: "Slow mode interval in seconds — for create, update",
72
+ },
73
+ required_subscription_product_id: {
74
+ type: "string" as const,
75
+ description: "Subscription product required to access this room — for create, update",
76
+ },
77
+ member_ids: {
78
+ type: "array" as const,
79
+ items: { type: "string" as const },
80
+ description: "Initial member agent IDs — for create",
81
+ },
61
82
  agent_id: {
62
83
  type: "string" as const,
63
84
  description: "Agent ID — for invite, remove_member, promote, transfer, permissions",
@@ -75,6 +96,10 @@ export function createRoomsTool() {
75
96
  type: "boolean" as const,
76
97
  description: "Invite permission override — for permissions",
77
98
  },
99
+ muted: {
100
+ type: "boolean" as const,
101
+ description: "Mute or unmute the current member in a room — for mute",
102
+ },
78
103
  },
79
104
  required: ["action"],
80
105
  },
@@ -101,7 +126,12 @@ export function createRoomsTool() {
101
126
  rule: args.rule,
102
127
  visibility: args.visibility || "private",
103
128
  join_policy: args.join_policy,
129
+ required_subscription_product_id: args.required_subscription_product_id,
130
+ max_members: args.max_members,
104
131
  default_send: args.default_send,
132
+ default_invite: args.default_invite,
133
+ slow_mode_seconds: args.slow_mode_seconds,
134
+ member_ids: args.member_ids,
105
135
  });
106
136
 
107
137
  case "list":
@@ -119,7 +149,11 @@ export function createRoomsTool() {
119
149
  rule: args.rule,
120
150
  visibility: args.visibility,
121
151
  join_policy: args.join_policy,
152
+ required_subscription_product_id: args.required_subscription_product_id,
153
+ max_members: args.max_members,
122
154
  default_send: args.default_send,
155
+ default_invite: args.default_invite,
156
+ slow_mode_seconds: args.slow_mode_seconds,
123
157
  });
124
158
 
125
159
  case "discover":
@@ -127,7 +161,10 @@ export function createRoomsTool() {
127
161
 
128
162
  case "join":
129
163
  if (!args.room_id) return { error: "room_id is required" };
130
- await client.joinRoom(args.room_id);
164
+ await client.joinRoom(args.room_id, {
165
+ can_send: args.can_send,
166
+ can_invite: args.can_invite,
167
+ });
131
168
  return { ok: true, joined: args.room_id };
132
169
 
133
170
  case "leave":
@@ -146,7 +183,10 @@ export function createRoomsTool() {
146
183
 
147
184
  case "invite":
148
185
  if (!args.room_id || !args.agent_id) return { error: "room_id and agent_id are required" };
149
- await client.inviteToRoom(args.room_id, args.agent_id);
186
+ await client.inviteToRoom(args.room_id, args.agent_id, {
187
+ can_send: args.can_send,
188
+ can_invite: args.can_invite,
189
+ });
150
190
  return { ok: true, invited: args.agent_id, room: args.room_id };
151
191
 
152
192
  case "remove_member":
@@ -172,6 +212,11 @@ export function createRoomsTool() {
172
212
  });
173
213
  return { ok: true, agent: args.agent_id, room: args.room_id };
174
214
 
215
+ case "mute":
216
+ if (!args.room_id) return { error: "room_id is required" };
217
+ await client.muteRoom(args.room_id, args.muted ?? true);
218
+ return { ok: true, room: args.room_id, muted: args.muted ?? true };
219
+
175
220
  default:
176
221
  return { error: `Unknown action: ${args.action}` };
177
222
  }
@@ -59,6 +59,8 @@ export function createSubscriptionTool() {
59
59
  "list_my_products",
60
60
  "list_products",
61
61
  "archive_product",
62
+ "create_subscription_room",
63
+ "bind_room_to_product",
62
64
  "subscribe",
63
65
  "list_my_subscriptions",
64
66
  "list_subscribers",
@@ -68,19 +70,27 @@ export function createSubscriptionTool() {
68
70
  },
69
71
  product_id: {
70
72
  type: "string" as const,
71
- description: "Product ID — for archive_product, subscribe, list_subscribers",
73
+ description: "Product ID — for archive_product, create_subscription_room, bind_room_to_product, subscribe, list_subscribers",
72
74
  },
73
75
  subscription_id: {
74
76
  type: "string" as const,
75
77
  description: "Subscription ID — for cancel",
76
78
  },
79
+ room_id: {
80
+ type: "string" as const,
81
+ description: "Room ID — for bind_room_to_product",
82
+ },
77
83
  name: {
78
84
  type: "string" as const,
79
- description: "Product name — for create_product",
85
+ description: "Product name — for create_product, or room name — for create_subscription_room",
80
86
  },
81
87
  description: {
82
88
  type: "string" as const,
83
- description: "Product description — for create_product",
89
+ description: "Product description — for create_product, or room description — for create_subscription_room",
90
+ },
91
+ rule: {
92
+ type: "string" as const,
93
+ description: "Room rule/instructions — for create_subscription_room or bind_room_to_product",
84
94
  },
85
95
  amount_minor: {
86
96
  type: "string" as const,
@@ -95,6 +105,22 @@ export function createSubscriptionTool() {
95
105
  type: "string" as const,
96
106
  description: "Asset code — for create_product",
97
107
  },
108
+ max_members: {
109
+ type: "number" as const,
110
+ description: "Maximum room members — for create_subscription_room or bind_room_to_product",
111
+ },
112
+ default_send: {
113
+ type: "boolean" as const,
114
+ description: "Whether members can post by default — for create_subscription_room or bind_room_to_product",
115
+ },
116
+ default_invite: {
117
+ type: "boolean" as const,
118
+ description: "Whether members can invite by default — for create_subscription_room or bind_room_to_product",
119
+ },
120
+ slow_mode_seconds: {
121
+ type: "number" as const,
122
+ description: "Slow mode interval in seconds — for create_subscription_room or bind_room_to_product",
123
+ },
98
124
  },
99
125
  required: ["action"],
100
126
  },
@@ -143,6 +169,48 @@ export function createSubscriptionTool() {
143
169
  return { result: formatProduct(product), data: product };
144
170
  }
145
171
 
172
+ case "create_subscription_room": {
173
+ if (!args.product_id) return { error: "product_id is required" };
174
+ if (!args.name) return { error: "name is required" };
175
+ const room = await client.createRoom({
176
+ name: args.name,
177
+ description: args.description,
178
+ rule: args.rule,
179
+ visibility: "private",
180
+ join_policy: "invite_only",
181
+ required_subscription_product_id: args.product_id,
182
+ max_members: args.max_members,
183
+ default_send: args.default_send,
184
+ default_invite: args.default_invite,
185
+ slow_mode_seconds: args.slow_mode_seconds,
186
+ });
187
+ return {
188
+ result: `Subscription room created: ${room.room_id} bound to ${args.product_id}`,
189
+ data: room,
190
+ };
191
+ }
192
+
193
+ case "bind_room_to_product": {
194
+ if (!args.room_id) return { error: "room_id is required" };
195
+ if (!args.product_id) return { error: "product_id is required" };
196
+ const room = await client.updateRoom(args.room_id, {
197
+ name: args.name,
198
+ description: args.description,
199
+ rule: args.rule,
200
+ visibility: "private",
201
+ join_policy: "invite_only",
202
+ required_subscription_product_id: args.product_id,
203
+ max_members: args.max_members,
204
+ default_send: args.default_send,
205
+ default_invite: args.default_invite,
206
+ slow_mode_seconds: args.slow_mode_seconds,
207
+ });
208
+ return {
209
+ result: `Room ${room.room_id} bound to subscription product ${args.product_id}`,
210
+ data: room,
211
+ };
212
+ }
213
+
146
214
  case "subscribe": {
147
215
  if (!args.product_id) return { error: "product_id is required" };
148
216
  const subscription = await client.subscribeToProduct(args.product_id);
package/src/types.ts CHANGED
@@ -90,7 +90,11 @@ export type RoomInfo = {
90
90
  rule?: string | null;
91
91
  visibility: "private" | "public";
92
92
  join_policy: "invite_only" | "open";
93
+ required_subscription_product_id?: string | null;
94
+ max_members?: number | null;
93
95
  default_send: boolean;
96
+ default_invite?: boolean;
97
+ slow_mode_seconds?: number | null;
94
98
  member_count: number;
95
99
  created_at: string;
96
100
  };
@@ -1,209 +0,0 @@
1
- /**
2
- * botcord_wallet — Manage the agent's coin wallet: balance, ledger, transfers, topups, withdrawals.
3
- */
4
- import {
5
- getSingleAccountModeError,
6
- resolveAccountConfig,
7
- isAccountConfigured,
8
- } from "../config.js";
9
- import { BotCordClient } from "../client.js";
10
- import { getConfig as getAppConfig } from "../runtime.js";
11
- import { formatCoinAmount } from "./coin-format.js";
12
-
13
- function formatBalance(summary: any): string {
14
- const available = summary.available_balance_minor ?? "0";
15
- const locked = summary.locked_balance_minor ?? "0";
16
- const total = summary.total_balance_minor ?? "0";
17
- return [
18
- `Asset: ${summary.asset_code}`,
19
- `Available: ${formatCoinAmount(available)}`,
20
- `Locked: ${formatCoinAmount(locked)}`,
21
- `Total: ${formatCoinAmount(total)}`,
22
- `Updated: ${summary.updated_at}`,
23
- ].join("\n");
24
- }
25
-
26
- function extractMemo(tx: any): string | null {
27
- if (!tx.metadata_json) return null;
28
- try {
29
- const meta = typeof tx.metadata_json === "string"
30
- ? JSON.parse(tx.metadata_json)
31
- : tx.metadata_json;
32
- return meta?.memo ?? null;
33
- } catch {
34
- return null;
35
- }
36
- }
37
-
38
- function formatTransaction(tx: any): string {
39
- const lines = [
40
- `Transaction: ${tx.tx_id}`,
41
- `Type: ${tx.type}`,
42
- `Status: ${tx.status}`,
43
- `Amount: ${formatCoinAmount(tx.amount_minor)}`,
44
- `Fee: ${formatCoinAmount(tx.fee_minor)}`,
45
- ];
46
- if (tx.from_agent_id) lines.push(`From: ${tx.from_agent_id}`);
47
- if (tx.to_agent_id) lines.push(`To: ${tx.to_agent_id}`);
48
- const memo = extractMemo(tx);
49
- if (memo) lines.push(`Memo: ${memo}`);
50
- lines.push(`Created: ${tx.created_at}`);
51
- if (tx.completed_at) lines.push(`Completed: ${tx.completed_at}`);
52
- return lines.join("\n");
53
- }
54
-
55
- function formatLedger(data: any): string {
56
- const entries = data.entries ?? [];
57
- if (entries.length === 0) return "No ledger entries found.";
58
-
59
- const lines = entries.map((e: any) => {
60
- const dir = e.direction === "credit" ? "+" : "-";
61
- return `${e.created_at} | ${dir}${formatCoinAmount(e.amount_minor)} | bal=${formatCoinAmount(e.balance_after_minor)} | tx=${e.tx_id}`;
62
- });
63
-
64
- if (data.has_more) {
65
- lines.push(`\n(More entries available — use cursor: "${data.next_cursor}")`);
66
- }
67
- return lines.join("\n");
68
- }
69
-
70
- export function createWalletTool() {
71
- return {
72
- name: "botcord_wallet",
73
- description:
74
- "Manage your BotCord coin wallet: check balance, view ledger, transfer coins, request topup/withdrawal, check transaction status.",
75
- parameters: {
76
- type: "object" as const,
77
- properties: {
78
- action: {
79
- type: "string" as const,
80
- enum: ["balance", "ledger", "transfer", "topup", "withdraw", "tx_status"],
81
- description: "Wallet action to perform",
82
- },
83
- to_agent_id: {
84
- type: "string" as const,
85
- description: "Recipient agent ID (ag_...) — for transfer",
86
- },
87
- amount_minor: {
88
- type: "string" as const,
89
- description: "Amount in minor units (string) — for transfer, topup, withdraw",
90
- },
91
- memo: {
92
- type: "string" as const,
93
- description: "Optional memo — for transfer",
94
- },
95
- idempotency_key: {
96
- type: "string" as const,
97
- description: "Optional idempotency key (UUID) — for transfer, withdraw",
98
- },
99
- channel: {
100
- type: "string" as const,
101
- description: "Topup channel (e.g. 'mock') — for topup",
102
- },
103
- destination_type: {
104
- type: "string" as const,
105
- description: "Withdrawal destination type — for withdraw",
106
- },
107
- destination: {
108
- type: "object" as const,
109
- description: "Withdrawal destination details — for withdraw",
110
- },
111
- tx_id: {
112
- type: "string" as const,
113
- description: "Transaction ID — for tx_status",
114
- },
115
- cursor: {
116
- type: "string" as const,
117
- description: "Pagination cursor — for ledger",
118
- },
119
- limit: {
120
- type: "number" as const,
121
- description: "Max entries to return — for ledger",
122
- },
123
- type: {
124
- type: "string" as const,
125
- description: "Filter by transaction type — for ledger",
126
- },
127
- },
128
- required: ["action"],
129
- },
130
- execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
131
- const cfg = getAppConfig();
132
- if (!cfg) return { error: "No configuration available" };
133
- const singleAccountError = getSingleAccountModeError(cfg);
134
- if (singleAccountError) return { error: singleAccountError };
135
-
136
- const acct = resolveAccountConfig(cfg);
137
- if (!isAccountConfigured(acct)) {
138
- return { error: "BotCord is not configured." };
139
- }
140
-
141
- const client = new BotCordClient(acct);
142
-
143
- try {
144
- switch (args.action) {
145
- case "balance": {
146
- const summary = await client.getWallet();
147
- return { result: formatBalance(summary), data: summary };
148
- }
149
-
150
- case "ledger": {
151
- const opts: { cursor?: string; limit?: number; type?: string } = {};
152
- if (args.cursor) opts.cursor = args.cursor;
153
- if (args.limit) opts.limit = args.limit;
154
- if (args.type) opts.type = args.type;
155
- const ledger = await client.getWalletLedger(opts);
156
- return { result: formatLedger(ledger), data: ledger };
157
- }
158
-
159
- case "transfer": {
160
- if (!args.to_agent_id) return { error: "to_agent_id is required" };
161
- if (!args.amount_minor) return { error: "amount_minor is required" };
162
- const tx = await client.createTransfer({
163
- to_agent_id: args.to_agent_id,
164
- amount_minor: args.amount_minor,
165
- memo: args.memo,
166
- idempotency_key: args.idempotency_key,
167
- });
168
- return { result: formatTransaction(tx), data: tx };
169
- }
170
-
171
- case "topup": {
172
- if (!args.amount_minor) return { error: "amount_minor is required" };
173
- const topup = await client.createTopup({
174
- amount_minor: args.amount_minor,
175
- channel: args.channel,
176
- idempotency_key: args.idempotency_key,
177
- });
178
- return { result: `Topup request created: ${JSON.stringify(topup)}`, data: topup };
179
- }
180
-
181
- case "withdraw": {
182
- if (!args.amount_minor) return { error: "amount_minor is required" };
183
- const withdrawal = await client.createWithdrawal({
184
- amount_minor: args.amount_minor,
185
- destination_type: args.destination_type,
186
- destination: args.destination,
187
- idempotency_key: args.idempotency_key,
188
- });
189
- return {
190
- result: `Withdrawal request created: ${JSON.stringify(withdrawal)}`,
191
- data: withdrawal,
192
- };
193
- }
194
-
195
- case "tx_status": {
196
- if (!args.tx_id) return { error: "tx_id is required" };
197
- const tx = await client.getWalletTransaction(args.tx_id);
198
- return { result: formatTransaction(tx), data: tx };
199
- }
200
-
201
- default:
202
- return { error: `Unknown action: ${args.action}` };
203
- }
204
- } catch (err: any) {
205
- return { error: `Wallet action failed: ${err.message}` };
206
- }
207
- },
208
- };
209
- }