@mint.club/v2-eliza-plugin 2.0.0 → 2.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mint Club contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -22,6 +22,8 @@ The plugin exposes nine protocol-specific actions:
22
22
  | `SEND_TOKEN` | Send native currency or an ERC-20 token |
23
23
  | `CREATE_TOKEN` | Create an ERC-20 bonding curve token |
24
24
 
25
+ `CREATE_TOKEN` performs onchain creation through the CLI. Configure logos and website links afterward using the creator wallet on Mint Club; see the [CLI metadata guidance](../cli/README.md#create-a-token). The plugin does not call the metadata API.
26
+
25
27
  There is no general-purpose DEX swap action. Zap routing checks only direct and one-intermediary homogeneous V2/V3/V4 paths by RPC.
26
28
 
27
29
  ## Setup
@@ -30,7 +32,7 @@ There is no general-purpose DEX swap action. Zap routing checks only direct and
30
32
  npm install @mint.club/v2-eliza-plugin
31
33
  ```
32
34
 
33
- Requires Node.js 18 or later and an ElizaOS project compatible with `@elizaos/core ^1.7.2`. The plugin installs a compatible `@mint.club/v2-cli` 2.x runtime dependency automatically.
35
+ Requires Node.js 22.13 or later and an ElizaOS project compatible with `@elizaos/core ^1.7.2`. The plugin installs a compatible `@mint.club/v2-cli` 2.x runtime dependency automatically.
34
36
 
35
37
  Add the plugin to an ElizaOS character configuration:
36
38
 
@@ -46,29 +48,39 @@ Read-only actions work without a wallet. Write actions and wallet balances requi
46
48
 
47
49
  > Never store a key in a character file, commit it, or paste it into an agent conversation. Use a dedicated wallet with limited funds.
48
50
 
51
+ A write action runs only when the **original user message** is one affirmative `Confirm:` statement. The parser rejects question-form confirmations, non-ASCII whitespace, control/format/bidirectional characters, negation, cancellation, multiple write clauses, and any unmatched text. The model and plugin must never add this prefix on the user’s behalf; a bare follow-up such as “yes” is insufficient.
52
+
53
+ Every confirmation must name exactly one chain and bind every effective financial parameter:
54
+
55
+ - direct buys: `with maximum cost AMOUNT reserve units`
56
+ - direct sells: `with minimum refund AMOUNT reserve units`
57
+ - routed buys/sells: an explicit slippage percentage
58
+ - sends: the native or ERC-20 asset, amount, recipient, and chain
59
+ - token creation: a printable ASCII name without transaction instructions, plus mint and burn royalties in basis points
60
+
49
61
  ## Example prompts
50
62
 
51
63
  - “Get info about SIGNET.”
52
64
  - “What is the price of TOKEN on Robinhood?”
53
- - “Buy 25 SIGNET.”
54
- - “Sell 5 SIGNET.”
55
- - “Buy TOKEN with 10 USDT on Arbitrum with 0.5% slippage.”
56
- - “Sell 5 TOKEN for USDC on Unichain.”
65
+ - “Confirm: Buy 25 SIGNET on Base with maximum cost 30 reserve units.”
66
+ - “Confirm: Sell 5 SIGNET on Base with minimum refund 4 reserve units.”
67
+ - “Confirm: Buy TOKEN with 10 USDT on Arbitrum with 0.5% slippage.”
68
+ - “Confirm: Sell 5 TOKEN for USDC on Unichain with 1% slippage.”
57
69
  - “Show my wallet balance on Polygon.”
58
- - “Send 10 USDG to `0x1111111111111111111111111111111111111111` on Robinhood.”
59
- - “Create token \"My Token\" (MYT) backed by USDG with max supply 1000000 using a linear curve from 0.01 to 1 on Robinhood.”
70
+ - “Confirm: Send 10 USDG to `0x1111111111111111111111111111111111111111` on Robinhood.”
71
+ - “Confirm: Create token \"My Token\" (MYT) backed by USDG with max supply 1000000 using a linear curve from 0.01 to 1 on Robinhood with 100 bps mint royalty and 100 bps burn royalty.”
60
72
 
61
- The Zap parser deliberately rejects the ambiguous target-amount form “Buy 10 TOKEN with ETH.” Use “Buy TOKEN with 0.1 ETH” so the exact routed input amount is explicit. Zap slippage defaults to 1% when omitted.
73
+ The Zap parser deliberately rejects the ambiguous target-amount form “Buy 10 TOKEN with ETH.” Use “Buy TOKEN with 0.1 ETH” so the exact routed input amount is explicit. The plugin does not accept an implicit Zap slippage default in a confirmed write.
62
74
 
63
75
  The target Mint Club token may be ERC-20 or ERC-1155; ERC-1155 amounts must be whole numbers. Routed input/output and `SEND_TOKEN` assets must be native currency or ERC-20 tokens.
64
76
 
65
- The text parser fails closed on explicit max/min or royalty constraints it cannot map without ambiguity. `CREATE_TOKEN` creates ERC-20 tokens with the CLI defaults of 100 basis points (1%) for both mint and burn royalties. Use the CLI when different royalties are required; use the CLI or MCP server for explicit trade limits.
77
+ The text parser fails closed on constraints it cannot map without ambiguity. Direct trade limits use numeric reserve-token units, and token-creation royalties use integer basis points. Use the CLI or MCP server when you need explicit routed minimum-output values instead of percentage slippage.
66
78
 
67
79
  Supported canonical chain keys:
68
80
 
69
81
  `ethereum` · `optimism` · `arbitrum` · `avalanche` · `base` · `polygon` · `bsc` · `zora` · `unichain` · `robinhood` · `sepolia` · `base-sepolia`
70
82
 
71
- Natural-language aliases such as “Ethereum mainnet,” “Ethereum Sepolia,” “Base Sepolia,” “Arbitrum One,” “BNB Chain,” and “Robinhood Chain” are loaded from the central registry. Base is the default. Contradictory or multiple positive chain instructions are rejected rather than guessed.
83
+ Natural-language aliases such as “Ethereum mainnet,” “Ethereum Sepolia,” “Base Sepolia,” “Arbitrum One,” “BNB Chain,” and “Robinhood Chain” are loaded from the central registry. Base is the default only for reads; every write confirmation must name one chain explicitly. Contradictory or repeated chain instructions are rejected rather than guessed.
72
84
 
73
85
  ZapV2 is deployed on every supported chain listed above. Blast is unsupported by this integration.
74
86
 
package/dist/commands.js CHANGED
@@ -2,6 +2,58 @@ import { createRequire } from 'module';
2
2
  const require = createRequire(import.meta.url);
3
3
  const chainRegistry = require('@mint.club/v2-cli/chain-registry.json');
4
4
  export const SUPPORTED_CHAINS = Object.freeze(chainRegistry.chains.map(({ key }) => key));
5
+ function matchSpan(match) {
6
+ const start = match.index;
7
+ if (start === undefined)
8
+ throw new Error('Parser match is missing its source position');
9
+ return [start, start + match[0].length];
10
+ }
11
+ function assertFullyParsed(text, spans) {
12
+ const ordered = [...spans].sort(([left], [right]) => left - right);
13
+ for (let index = 1; index < ordered.length; index += 1) {
14
+ if (ordered[index][0] < ordered[index - 1][1]) {
15
+ throw new Error('Confirmed write contains overlapping transaction clauses');
16
+ }
17
+ }
18
+ const remaining = text.split('');
19
+ for (const [start, end] of spans) {
20
+ for (let index = start; index < end; index += 1)
21
+ remaining[index] = ' ';
22
+ }
23
+ const unmatched = remaining
24
+ .join('')
25
+ .replace(/[.,!?;]+/g, ' ')
26
+ .trim();
27
+ if (unmatched) {
28
+ throw new Error(`Confirmed write contains unsupported or unmatched text: "${unmatched}"`);
29
+ }
30
+ }
31
+ const WRITE_ACTIONS = new Set([
32
+ 'BUY_TOKEN',
33
+ 'SELL_TOKEN',
34
+ 'ZAP_BUY',
35
+ 'ZAP_SELL',
36
+ 'SEND_TOKEN',
37
+ 'CREATE_TOKEN',
38
+ ]);
39
+ const UNSAFE_CONFIRMATION_CHARS = /[^\S ]|[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
40
+ const TRANSACTION_NAME_WORDS = /(?:^|[^a-z])(?:buy|mint|sell|burn|send|transfer|create|launch|approve|revoke|execute|confirm)(?=$|[^a-z])/i;
41
+ function confirmationEnvelopeSpan(text) {
42
+ const confirmation = text.match(/^\s*confirm\s*:\s+/i);
43
+ if (!confirmation) {
44
+ throw new Error('Write requests must start with "Confirm:" and repeat the full transaction details');
45
+ }
46
+ if (UNSAFE_CONFIRMATION_CHARS.test(text)) {
47
+ throw new Error('Confirmed writes must use printable text with ASCII spaces; control, formatting, and bidirectional characters are not allowed');
48
+ }
49
+ if (text.includes('?')) {
50
+ throw new Error('Confirmed writes must be affirmative statements, not questions');
51
+ }
52
+ if (/\b(?:do\s+not|don['’]?t|not|never|cancel|abort|stop|without|except|no\s+longer)\b/i.test(text)) {
53
+ throw new Error('Confirmed write must be a single affirmative transaction without negation or cancellation');
54
+ }
55
+ return matchSpan(confirmation);
56
+ }
5
57
  function escapeRegExp(value) {
6
58
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
59
  }
@@ -48,17 +100,25 @@ function resolveChainAlias(alias) {
48
100
  throw new Error(`Unknown chain alias: ${alias}`);
49
101
  return chain;
50
102
  }
51
- function chainArgs(text) {
103
+ function chainSelection(text, strictWrite = false) {
52
104
  const negated = new Set();
53
105
  const negation = new RegExp(`\\b(?:not|without|except)\\s+(?:on\\s+)?(?:the\\s+)?(${CHAIN_ALIAS_PATTERN})(?:\\s+chain)?\\b`, 'gi');
54
- const positiveText = text.replace(negation, (_match, alias) => {
106
+ const positiveText = text.replace(negation, (match, alias) => {
55
107
  negated.add(resolveChainAlias(alias));
56
- return ' ';
108
+ return ' '.repeat(match.length);
57
109
  });
110
+ if (positiveText.length !== text.length) {
111
+ throw new Error('Chain parser span alignment failed');
112
+ }
58
113
  const mentioned = new Set();
59
- const positive = new RegExp(`(?:\\bon\\s+(?:the\\s+)?(${CHAIN_ALIAS_PATTERN})(?:\\s+chain)?\\b|\\b(${CHAIN_ALIAS_PATTERN})\\s+(?:chain|mainnet|wallet|balance|holdings)\\b)`, 'gi');
114
+ const spans = [];
115
+ const contextSuffix = strictWrite
116
+ ? '(?:chain|mainnet)'
117
+ : '(?:chain|mainnet|wallet|balance|holdings)';
118
+ const positive = new RegExp(`(?:\\bon\\s+(?:the\\s+)?(${CHAIN_ALIAS_PATTERN})(?:\\s+chain)?\\b|\\b(${CHAIN_ALIAS_PATTERN})\\s+${contextSuffix}\\b)`, 'gi');
60
119
  for (const match of positiveText.matchAll(positive)) {
61
120
  mentioned.add(resolveChainAlias(match[1] ?? match[2]));
121
+ spans.push(matchSpan(match));
62
122
  }
63
123
  const hasConflict = [...mentioned].some((chain) => negated.has(chain));
64
124
  if (mentioned.size > 1 ||
@@ -67,7 +127,7 @@ function chainArgs(text) {
67
127
  throw new Error(`Specify exactly one chain: ${SUPPORTED_CHAINS.join(', ')}`);
68
128
  }
69
129
  const selected = mentioned.values().next().value ?? 'base';
70
- return ['--chain', selected];
130
+ return { args: ['--chain', selected], spans, mentionCount: spans.length };
71
131
  }
72
132
  const TOKEN_PATTERN = '(0x[a-fA-F0-9]{40}|[a-zA-Z][a-zA-Z0-9_-]*)';
73
133
  const ASSET_PATTERN = '(native(?:\\s+currency)?|0x[a-fA-F0-9]{40}|[a-zA-Z][a-zA-Z0-9_-]*)';
@@ -80,16 +140,33 @@ function rejectUnsupportedIntent(text, pattern, description) {
80
140
  throw new Error(`${description} is not supported by this text parser`);
81
141
  }
82
142
  }
143
+ function tradeLimitPattern(action) {
144
+ const limit = action === 'buy'
145
+ ? 'max(?:imum)?\\s+cost'
146
+ : 'min(?:imum)?\\s+refund';
147
+ return new RegExp(`\\bwith\\s+(?:a\\s+)?${limit}\\s+${AMOUNT_PATTERN}\\s+reserve\\s+(?:tokens?|units?)\\b`, 'i');
148
+ }
149
+ function tradeLimitParts(text, action) {
150
+ const match = text.match(tradeLimitPattern(action));
151
+ if (!match) {
152
+ throw new Error(action === 'buy'
153
+ ? 'Confirmed buy must include "with maximum cost AMOUNT reserve units"'
154
+ : 'Confirmed sell must include "with minimum refund AMOUNT reserve units"');
155
+ }
156
+ return {
157
+ args: [action === 'buy' ? '--max-cost' : '--min-refund', match[1]],
158
+ span: matchSpan(match),
159
+ };
160
+ }
83
161
  function tradeParts(text, action) {
84
- rejectUnsupportedIntent(text, action === 'buy'
85
- ? /\bmax(?:imum)?\s+cost\b/i
86
- : /\bmin(?:imum)?\s+refund\b/i, action === 'buy' ? 'Maximum cost' : 'Minimum refund');
87
162
  const verbs = action === 'buy' ? '(?:buy|mint)' : '(?:sell|burn)';
88
163
  const match = text.match(new RegExp(`\\b${verbs}\\s+${AMOUNT_PATTERN}\\s+(?:of\\s+)?${TOKEN_PATTERN}\\b`, 'i'));
89
164
  if (!match) {
90
165
  throw new Error('Specify an amount and token, for example: "buy 10 SIGNET"');
91
166
  }
92
- const trailingText = text.slice((match.index ?? 0) + match[0].length);
167
+ const trailingText = text
168
+ .slice((match.index ?? 0) + match[0].length)
169
+ .replace(tradeLimitPattern(action), ' ');
93
170
  const routedIntent = action === 'buy'
94
171
  ? /\b(?:with|using|from)\b/i
95
172
  : /\b(?:for|to|into)\b/i;
@@ -98,7 +175,7 @@ function tradeParts(text, action) {
98
175
  ? 'Use the exact-input form: Buy TOKEN with AMOUNT INPUT_TOKEN'
99
176
  : 'Use the routed-output form: Sell AMOUNT TOKEN for OUTPUT_TOKEN');
100
177
  }
101
- return { amount: match[1], token: match[2] };
178
+ return { amount: match[1], token: match[2], span: matchSpan(match) };
102
179
  }
103
180
  function zapBuyParts(text) {
104
181
  rejectUnsupportedIntent(text, /\bmin(?:imum)?\s+tokens?\b/i, 'Minimum token output');
@@ -110,6 +187,7 @@ function zapBuyParts(text) {
110
187
  token: match[1],
111
188
  inputAmount: match[2],
112
189
  inputToken: normalizeAsset(match[3]),
190
+ span: matchSpan(match),
113
191
  };
114
192
  }
115
193
  function zapSellParts(text) {
@@ -122,11 +200,18 @@ function zapSellParts(text) {
122
200
  amount: match[1],
123
201
  token: match[2],
124
202
  outputToken: normalizeAsset(match[3]),
203
+ span: matchSpan(match),
125
204
  };
126
205
  }
127
- function slippageArgs(text) {
206
+ function slippageParts(text) {
128
207
  const match = text.match(/\b(?:with|at)?\s*([0-9]+(?:\.[0-9]+)?)\s*%\s+slippage\b/i);
129
- return match ? ['--slippage', match[1]] : [];
208
+ if (!match) {
209
+ throw new Error('Confirmed routed write must include an explicit slippage percentage');
210
+ }
211
+ return {
212
+ args: ['--slippage', match[1]],
213
+ spans: [matchSpan(match)],
214
+ };
130
215
  }
131
216
  function tokenFromText(text, action) {
132
217
  const patterns = action === 'info'
@@ -148,9 +233,9 @@ function tokenFromText(text, action) {
148
233
  throw new Error(`Specify a token for ${action}`);
149
234
  }
150
235
  function sendParts(text, chain) {
151
- const match = text.match(new RegExp(`\\b(?:send|transfer)\\s+${AMOUNT_PATTERN}\\s+(?:${ASSET_PATTERN}\\s+)?to\\s+(0x[a-fA-F0-9]{40})\\b`, 'i'));
236
+ const match = text.match(new RegExp(`\\b(?:send|transfer)\\s+${AMOUNT_PATTERN}\\s+${ASSET_PATTERN}\\s+to\\s+(0x[a-fA-F0-9]{40})\\b`, 'i'));
152
237
  if (!match) {
153
- throw new Error('Specify amount, optional token, and recipient, for example: "send 10 USDC to 0x..."');
238
+ throw new Error('Specify amount, token or native asset, and recipient, for example: "send 10 USDC to 0x..."');
154
239
  }
155
240
  const token = match[2];
156
241
  const normalizedToken = token
@@ -175,13 +260,39 @@ function sendParts(text, chain) {
175
260
  ? undefined
176
261
  : token,
177
262
  recipient: match[3],
263
+ span: matchSpan(match),
264
+ };
265
+ }
266
+ function royaltyParts(text) {
267
+ const mint = text.match(/\bwith\s+([0-9]+)\s*(?:bp|bps|basis\s+points?)\s+mint\s+royalt(?:y|ies)\b/i);
268
+ const burn = text.match(/\band\s+([0-9]+)\s*(?:bp|bps|basis\s+points?)\s+burn\s+royalt(?:y|ies)\b/i);
269
+ if (!mint || !burn) {
270
+ throw new Error('Confirmed token creation must include explicit mint and burn royalties in basis points');
271
+ }
272
+ for (const value of [mint[1], burn[1]]) {
273
+ if (Number(value) > 10_000) {
274
+ throw new Error('Royalty basis points must be between 0 and 10000');
275
+ }
276
+ }
277
+ return {
278
+ args: [
279
+ '--mint-royalty',
280
+ mint[1],
281
+ '--burn-royalty',
282
+ burn[1],
283
+ ],
284
+ spans: [matchSpan(mint), matchSpan(burn)],
178
285
  };
179
286
  }
180
287
  function createParts(text) {
181
- rejectUnsupportedIntent(text, /\b(?:(?:mint|burn)\s+)?royalt(?:y|ies)\b/i, 'Explicit royalty configuration');
182
- const match = text.match(/\b(?:create|launch)\s+(?:a\s+)?token\s+"([^"]{1,64})"\s+\(([a-zA-Z][a-zA-Z0-9_-]*)\)\s+backed\s+by\s+(0x[a-fA-F0-9]{40}|[a-zA-Z][a-zA-Z0-9_-]*)\s+with\s+max(?:imum)?\s+supply\s+([0-9]+(?:\.[0-9]+)?)\s+using\s+(?:a\s+)?(linear|exponential|logarithmic|flat)\s+curve\s+from\s+([0-9]+(?:\.[0-9]+)?)\s+to\s+([0-9]+(?:\.[0-9]+)?)\b/i);
288
+ const match = text.match(/\b(?:create|launch)\s+(?:a\s+)?token\s+"([a-zA-Z0-9][a-zA-Z0-9 ._&()+-]{0,63})"\s+\(([a-zA-Z][a-zA-Z0-9_-]*)\)\s+backed\s+by\s+(0x[a-fA-F0-9]{40}|[a-zA-Z][a-zA-Z0-9_-]*)\s+with\s+max(?:imum)?\s+supply\s+([0-9]+(?:\.[0-9]+)?)\s+using\s+(?:a\s+)?(linear|exponential|logarithmic|flat)\s+curve\s+from\s+([0-9]+(?:\.[0-9]+)?)\s+to\s+([0-9]+(?:\.[0-9]+)?)\b/i);
183
289
  if (!match) {
184
- throw new Error('Use: create token "Name" (SYMBOL) backed by RESERVE with max supply N using a CURVE curve from START to END');
290
+ throw new Error('Use a printable ASCII name: create token "Name" (SYMBOL) backed by RESERVE with max supply N using a CURVE curve from START to END');
291
+ }
292
+ if (match[1] !== match[1].trim() ||
293
+ / {2,}/.test(match[1]) ||
294
+ TRANSACTION_NAME_WORDS.test(match[1])) {
295
+ throw new Error('Token name must be printable ASCII without transaction instructions');
185
296
  }
186
297
  return {
187
298
  name: match[1],
@@ -191,10 +302,23 @@ function createParts(text) {
191
302
  curve: match[5].toLowerCase(),
192
303
  initialPrice: match[6],
193
304
  finalPrice: match[7],
305
+ span: matchSpan(match),
194
306
  };
195
307
  }
308
+ function confirmedWriteArgs(text, argv, spans, chainMentionCount) {
309
+ const confirmationSpan = confirmationEnvelopeSpan(text);
310
+ if (chainMentionCount !== 1) {
311
+ throw new Error(`Confirmed write must specify exactly one chain: ${SUPPORTED_CHAINS.join(', ')}`);
312
+ }
313
+ assertFullyParsed(text, [confirmationSpan, ...spans]);
314
+ return [...argv, '--yes'];
315
+ }
196
316
  export function buildActionArgs(action, text) {
197
- const prefix = chainArgs(text);
317
+ const isWrite = WRITE_ACTIONS.has(action);
318
+ if (isWrite)
319
+ confirmationEnvelopeSpan(text);
320
+ const chain = chainSelection(text, isWrite);
321
+ const prefix = chain.args;
198
322
  switch (action) {
199
323
  case 'TOKEN_INFO':
200
324
  return [...prefix, 'info', tokenFromText(text, 'info')];
@@ -203,53 +327,72 @@ export function buildActionArgs(action, text) {
203
327
  case 'WALLET_BALANCE':
204
328
  return [...prefix, 'wallet'];
205
329
  case 'BUY_TOKEN': {
206
- const { token, amount } = tradeParts(text, 'buy');
207
- return [...prefix, 'buy', token, '--amount', amount];
330
+ const parts = tradeParts(text, 'buy');
331
+ const limit = tradeLimitParts(text, 'buy');
332
+ return confirmedWriteArgs(text, [
333
+ ...prefix,
334
+ 'buy',
335
+ parts.token,
336
+ '--amount',
337
+ parts.amount,
338
+ ...limit.args,
339
+ ], [...chain.spans, parts.span, limit.span], chain.mentionCount);
208
340
  }
209
341
  case 'SELL_TOKEN': {
210
- const { token, amount } = tradeParts(text, 'sell');
211
- return [...prefix, 'sell', token, '--amount', amount];
342
+ const parts = tradeParts(text, 'sell');
343
+ const limit = tradeLimitParts(text, 'sell');
344
+ return confirmedWriteArgs(text, [
345
+ ...prefix,
346
+ 'sell',
347
+ parts.token,
348
+ '--amount',
349
+ parts.amount,
350
+ ...limit.args,
351
+ ], [...chain.spans, parts.span, limit.span], chain.mentionCount);
212
352
  }
213
353
  case 'ZAP_BUY': {
214
- const { token, inputAmount, inputToken } = zapBuyParts(text);
215
- return [
354
+ const parts = zapBuyParts(text);
355
+ const slippage = slippageParts(text);
356
+ return confirmedWriteArgs(text, [
216
357
  ...prefix,
217
358
  'zap-buy',
218
- token,
359
+ parts.token,
219
360
  '--input-token',
220
- inputToken,
361
+ parts.inputToken,
221
362
  '--input-amount',
222
- inputAmount,
223
- ...slippageArgs(text),
224
- ];
363
+ parts.inputAmount,
364
+ ...slippage.args,
365
+ ], [...chain.spans, parts.span, ...slippage.spans], chain.mentionCount);
225
366
  }
226
367
  case 'ZAP_SELL': {
227
- const { token, amount, outputToken } = zapSellParts(text);
228
- return [
368
+ const parts = zapSellParts(text);
369
+ const slippage = slippageParts(text);
370
+ return confirmedWriteArgs(text, [
229
371
  ...prefix,
230
372
  'zap-sell',
231
- token,
373
+ parts.token,
232
374
  '--amount',
233
- amount,
375
+ parts.amount,
234
376
  '--output-token',
235
- outputToken,
236
- ...slippageArgs(text),
237
- ];
377
+ parts.outputToken,
378
+ ...slippage.args,
379
+ ], [...chain.spans, parts.span, ...slippage.spans], chain.mentionCount);
238
380
  }
239
381
  case 'SEND_TOKEN': {
240
- const { amount, token, recipient } = sendParts(text, prefix[1]);
241
- return [
382
+ const parts = sendParts(text, prefix[1]);
383
+ return confirmedWriteArgs(text, [
242
384
  ...prefix,
243
385
  'send',
244
- recipient,
386
+ parts.recipient,
245
387
  '--amount',
246
- amount,
247
- ...(token ? ['--token', token] : []),
248
- ];
388
+ parts.amount,
389
+ ...(parts.token ? ['--token', parts.token] : []),
390
+ ], [...chain.spans, parts.span], chain.mentionCount);
249
391
  }
250
392
  case 'CREATE_TOKEN': {
251
393
  const parts = createParts(text);
252
- return [
394
+ const royalties = royaltyParts(text);
395
+ return confirmedWriteArgs(text, [
253
396
  ...prefix,
254
397
  'create',
255
398
  '--name',
@@ -266,8 +409,8 @@ export function buildActionArgs(action, text) {
266
409
  parts.initialPrice,
267
410
  '--final-price',
268
411
  parts.finalPrice,
269
- '--yes',
270
- ];
412
+ ...royalties.args,
413
+ ], [...chain.spans, parts.span, ...royalties.spans], chain.mentionCount);
271
414
  }
272
415
  }
273
416
  }
package/dist/plugin.js CHANGED
@@ -105,7 +105,7 @@ const buyTokenAction = createAction({
105
105
  description: 'Mint a token with its Bond reserve ERC-20',
106
106
  validateText: (text) => canBuildActionArgs('BUY_TOKEN', text) &&
107
107
  !canBuildActionArgs('ZAP_BUY', text),
108
- examplePrompt: 'Buy 10 SIGNET',
108
+ examplePrompt: 'Confirm: Buy 10 SIGNET on Base with maximum cost 12 reserve units',
109
109
  exampleResult: 'Minted 10 SIGNET through MCV2_Bond.',
110
110
  });
111
111
  const sellTokenAction = createAction({
@@ -114,7 +114,7 @@ const sellTokenAction = createAction({
114
114
  description: 'Burn a token for its Bond reserve ERC-20',
115
115
  validateText: (text) => canBuildActionArgs('SELL_TOKEN', text) &&
116
116
  !canBuildActionArgs('ZAP_SELL', text),
117
- examplePrompt: 'Sell 5 SIGNET',
117
+ examplePrompt: 'Confirm: Sell 5 SIGNET on Base with minimum refund 4 reserve units',
118
118
  exampleResult: 'Burned 5 SIGNET through MCV2_Bond.',
119
119
  });
120
120
  const zapBuyAction = createAction({
@@ -122,7 +122,7 @@ const zapBuyAction = createAction({
122
122
  similes: ['BUY_WITH_ASSET', 'MINT_WITH_ASSET', 'ROUTED_MINT'],
123
123
  description: 'Mint a Mint Club token from an exact amount of a routed native/ERC-20 asset via MCV2_ZapV2',
124
124
  validateText: (text) => canBuildActionArgs('ZAP_BUY', text),
125
- examplePrompt: 'Buy TOKEN with 10 USDC on Arbitrum',
125
+ examplePrompt: 'Confirm: Buy TOKEN with 10 USDC on Arbitrum with 1% slippage',
126
126
  exampleResult: 'Minted TOKEN from 10 USDC through MCV2_ZapV2.',
127
127
  });
128
128
  const zapSellAction = createAction({
@@ -130,7 +130,7 @@ const zapSellAction = createAction({
130
130
  similes: ['SELL_FOR_ASSET', 'BURN_TO_ASSET', 'ROUTED_BURN'],
131
131
  description: 'Burn a Mint Club token into a routed native/ERC-20 output asset via MCV2_ZapV2',
132
132
  validateText: (text) => canBuildActionArgs('ZAP_SELL', text),
133
- examplePrompt: 'Sell 5 TOKEN for USDC on Unichain',
133
+ examplePrompt: 'Confirm: Sell 5 TOKEN for USDC on Unichain with 1% slippage',
134
134
  exampleResult: 'Burned 5 TOKEN into USDC through MCV2_ZapV2.',
135
135
  });
136
136
  const walletBalanceAction = createAction({
@@ -146,7 +146,7 @@ const sendTokenAction = createAction({
146
146
  similes: ['TRANSFER_TOKEN', 'SEND_ASSET'],
147
147
  description: 'Send native currency or an ERC-20 token to an address',
148
148
  validateText: (text) => canBuildActionArgs('SEND_TOKEN', text),
149
- examplePrompt: 'Send 10 USDG to 0x1111111111111111111111111111111111111111 on Robinhood',
149
+ examplePrompt: 'Confirm: Send 10 USDG to 0x1111111111111111111111111111111111111111 on Robinhood',
150
150
  exampleResult: 'Sent 10 USDG on Robinhood Chain.',
151
151
  });
152
152
  const createTokenAction = createAction({
@@ -154,7 +154,7 @@ const createTokenAction = createAction({
154
154
  similes: ['LAUNCH_TOKEN', 'CREATE_BONDING_CURVE'],
155
155
  description: 'Create an ERC-20 Mint Club V2 bonding curve token',
156
156
  validateText: (text) => canBuildActionArgs('CREATE_TOKEN', text),
157
- examplePrompt: 'Create token "My Token" (MYT) backed by USDG with max supply 1000000 using a linear curve from 0.01 to 1 on Robinhood',
157
+ examplePrompt: 'Confirm: Create token "My Token" (MYT) backed by USDG with max supply 1000000 using a linear curve from 0.01 to 1 on Robinhood with 100 bps mint royalty and 100 bps burn royalty',
158
158
  exampleResult: 'Created the MYT bonding curve token on Robinhood Chain.',
159
159
  });
160
160
  const mintclubProvider = {
@@ -170,9 +170,10 @@ const mintclubProvider = {
170
170
  '- SEND_TOKEN: send native currency or ERC-20 tokens',
171
171
  '- CREATE_TOKEN: create an ERC-20 bonding curve token',
172
172
  '',
173
- `Supported chains: ${SUPPORTED_CHAINS.join(', ')}. Base is the default.`,
173
+ `Supported chains: ${SUPPORTED_CHAINS.join(', ')}. Base is the default for reads; writes require exactly one explicit chain.`,
174
174
  'Zap routing enumerates direct and one-intermediary V2/V3/V4 candidates and uses the configured per-chain ZapV2 deployment.',
175
- 'The compatible mc CLI is installed as a plugin dependency; write actions require PRIVATE_KEY or the CLI wallet file.',
175
+ `Write actions run only when the original user message is one affirmative "Confirm:" statement that repeats the full transaction details; never add this prefix on the user's behalf. Direct trades require max-cost/min-refund reserve units, routed trades require slippage, sends require an asset, and creation requires a printable ASCII name without transaction instructions plus mint/burn royalty basis points.`,
176
+ 'The compatible mc CLI is installed as a plugin dependency; writes also require PRIVATE_KEY or the CLI wallet file.',
176
177
  ].join('\n'),
