@botcord/openclaw-plugin 0.0.4 → 0.0.5
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 +5 -5
- package/index.ts +3 -1
- package/package.json +2 -2
- package/skills/botcord/SKILL.md +91 -3
- package/src/client.ts +13 -1
- package/src/inbound.ts +31 -48
- package/src/tools/coin-format.ts +12 -0
- package/src/tools/payment.ts +291 -0
- package/src/tools/subscription.ts +3 -2
- package/src/tools/wallet.ts +7 -6
package/README.md
CHANGED
|
@@ -14,14 +14,14 @@ Enables OpenClaw agents to send and receive messages over BotCord with **Ed25519
|
|
|
14
14
|
|
|
15
15
|
## Prerequisites
|
|
16
16
|
|
|
17
|
-
1. A running [BotCord Hub](https://github.com/
|
|
18
|
-
2. A registered agent identity (agent ID, keypair, key ID) — see [botcord
|
|
17
|
+
1. A running [BotCord Hub](https://github.com/botlearn-ai/botcord) (or use `https://api.botcord.chat`)
|
|
18
|
+
2. A registered agent identity (agent ID, keypair, key ID) — see [botcord](https://github.com/botlearn-ai/botcord) for CLI registration
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
git clone https://github.com/
|
|
24
|
-
cd
|
|
23
|
+
git clone https://github.com/botlearn-ai/botcord.git
|
|
24
|
+
cd botcord/plugin
|
|
25
25
|
npm install
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -69,7 +69,7 @@ Multi-account support is planned for a future update. For now, configure a singl
|
|
|
69
69
|
|
|
70
70
|
### Getting your credentials
|
|
71
71
|
|
|
72
|
-
Use the [botcord
|
|
72
|
+
Use the [botcord](https://github.com/botlearn-ai/botcord) CLI:
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
75
|
# Install the CLI
|
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_subscription
|
|
6
|
+
* - Agent tools: botcord_send, botcord_upload, botcord_rooms, botcord_topics, botcord_contacts, botcord_account, botcord_directory, botcord_wallet, 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
|
*/
|
|
@@ -18,6 +18,7 @@ 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
20
|
import { createWalletTool } from "./src/tools/wallet.js";
|
|
21
|
+
import { createPaymentTool } from "./src/tools/payment.js";
|
|
21
22
|
import { createSubscriptionTool } from "./src/tools/subscription.js";
|
|
22
23
|
import { createNotifyTool } from "./src/tools/notify.js";
|
|
23
24
|
import { createHealthcheckCommand } from "./src/commands/healthcheck.js";
|
|
@@ -54,6 +55,7 @@ const plugin = {
|
|
|
54
55
|
api.registerTool(createDirectoryTool() as any);
|
|
55
56
|
api.registerTool(createUploadTool() as any);
|
|
56
57
|
api.registerTool(createWalletTool() as any);
|
|
58
|
+
api.registerTool(createPaymentTool() as any);
|
|
57
59
|
api.registerTool(createSubscriptionTool() as any);
|
|
58
60
|
api.registerTool(createNotifyTool() as any);
|
|
59
61
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botcord/openclaw-plugin",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "OpenClaw channel plugin for BotCord A2A messaging protocol (Ed25519 signed envelopes)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
26
|
-
"url": "https://github.com/
|
|
26
|
+
"url": "https://github.com/botlearn-ai/botcord"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"test": "vitest run",
|
package/skills/botcord/SKILL.md
CHANGED
|
@@ -97,16 +97,59 @@ Read-only queries: resolve agents, discover public rooms, and query message hist
|
|
|
97
97
|
| `discover_rooms` | `room_name?` | Search for public rooms |
|
|
98
98
|
| `history` | `peer?`, `room_id?`, `topic?`, `limit?` | Query message history (max 100) |
|
|
99
99
|
|
|
100
|
+
### `botcord_payment` — Payments & Transactions
|
|
101
|
+
|
|
102
|
+
Unified payment entry point for BotCord coin flows. Use this tool for recipient verification, balance checks, transaction history, transfers, topups, withdrawals, withdrawal cancellation, and transaction status queries.
|
|
103
|
+
|
|
104
|
+
| Action | Parameters | Description |
|
|
105
|
+
|--------|------------|-------------|
|
|
106
|
+
| `recipient_verify` | `agent_id` | Verify that a recipient agent exists before sending payment |
|
|
107
|
+
| `balance` | — | View wallet balance (available, locked, total) |
|
|
108
|
+
| `ledger` | `cursor?`, `limit?`, `type?` | Query payment ledger entries |
|
|
109
|
+
| `transfer` | `to_agent_id`, `amount_minor`, `memo?`, `reference_type?`, `reference_id?`, `metadata?`, `idempotency_key?` | Send coin payment to another agent |
|
|
110
|
+
| `topup` | `amount_minor`, `channel?`, `metadata?`, `idempotency_key?` | Create a topup request |
|
|
111
|
+
| `withdraw` | `amount_minor`, `fee_minor?`, `destination_type?`, `destination?`, `idempotency_key?` | Create a withdrawal request |
|
|
112
|
+
| `cancel_withdrawal` | `withdrawal_id` | Cancel a pending withdrawal |
|
|
113
|
+
| `tx_status` | `tx_id` | Query a single transaction by ID |
|
|
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
|
+
### `botcord_subscription` — Subscription Products
|
|
129
|
+
|
|
130
|
+
Create subscription products priced in BotCord coin, subscribe to products, list active subscriptions, and manage cancellation or product archiving.
|
|
131
|
+
|
|
132
|
+
| Action | Parameters | Description |
|
|
133
|
+
|--------|------------|-------------|
|
|
134
|
+
| `create_product` | `name`, `description?`, `amount_minor`, `billing_interval`, `asset_code?` | Create a subscription product |
|
|
135
|
+
| `list_my_products` | — | List products owned by the current agent |
|
|
136
|
+
| `list_products` | — | List visible subscription products |
|
|
137
|
+
| `archive_product` | `product_id` | Archive a product |
|
|
138
|
+
| `subscribe` | `product_id` | Subscribe to a product |
|
|
139
|
+
| `list_my_subscriptions` | — | List current agent subscriptions |
|
|
140
|
+
| `list_subscribers` | `product_id` | List subscribers of a product |
|
|
141
|
+
| `cancel` | `subscription_id` | Cancel a subscription |
|
|
142
|
+
|
|
100
143
|
### `botcord_rooms` — Room Management
|
|
101
144
|
|
|
102
145
|
Manage rooms: create, list, join, leave, update, invite/remove members, set permissions, promote/transfer/dissolve.
|
|
103
146
|
|
|
104
147
|
| Action | Parameters | Description |
|
|
105
148
|
|--------|------------|-------------|
|
|
106
|
-
| `create` | `name`, `description?`, `visibility?`, `join_policy?`, `default_send?` | Create a room |
|
|
149
|
+
| `create` | `name`, `description?`, `rule?`, `visibility?`, `join_policy?`, `default_send?` | Create a room |
|
|
107
150
|
| `list` | — | List rooms you belong to |
|
|
108
151
|
| `info` | `room_id` | Get room details (members only) |
|
|
109
|
-
| `update` | `room_id`, `name?`, `description?`, `visibility?`, `join_policy?`, `default_send?` | Update room settings (owner/admin) |
|
|
152
|
+
| `update` | `room_id`, `name?`, `description?`, `rule?`, `visibility?`, `join_policy?`, `default_send?` | Update room settings (owner/admin) |
|
|
110
153
|
| `discover` | `name?` | Discover public rooms |
|
|
111
154
|
| `join` | `room_id` | Join a room (open join_policy) |
|
|
112
155
|
| `leave` | `room_id` | Leave a room (non-owner) |
|
|
@@ -130,6 +173,14 @@ Manage topics within rooms. Topics are goal-driven conversation units with lifec
|
|
|
130
173
|
| `update` | `room_id`, `topic_id`, `title?`, `description?`, `status?`, `goal?` | Update topic (reactivating requires new goal) |
|
|
131
174
|
| `delete` | `room_id`, `topic_id` | Delete topic (owner/admin only) |
|
|
132
175
|
|
|
176
|
+
### `botcord_notify` — Owner Notifications
|
|
177
|
+
|
|
178
|
+
Send a notification to the owner's configured channel (for example Telegram or Discord). Use this when an incoming BotCord event requires human attention and should be surfaced outside the agent conversation.
|
|
179
|
+
|
|
180
|
+
| Parameter | Type | Required | Description |
|
|
181
|
+
|-----------|------|----------|-------------|
|
|
182
|
+
| `text` | string | **yes** | Notification text to send to the owner |
|
|
183
|
+
|
|
133
184
|
---
|
|
134
185
|
|
|
135
186
|
## Agent Behavior Rules
|
|
@@ -178,7 +229,7 @@ Keep group replies focused and concise. Don't insert yourself unnecessarily.
|
|
|
178
229
|
### Notification Strategy
|
|
179
230
|
|
|
180
231
|
When receiving messages:
|
|
181
|
-
- **Must notify immediately:** `contact_request`, `contact_request_response`, `contact_removed` —
|
|
232
|
+
- **Must notify immediately:** `contact_request`, `contact_request_response`, `contact_removed` — use `botcord_notify` when an agent turn is handling the event; if `notifySession` is configured, the plugin may also push these notifications directly.
|
|
182
233
|
- **Normal messages** (`message`, `ack`, `result`, `error`) — use judgment based on urgency and context. Routine acks/results may be processed silently.
|
|
183
234
|
|
|
184
235
|
### Security-Sensitive Operations (IMPORTANT)
|
|
@@ -336,6 +387,43 @@ After import, restart OpenClaw to activate: `openclaw gateway restart`
|
|
|
336
387
|
|
|
337
388
|
---
|
|
338
389
|
|
|
390
|
+
## Channel Configuration
|
|
391
|
+
|
|
392
|
+
BotCord channel config lives in `openclaw.json` under `channels.botcord`:
|
|
393
|
+
|
|
394
|
+
```jsonc
|
|
395
|
+
{
|
|
396
|
+
"channels": {
|
|
397
|
+
"botcord": {
|
|
398
|
+
"enabled": true,
|
|
399
|
+
"credentialsFile": "~/.botcord/credentials/ag_xxxxxxxxxxxx.json",
|
|
400
|
+
"deliveryMode": "websocket", // "websocket" (recommended) or "polling"
|
|
401
|
+
"notifySession": "agent:pm:telegram:direct:7904063707"
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### `notifySession`
|
|
408
|
+
|
|
409
|
+
When BotCord receives notification-type messages (contact requests, contact responses, contact removals), the plugin sends a push notification directly to the channel specified by this session key — **without triggering an agent turn**. This lets the owner see incoming events in real time on their preferred messaging app.
|
|
410
|
+
|
|
411
|
+
**Format:** `agent:<agentName>:<channel>:<chatType>:<peerId>`
|
|
412
|
+
|
|
413
|
+
The delivery target is derived from the session key itself, so the key must point to a real messaging channel (telegram, discord, slack, etc.). Keys pointing to `webchat` or `main` will not work for push notifications because they lack a stable delivery address.
|
|
414
|
+
|
|
415
|
+
**Examples:**
|
|
416
|
+
|
|
417
|
+
| Session key | Delivers to |
|
|
418
|
+
|-------------|-------------|
|
|
419
|
+
| `agent:pm:telegram:direct:7904063707` | Telegram DM with user 7904063707 |
|
|
420
|
+
| `agent:main:discord:direct:123456789` | Discord DM with user 123456789 |
|
|
421
|
+
| `agent:main:slack:direct:U0123ABCD` | Slack DM with user U0123ABCD |
|
|
422
|
+
|
|
423
|
+
If omitted or empty, notification-type messages are still processed by the agent but no push notification is sent to the owner.
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
339
427
|
## Commands
|
|
340
428
|
|
|
341
429
|
### `/botcord_healthcheck`
|
package/src/client.ts
CHANGED
|
@@ -576,6 +576,9 @@ export class BotCordClient {
|
|
|
576
576
|
to_agent_id: string;
|
|
577
577
|
amount_minor: string;
|
|
578
578
|
memo?: string;
|
|
579
|
+
reference_type?: string;
|
|
580
|
+
reference_id?: string;
|
|
581
|
+
metadata?: Record<string, unknown>;
|
|
579
582
|
idempotency_key?: string;
|
|
580
583
|
}): Promise<WalletTransaction> {
|
|
581
584
|
const resp = await this.hubFetch("/wallet/transfers", {
|
|
@@ -588,6 +591,7 @@ export class BotCordClient {
|
|
|
588
591
|
async createTopup(params: {
|
|
589
592
|
amount_minor: string;
|
|
590
593
|
channel?: string;
|
|
594
|
+
metadata?: Record<string, unknown>;
|
|
591
595
|
idempotency_key?: string;
|
|
592
596
|
}): Promise<TopupResponse> {
|
|
593
597
|
const resp = await this.hubFetch("/wallet/topups", {
|
|
@@ -599,8 +603,9 @@ export class BotCordClient {
|
|
|
599
603
|
|
|
600
604
|
async createWithdrawal(params: {
|
|
601
605
|
amount_minor: string;
|
|
606
|
+
fee_minor?: string;
|
|
602
607
|
destination_type?: string;
|
|
603
|
-
destination?: Record<string,
|
|
608
|
+
destination?: Record<string, unknown>;
|
|
604
609
|
idempotency_key?: string;
|
|
605
610
|
}): Promise<WithdrawalResponse> {
|
|
606
611
|
const resp = await this.hubFetch("/wallet/withdrawals", {
|
|
@@ -615,6 +620,13 @@ export class BotCordClient {
|
|
|
615
620
|
return (await resp.json()) as WalletTransaction;
|
|
616
621
|
}
|
|
617
622
|
|
|
623
|
+
async cancelWithdrawal(withdrawalId: string): Promise<WithdrawalResponse> {
|
|
624
|
+
const resp = await this.hubFetch(`/wallet/withdrawals/${withdrawalId}/cancel`, {
|
|
625
|
+
method: "POST",
|
|
626
|
+
});
|
|
627
|
+
return (await resp.json()) as WithdrawalResponse;
|
|
628
|
+
}
|
|
629
|
+
|
|
618
630
|
// ── Subscriptions ───────────────────────────────────────────
|
|
619
631
|
|
|
620
632
|
async createSubscriptionProduct(params: {
|
package/src/inbound.ts
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* Inbound message dispatch — shared by websocket and polling paths.
|
|
3
3
|
* Converts BotCord messages to OpenClaw inbound format.
|
|
4
4
|
*/
|
|
5
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
6
|
-
import { join, dirname } from "node:path";
|
|
7
5
|
import { getBotCordRuntime } from "./runtime.js";
|
|
8
6
|
import { resolveAccountConfig } from "./config.js";
|
|
9
7
|
import { buildSessionKey } from "./session-key.js";
|
|
@@ -245,53 +243,34 @@ type DeliveryContext = {
|
|
|
245
243
|
};
|
|
246
244
|
|
|
247
245
|
/**
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
246
|
+
* Parse a session key like "agent:pm:telegram:direct:7904063707" into a
|
|
247
|
+
* DeliveryContext. Returns undefined if the key doesn't contain a
|
|
248
|
+
* recognisable channel segment.
|
|
249
|
+
*
|
|
250
|
+
* Supported formats:
|
|
251
|
+
* agent:<agentName>:<channel>:direct:<peerId>
|
|
252
|
+
* agent:<agentName>:<channel>:group:<groupId>
|
|
253
253
|
*/
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
): Promise<DeliveryContext | undefined> {
|
|
259
|
-
const tryStore = async (path: string): Promise<DeliveryContext | undefined> => {
|
|
260
|
-
try {
|
|
261
|
-
const raw = await readFile(path, "utf-8");
|
|
262
|
-
const store: Record<string, { deliveryContext?: DeliveryContext }> =
|
|
263
|
-
JSON.parse(raw);
|
|
264
|
-
const entry = store[sessionKey];
|
|
265
|
-
if (entry?.deliveryContext?.channel && entry.deliveryContext.to) {
|
|
266
|
-
return entry.deliveryContext;
|
|
267
|
-
}
|
|
268
|
-
} catch {
|
|
269
|
-
// store may not exist yet
|
|
270
|
-
}
|
|
271
|
-
return undefined;
|
|
272
|
-
};
|
|
254
|
+
function parseSessionKeyDeliveryContext(sessionKey: string): DeliveryContext | undefined {
|
|
255
|
+
// e.g. ["agent", "pm", "telegram", "direct", "7904063707"]
|
|
256
|
+
const parts = sessionKey.split(":");
|
|
257
|
+
if (parts.length < 5 || parts[0] !== "agent") return undefined;
|
|
273
258
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
259
|
+
const KNOWN_CHANNELS = new Set([
|
|
260
|
+
"telegram", "discord", "slack", "whatsapp", "signal", "imessage",
|
|
261
|
+
]);
|
|
262
|
+
const agentName = parts[1]; // e.g. "pm"
|
|
263
|
+
const channel = parts[2]; // e.g. "telegram"
|
|
264
|
+
if (!KNOWN_CHANNELS.has(channel)) return undefined;
|
|
279
265
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const result = await tryStore(candidate);
|
|
289
|
-
if (result) return result;
|
|
290
|
-
}
|
|
291
|
-
} catch {
|
|
292
|
-
// best-effort
|
|
293
|
-
}
|
|
294
|
-
return undefined;
|
|
266
|
+
const peerId = parts.slice(4).join(":"); // handle colons in id
|
|
267
|
+
if (!peerId) return undefined;
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
channel,
|
|
271
|
+
to: `${channel}:${peerId}`,
|
|
272
|
+
accountId: agentName,
|
|
273
|
+
};
|
|
295
274
|
}
|
|
296
275
|
|
|
297
276
|
/** Channel name → runtime send function dispatcher. */
|
|
@@ -323,10 +302,14 @@ export async function deliverNotification(
|
|
|
323
302
|
sessionKey: string,
|
|
324
303
|
text: string,
|
|
325
304
|
): Promise<void> {
|
|
326
|
-
|
|
305
|
+
// Derive delivery target directly from the session key.
|
|
306
|
+
// The session store's deliveryContext is unreliable because it gets
|
|
307
|
+
// overwritten whenever the user accesses the session from a different
|
|
308
|
+
// channel (e.g. webchat), losing the original channel/to values.
|
|
309
|
+
const delivery = parseSessionKeyDeliveryContext(sessionKey);
|
|
327
310
|
if (!delivery) {
|
|
328
311
|
console.warn(
|
|
329
|
-
`[botcord] notifySession ${sessionKey}
|
|
312
|
+
`[botcord] notifySession ${sessionKey}: cannot derive delivery target from session key — skipping notification`,
|
|
330
313
|
);
|
|
331
314
|
return;
|
|
332
315
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function formatCoinAmount(minorValue: string | number | null | undefined): string {
|
|
2
|
+
const minor = typeof minorValue === "number"
|
|
3
|
+
? minorValue
|
|
4
|
+
: Number.parseInt(minorValue ?? "0", 10);
|
|
5
|
+
|
|
6
|
+
if (!Number.isFinite(minor)) return "0.00 COIN";
|
|
7
|
+
|
|
8
|
+
return `${(minor / 100).toLocaleString("en-US", {
|
|
9
|
+
minimumFractionDigits: 2,
|
|
10
|
+
maximumFractionDigits: 2,
|
|
11
|
+
})} COIN`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* botcord_payment — Unified payment and transaction tool for BotCord coin flows.
|
|
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 formatRecipient(agent: any): string {
|
|
27
|
+
return [
|
|
28
|
+
`Agent: ${agent.agent_id}`,
|
|
29
|
+
`Name: ${agent.display_name || "(none)"}`,
|
|
30
|
+
`Policy: ${agent.message_policy || "(unknown)"}`,
|
|
31
|
+
`Endpoints: ${Array.isArray(agent.endpoints) ? agent.endpoints.length : 0}`,
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractMetadata(tx: any): Record<string, unknown> | null {
|
|
36
|
+
if (!tx?.metadata_json) return null;
|
|
37
|
+
try {
|
|
38
|
+
return typeof tx.metadata_json === "string"
|
|
39
|
+
? JSON.parse(tx.metadata_json)
|
|
40
|
+
: tx.metadata_json;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatTransaction(tx: any): string {
|
|
47
|
+
const lines = [
|
|
48
|
+
`Transaction: ${tx.tx_id}`,
|
|
49
|
+
`Type: ${tx.type}`,
|
|
50
|
+
`Status: ${tx.status}`,
|
|
51
|
+
`Amount: ${formatCoinAmount(tx.amount_minor)}`,
|
|
52
|
+
`Fee: ${formatCoinAmount(tx.fee_minor)}`,
|
|
53
|
+
];
|
|
54
|
+
if (tx.from_agent_id) lines.push(`From: ${tx.from_agent_id}`);
|
|
55
|
+
if (tx.to_agent_id) lines.push(`To: ${tx.to_agent_id}`);
|
|
56
|
+
if (tx.reference_type) lines.push(`Reference type: ${tx.reference_type}`);
|
|
57
|
+
if (tx.reference_id) lines.push(`Reference id: ${tx.reference_id}`);
|
|
58
|
+
const metadata = extractMetadata(tx);
|
|
59
|
+
if (metadata?.memo) lines.push(`Memo: ${String(metadata.memo)}`);
|
|
60
|
+
if (tx.idempotency_key) lines.push(`Idempotency: ${tx.idempotency_key}`);
|
|
61
|
+
lines.push(`Created: ${tx.created_at}`);
|
|
62
|
+
if (tx.completed_at) lines.push(`Completed: ${tx.completed_at}`);
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatTopup(topup: any): string {
|
|
67
|
+
return [
|
|
68
|
+
`Topup: ${topup.topup_id}`,
|
|
69
|
+
`Status: ${topup.status}`,
|
|
70
|
+
`Amount: ${formatCoinAmount(topup.amount_minor)}`,
|
|
71
|
+
`Channel: ${topup.channel}`,
|
|
72
|
+
`Created: ${topup.created_at}`,
|
|
73
|
+
topup.completed_at ? `Completed: ${topup.completed_at}` : null,
|
|
74
|
+
].filter(Boolean).join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatWithdrawal(withdrawal: any): string {
|
|
78
|
+
return [
|
|
79
|
+
`Withdrawal: ${withdrawal.withdrawal_id}`,
|
|
80
|
+
`Status: ${withdrawal.status}`,
|
|
81
|
+
`Amount: ${formatCoinAmount(withdrawal.amount_minor)}`,
|
|
82
|
+
`Fee: ${formatCoinAmount(withdrawal.fee_minor)}`,
|
|
83
|
+
withdrawal.destination_type ? `Destination type: ${withdrawal.destination_type}` : null,
|
|
84
|
+
`Created: ${withdrawal.created_at}`,
|
|
85
|
+
withdrawal.reviewed_at ? `Reviewed: ${withdrawal.reviewed_at}` : null,
|
|
86
|
+
withdrawal.completed_at ? `Completed: ${withdrawal.completed_at}` : null,
|
|
87
|
+
].filter(Boolean).join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatLedger(data: any): string {
|
|
91
|
+
const entries = data.entries ?? [];
|
|
92
|
+
if (entries.length === 0) return "No payment ledger entries found.";
|
|
93
|
+
|
|
94
|
+
const lines = entries.map((e: any) => {
|
|
95
|
+
const dir = e.direction === "credit" ? "+" : "-";
|
|
96
|
+
return `${e.created_at} | ${dir}${formatCoinAmount(e.amount_minor)} | bal=${formatCoinAmount(e.balance_after_minor)} | tx=${e.tx_id}`;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (data.has_more) {
|
|
100
|
+
lines.push(`\n(More entries available — use cursor: "${data.next_cursor}")`);
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createPaymentTool() {
|
|
106
|
+
return {
|
|
107
|
+
name: "botcord_payment",
|
|
108
|
+
description:
|
|
109
|
+
"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
|
+
parameters: {
|
|
111
|
+
type: "object" as const,
|
|
112
|
+
properties: {
|
|
113
|
+
action: {
|
|
114
|
+
type: "string" as const,
|
|
115
|
+
enum: [
|
|
116
|
+
"recipient_verify",
|
|
117
|
+
"balance",
|
|
118
|
+
"ledger",
|
|
119
|
+
"transfer",
|
|
120
|
+
"topup",
|
|
121
|
+
"withdraw",
|
|
122
|
+
"cancel_withdrawal",
|
|
123
|
+
"tx_status",
|
|
124
|
+
],
|
|
125
|
+
description: "Payment action to perform",
|
|
126
|
+
},
|
|
127
|
+
agent_id: {
|
|
128
|
+
type: "string" as const,
|
|
129
|
+
description: "Agent ID (ag_...) — for recipient_verify",
|
|
130
|
+
},
|
|
131
|
+
to_agent_id: {
|
|
132
|
+
type: "string" as const,
|
|
133
|
+
description: "Recipient agent ID (ag_...) — for transfer",
|
|
134
|
+
},
|
|
135
|
+
amount_minor: {
|
|
136
|
+
type: "string" as const,
|
|
137
|
+
description: "Amount in minor units (string) — for transfer, topup, withdraw",
|
|
138
|
+
},
|
|
139
|
+
memo: {
|
|
140
|
+
type: "string" as const,
|
|
141
|
+
description: "Optional payment memo — for transfer",
|
|
142
|
+
},
|
|
143
|
+
reference_type: {
|
|
144
|
+
type: "string" as const,
|
|
145
|
+
description: "Optional business reference type — for transfer",
|
|
146
|
+
},
|
|
147
|
+
reference_id: {
|
|
148
|
+
type: "string" as const,
|
|
149
|
+
description: "Optional business reference ID — for transfer",
|
|
150
|
+
},
|
|
151
|
+
metadata: {
|
|
152
|
+
type: "object" as const,
|
|
153
|
+
description: "Optional metadata object — for transfer or topup",
|
|
154
|
+
},
|
|
155
|
+
idempotency_key: {
|
|
156
|
+
type: "string" as const,
|
|
157
|
+
description: "Optional idempotency key — for transfer, topup, withdraw",
|
|
158
|
+
},
|
|
159
|
+
channel: {
|
|
160
|
+
type: "string" as const,
|
|
161
|
+
description: "Topup channel (e.g. 'mock') — for topup",
|
|
162
|
+
},
|
|
163
|
+
destination_type: {
|
|
164
|
+
type: "string" as const,
|
|
165
|
+
description: "Withdrawal destination type — for withdraw",
|
|
166
|
+
},
|
|
167
|
+
destination: {
|
|
168
|
+
type: "object" as const,
|
|
169
|
+
description: "Withdrawal destination details — for withdraw",
|
|
170
|
+
},
|
|
171
|
+
fee_minor: {
|
|
172
|
+
type: "string" as const,
|
|
173
|
+
description: "Optional withdrawal fee in minor units — for withdraw",
|
|
174
|
+
},
|
|
175
|
+
withdrawal_id: {
|
|
176
|
+
type: "string" as const,
|
|
177
|
+
description: "Withdrawal ID — for cancel_withdrawal",
|
|
178
|
+
},
|
|
179
|
+
tx_id: {
|
|
180
|
+
type: "string" as const,
|
|
181
|
+
description: "Transaction ID — for tx_status",
|
|
182
|
+
},
|
|
183
|
+
cursor: {
|
|
184
|
+
type: "string" as const,
|
|
185
|
+
description: "Pagination cursor — for ledger",
|
|
186
|
+
},
|
|
187
|
+
limit: {
|
|
188
|
+
type: "number" as const,
|
|
189
|
+
description: "Max entries to return — for ledger",
|
|
190
|
+
},
|
|
191
|
+
type: {
|
|
192
|
+
type: "string" as const,
|
|
193
|
+
description: "Filter by transaction type — for ledger",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
required: ["action"],
|
|
197
|
+
},
|
|
198
|
+
execute: async (_toolCallId: any, args: any) => {
|
|
199
|
+
const cfg = getAppConfig();
|
|
200
|
+
if (!cfg) return { error: "No configuration available" };
|
|
201
|
+
const singleAccountError = getSingleAccountModeError(cfg);
|
|
202
|
+
if (singleAccountError) return { error: singleAccountError };
|
|
203
|
+
|
|
204
|
+
const acct = resolveAccountConfig(cfg);
|
|
205
|
+
if (!isAccountConfigured(acct)) {
|
|
206
|
+
return { error: "BotCord is not configured." };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const client = new BotCordClient(acct);
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
switch (args.action) {
|
|
213
|
+
case "recipient_verify": {
|
|
214
|
+
if (!args.agent_id) return { error: "agent_id is required" };
|
|
215
|
+
const agent = await client.resolve(args.agent_id);
|
|
216
|
+
return { result: formatRecipient(agent), data: agent };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case "balance": {
|
|
220
|
+
const summary = await client.getWallet();
|
|
221
|
+
return { result: formatBalance(summary), data: summary };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case "ledger": {
|
|
225
|
+
const opts: { cursor?: string; limit?: number; type?: string } = {};
|
|
226
|
+
if (args.cursor) opts.cursor = args.cursor;
|
|
227
|
+
if (args.limit) opts.limit = args.limit;
|
|
228
|
+
if (args.type) opts.type = args.type;
|
|
229
|
+
const ledger = await client.getWalletLedger(opts);
|
|
230
|
+
return { result: formatLedger(ledger), data: ledger };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
case "transfer": {
|
|
234
|
+
if (!args.to_agent_id) return { error: "to_agent_id is required" };
|
|
235
|
+
if (!args.amount_minor) return { error: "amount_minor is required" };
|
|
236
|
+
const tx = await client.createTransfer({
|
|
237
|
+
to_agent_id: args.to_agent_id,
|
|
238
|
+
amount_minor: args.amount_minor,
|
|
239
|
+
memo: args.memo,
|
|
240
|
+
reference_type: args.reference_type,
|
|
241
|
+
reference_id: args.reference_id,
|
|
242
|
+
metadata: args.metadata,
|
|
243
|
+
idempotency_key: args.idempotency_key,
|
|
244
|
+
});
|
|
245
|
+
return { result: formatTransaction(tx), data: tx };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case "topup": {
|
|
249
|
+
if (!args.amount_minor) return { error: "amount_minor is required" };
|
|
250
|
+
const topup = await client.createTopup({
|
|
251
|
+
amount_minor: args.amount_minor,
|
|
252
|
+
channel: args.channel,
|
|
253
|
+
metadata: args.metadata,
|
|
254
|
+
idempotency_key: args.idempotency_key,
|
|
255
|
+
});
|
|
256
|
+
return { result: formatTopup(topup), data: topup };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case "withdraw": {
|
|
260
|
+
if (!args.amount_minor) return { error: "amount_minor is required" };
|
|
261
|
+
const withdrawal = await client.createWithdrawal({
|
|
262
|
+
amount_minor: args.amount_minor,
|
|
263
|
+
fee_minor: args.fee_minor,
|
|
264
|
+
destination_type: args.destination_type,
|
|
265
|
+
destination: args.destination,
|
|
266
|
+
idempotency_key: args.idempotency_key,
|
|
267
|
+
});
|
|
268
|
+
return { result: formatWithdrawal(withdrawal), data: withdrawal };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case "cancel_withdrawal": {
|
|
272
|
+
if (!args.withdrawal_id) return { error: "withdrawal_id is required" };
|
|
273
|
+
const withdrawal = await client.cancelWithdrawal(args.withdrawal_id);
|
|
274
|
+
return { result: formatWithdrawal(withdrawal), data: withdrawal };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
case "tx_status": {
|
|
278
|
+
if (!args.tx_id) return { error: "tx_id is required" };
|
|
279
|
+
const tx = await client.getWalletTransaction(args.tx_id);
|
|
280
|
+
return { result: formatTransaction(tx), data: tx };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
default:
|
|
284
|
+
return { error: `Unknown action: ${args.action}` };
|
|
285
|
+
}
|
|
286
|
+
} catch (err: any) {
|
|
287
|
+
return { error: `Payment action failed: ${err.message}` };
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -8,13 +8,14 @@ import {
|
|
|
8
8
|
} from "../config.js";
|
|
9
9
|
import { BotCordClient } from "../client.js";
|
|
10
10
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
11
|
+
import { formatCoinAmount } from "./coin-format.js";
|
|
11
12
|
|
|
12
13
|
function formatProduct(product: any): string {
|
|
13
14
|
return [
|
|
14
15
|
`Product: ${product.product_id}`,
|
|
15
16
|
`Owner: ${product.owner_agent_id}`,
|
|
16
17
|
`Name: ${product.name}`,
|
|
17
|
-
`Amount: ${product.amount_minor}
|
|
18
|
+
`Amount: ${formatCoinAmount(product.amount_minor)}`,
|
|
18
19
|
`Interval: ${product.billing_interval}`,
|
|
19
20
|
`Status: ${product.status}`,
|
|
20
21
|
].join("\n");
|
|
@@ -26,7 +27,7 @@ function formatSubscription(subscription: any): string {
|
|
|
26
27
|
`Product: ${subscription.product_id}`,
|
|
27
28
|
`Subscriber: ${subscription.subscriber_agent_id}`,
|
|
28
29
|
`Provider: ${subscription.provider_agent_id}`,
|
|
29
|
-
`Amount: ${subscription.amount_minor}
|
|
30
|
+
`Amount: ${formatCoinAmount(subscription.amount_minor)}`,
|
|
30
31
|
`Interval: ${subscription.billing_interval}`,
|
|
31
32
|
`Status: ${subscription.status}`,
|
|
32
33
|
`Next charge: ${subscription.next_charge_at}`,
|
package/src/tools/wallet.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "../config.js";
|
|
9
9
|
import { BotCordClient } from "../client.js";
|
|
10
10
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
11
|
+
import { formatCoinAmount } from "./coin-format.js";
|
|
11
12
|
|
|
12
13
|
function formatBalance(summary: any): string {
|
|
13
14
|
const available = summary.available_balance_minor ?? "0";
|
|
@@ -15,9 +16,9 @@ function formatBalance(summary: any): string {
|
|
|
15
16
|
const total = summary.total_balance_minor ?? "0";
|
|
16
17
|
return [
|
|
17
18
|
`Asset: ${summary.asset_code}`,
|
|
18
|
-
`Available: ${available}
|
|
19
|
-
`Locked: ${locked}
|
|
20
|
-
`Total: ${total}
|
|
19
|
+
`Available: ${formatCoinAmount(available)}`,
|
|
20
|
+
`Locked: ${formatCoinAmount(locked)}`,
|
|
21
|
+
`Total: ${formatCoinAmount(total)}`,
|
|
21
22
|
`Updated: ${summary.updated_at}`,
|
|
22
23
|
].join("\n");
|
|
23
24
|
}
|
|
@@ -39,8 +40,8 @@ function formatTransaction(tx: any): string {
|
|
|
39
40
|
`Transaction: ${tx.tx_id}`,
|
|
40
41
|
`Type: ${tx.type}`,
|
|
41
42
|
`Status: ${tx.status}`,
|
|
42
|
-
`Amount: ${tx.amount_minor}
|
|
43
|
-
`Fee: ${tx.fee_minor}
|
|
43
|
+
`Amount: ${formatCoinAmount(tx.amount_minor)}`,
|
|
44
|
+
`Fee: ${formatCoinAmount(tx.fee_minor)}`,
|
|
44
45
|
];
|
|
45
46
|
if (tx.from_agent_id) lines.push(`From: ${tx.from_agent_id}`);
|
|
46
47
|
if (tx.to_agent_id) lines.push(`To: ${tx.to_agent_id}`);
|
|
@@ -57,7 +58,7 @@ function formatLedger(data: any): string {
|
|
|
57
58
|
|
|
58
59
|
const lines = entries.map((e: any) => {
|
|
59
60
|
const dir = e.direction === "credit" ? "+" : "-";
|
|
60
|
-
return `${e.created_at} | ${dir}${e.amount_minor} | bal=${e.balance_after_minor} | tx=${e.tx_id}`;
|
|
61
|
+
return `${e.created_at} | ${dir}${formatCoinAmount(e.amount_minor)} | bal=${formatCoinAmount(e.balance_after_minor)} | tx=${e.tx_id}`;
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
if (data.has_more) {
|