177
178
  values: {
178
179
  platform: 'Mint Club V2',
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@mint.club/v2-eliza-plugin",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "ElizaOS plugin for Mint Club V2 Bond operations and bounded local Uniswap ZapV2 routing across supported chains",
5
5
  "files": [
6
- "dist",
7
- "README.md"
6
+ "dist/",
7
+ "README.md",
8
+ "LICENSE"
8
9
  ],
9
10
  "type": "module",
10
11
  "main": "dist/index.js",
@@ -17,7 +18,7 @@
17
18
  "prepublishOnly": "npm run build"
18
19
  },
19
20
  "dependencies": {
20
- "@mint.club/v2-cli": "^2.0.0",
21
+ "@mint.club/v2-cli": "^2.0.2",
21
22
  "zod": "^4.3.5"
22
23
  },
23
24
  "peerDependencies": {
@@ -46,10 +47,13 @@
46
47
  "@elizaos/core": "^1.7.2",
47
48
  "@types/node": "^22.0.0",
48
49
  "typescript": "^5.0.0",
49
- "vitest": "^2.1.9"
50
+ "vitest": "^3.2.7"
50
51
  },
51
52
  "license": "MIT",
52
53
  "publishConfig": {
53
54
  "access": "public"
55
+ },
56
+ "engines": {
57
+ "node": ">=22.13.0"
54
58
  }
55
59
  }