@kaleidorg/mind 0.5.1 → 0.6.1

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.
Files changed (174) hide show
  1. package/dist/autonomy/index.d.ts +21 -0
  2. package/dist/autonomy/index.d.ts.map +1 -0
  3. package/dist/autonomy/index.js +16 -0
  4. package/dist/autonomy/index.js.map +1 -0
  5. package/dist/autonomy/prompt.d.ts +21 -0
  6. package/dist/autonomy/prompt.d.ts.map +1 -0
  7. package/dist/autonomy/prompt.js +37 -0
  8. package/dist/autonomy/prompt.js.map +1 -0
  9. package/dist/autonomy/risk.d.ts +53 -0
  10. package/dist/autonomy/risk.d.ts.map +1 -0
  11. package/dist/autonomy/risk.js +74 -0
  12. package/dist/autonomy/risk.js.map +1 -0
  13. package/dist/autonomy/run-state.d.ts +39 -0
  14. package/dist/autonomy/run-state.d.ts.map +1 -0
  15. package/dist/autonomy/run-state.js +118 -0
  16. package/dist/autonomy/run-state.js.map +1 -0
  17. package/dist/autonomy/scheduler.d.ts +18 -0
  18. package/dist/autonomy/scheduler.d.ts.map +1 -0
  19. package/dist/autonomy/scheduler.js +113 -0
  20. package/dist/autonomy/scheduler.js.map +1 -0
  21. package/dist/autonomy/task-store.d.ts +44 -0
  22. package/dist/autonomy/task-store.d.ts.map +1 -0
  23. package/dist/autonomy/task-store.js +139 -0
  24. package/dist/autonomy/task-store.js.map +1 -0
  25. package/dist/autonomy/types.d.ts +164 -0
  26. package/dist/autonomy/types.d.ts.map +1 -0
  27. package/dist/autonomy/types.js +20 -0
  28. package/dist/autonomy/types.js.map +1 -0
  29. package/dist/bitrefill/contract.d.ts +60 -0
  30. package/dist/bitrefill/contract.d.ts.map +1 -0
  31. package/dist/bitrefill/contract.js +119 -0
  32. package/dist/bitrefill/contract.js.map +1 -0
  33. package/dist/context/compress.d.ts +65 -0
  34. package/dist/context/compress.d.ts.map +1 -0
  35. package/dist/context/compress.js +181 -0
  36. package/dist/context/compress.js.map +1 -0
  37. package/dist/engine.d.ts +20 -0
  38. package/dist/engine.d.ts.map +1 -1
  39. package/dist/engine.js +23 -4
  40. package/dist/engine.js.map +1 -1
  41. package/dist/evidence.d.ts +62 -0
  42. package/dist/evidence.d.ts.map +1 -0
  43. package/dist/evidence.js +47 -0
  44. package/dist/evidence.js.map +1 -0
  45. package/dist/flashnet/contract.d.ts +56 -0
  46. package/dist/flashnet/contract.d.ts.map +1 -0
  47. package/dist/flashnet/contract.js +100 -0
  48. package/dist/flashnet/contract.js.map +1 -0
  49. package/dist/funnel.d.ts +11 -0
  50. package/dist/funnel.d.ts.map +1 -1
  51. package/dist/funnel.js +62 -7
  52. package/dist/funnel.js.map +1 -1
  53. package/dist/index.d.ts +12 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +11 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/kaleidoswap/contract.js +1 -1
  58. package/dist/kaleidoswap/contract.js.map +1 -1
  59. package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
  60. package/dist/knowledge/bitcoin-copilot.js +85 -2
  61. package/dist/knowledge/bitcoin-copilot.js.map +1 -1
  62. package/dist/providers/types.d.ts +17 -0
  63. package/dist/providers/types.d.ts.map +1 -1
  64. package/dist/qvac/index.d.ts +1 -1
  65. package/dist/qvac/index.d.ts.map +1 -1
  66. package/dist/qvac/index.js.map +1 -1
  67. package/dist/qvac/parse.d.ts +18 -0
  68. package/dist/qvac/parse.d.ts.map +1 -1
  69. package/dist/qvac/parse.js +1 -0
  70. package/dist/qvac/parse.js.map +1 -1
  71. package/dist/qvac/provider.d.ts +16 -0
  72. package/dist/qvac/provider.d.ts.map +1 -1
  73. package/dist/qvac/provider.js +40 -1
  74. package/dist/qvac/provider.js.map +1 -1
  75. package/dist/qvac/stream.d.ts +22 -0
  76. package/dist/qvac/stream.d.ts.map +1 -1
  77. package/dist/qvac/stream.js +33 -1
  78. package/dist/qvac/stream.js.map +1 -1
  79. package/dist/recipe/buy-asset-channel.d.ts +1 -1
  80. package/dist/recipe/buy-asset-channel.d.ts.map +1 -1
  81. package/dist/recipe/buy-asset-channel.js +4 -3
  82. package/dist/recipe/buy-asset-channel.js.map +1 -1
  83. package/dist/recipe/flashnet-swap.d.ts +35 -0
  84. package/dist/recipe/flashnet-swap.d.ts.map +1 -0
  85. package/dist/recipe/flashnet-swap.js +239 -0
  86. package/dist/recipe/flashnet-swap.js.map +1 -0
  87. package/dist/recipe/kaleidoswap-atomic.d.ts +1 -1
  88. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
  89. package/dist/recipe/kaleidoswap-atomic.js +42 -20
  90. package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
  91. package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -1
  92. package/dist/recipe/kaleidoswap-channel-order.js +31 -10
  93. package/dist/recipe/kaleidoswap-channel-order.js.map +1 -1
  94. package/dist/recipe/kaleidoswap-price.d.ts.map +1 -1
  95. package/dist/recipe/kaleidoswap-price.js +7 -1
  96. package/dist/recipe/kaleidoswap-price.js.map +1 -1
  97. package/dist/recipe/runner.d.ts.map +1 -1
  98. package/dist/recipe/runner.js +43 -3
  99. package/dist/recipe/runner.js.map +1 -1
  100. package/dist/recipe/swap.d.ts.map +1 -1
  101. package/dist/recipe/swap.js +14 -1
  102. package/dist/recipe/swap.js.map +1 -1
  103. package/dist/tools/mcp.d.ts +19 -0
  104. package/dist/tools/mcp.d.ts.map +1 -1
  105. package/dist/tools/mcp.js +51 -9
  106. package/dist/tools/mcp.js.map +1 -1
  107. package/dist/wallet/confirm.d.ts.map +1 -1
  108. package/dist/wallet/confirm.js +1 -0
  109. package/dist/wallet/confirm.js.map +1 -1
  110. package/dist/wallet/contract.d.ts.map +1 -1
  111. package/dist/wallet/contract.js +20 -4
  112. package/dist/wallet/contract.js.map +1 -1
  113. package/package.json +5 -4
  114. package/skills/bitrefill/SKILL.md +152 -52
  115. package/skills/channel-manager/SKILL.md +59 -0
  116. package/skills/dca/SKILL.md +48 -0
  117. package/skills/flashnet-swaps/SKILL.md +158 -0
  118. package/skills/kaleido-lsps/SKILL.md +34 -17
  119. package/skills/kaleido-trading/SKILL.md +37 -13
  120. package/skills/liquidity-optimizer/SKILL.md +91 -0
  121. package/skills/merchant-finder/SKILL.md +2 -2
  122. package/skills/portfolio-manager/SKILL.md +67 -0
  123. package/skills/rgb-lightning-node/SKILL.md +38 -11
  124. package/skills/spark-wallet/SKILL.md +235 -0
  125. package/skills/wallet-assistant/SKILL.md +2 -2
  126. package/src/autonomy/autonomy.test.ts +348 -0
  127. package/src/autonomy/index.ts +50 -0
  128. package/src/autonomy/prompt.ts +48 -0
  129. package/src/autonomy/risk.ts +139 -0
  130. package/src/autonomy/run-state.ts +144 -0
  131. package/src/autonomy/scheduler.ts +120 -0
  132. package/src/autonomy/task-store.ts +167 -0
  133. package/src/autonomy/types.ts +186 -0
  134. package/src/bitrefill/contract.test.ts +89 -0
  135. package/src/bitrefill/contract.ts +190 -0
  136. package/src/context/compress.test.ts +120 -0
  137. package/src/context/compress.ts +230 -0
  138. package/src/engine.test.ts +34 -0
  139. package/src/engine.ts +35 -4
  140. package/src/evidence.test.ts +80 -0
  141. package/src/evidence.ts +114 -0
  142. package/src/flashnet/contract.test.ts +101 -0
  143. package/src/flashnet/contract.ts +164 -0
  144. package/src/funnel.mind.test.ts +390 -0
  145. package/src/funnel.ts +73 -8
  146. package/src/index.ts +92 -1
  147. package/src/kaleidoswap/contract.ts +1 -1
  148. package/src/knowledge/bitcoin-copilot.ts +96 -2
  149. package/src/providers/types.ts +18 -0
  150. package/src/qvac/index.ts +1 -0
  151. package/src/qvac/parse.ts +20 -0
  152. package/src/qvac/provider.test.ts +17 -0
  153. package/src/qvac/provider.ts +62 -2
  154. package/src/qvac/stream.test.ts +36 -0
  155. package/src/qvac/stream.ts +54 -1
  156. package/src/recipe/buy-asset-channel.test.ts +5 -0
  157. package/src/recipe/buy-asset-channel.ts +6 -3
  158. package/src/recipe/flashnet-swap.test.ts +114 -0
  159. package/src/recipe/flashnet-swap.ts +266 -0
  160. package/src/recipe/kaleidoswap-atomic.test.ts +24 -3
  161. package/src/recipe/kaleidoswap-atomic.ts +39 -20
  162. package/src/recipe/kaleidoswap-channel-order.test.ts +38 -0
  163. package/src/recipe/kaleidoswap-channel-order.ts +27 -9
  164. package/src/recipe/kaleidoswap-price.ts +7 -1
  165. package/src/recipe/recipe.test.ts +21 -0
  166. package/src/recipe/runner.ts +46 -3
  167. package/src/recipe/swap.ts +16 -1
  168. package/src/tools/mcp.live.test.ts +116 -0
  169. package/src/tools/mcp.parse.test.ts +37 -0
  170. package/src/tools/mcp.ts +55 -9
  171. package/src/wallet/confirm.test.ts +8 -0
  172. package/src/wallet/confirm.ts +1 -0
  173. package/src/wallet/contract.test.ts +10 -0
  174. package/src/wallet/contract.ts +26 -4
@@ -27,10 +27,37 @@ export interface StreamHandlers {
27
27
  onToken?: (token: string) => void;
28
28
  /** The model's `<think>` reasoning, streamed separately. */
29
29
  onThinking?: (token: string) => void;
30
+ /**
31
+ * Cap the `<think>` reasoning at this many tokens. The cap is on TOKENS, not
32
+ * wall-clock seconds — tok/s varies by model and hardware, so a time budget is
33
+ * unreliable; the SDK has no numeric reasoning budget (`reasoning_budget` is
34
+ * only on/off), so we count thinking tokens and stop the run once they exceed
35
+ * this. Omit for unlimited reasoning.
36
+ */
37
+ maxThinkingTokens?: number;
38
+ /**
39
+ * Fires once, the moment the thinking budget is exceeded, so the host can
40
+ * cancel the in-flight run (the SDK keeps generating otherwise). consumeRun
41
+ * stops forwarding deltas after this.
42
+ */
43
+ onThinkingBudgetExceeded?: () => void;
44
+ /** Injectable monotonic-ish wall clock for deterministic timing tests. */
45
+ now?: () => number;
30
46
  }
31
47
 
32
48
  export interface ConsumedTurn extends ParsedTurn {
33
49
  requestId: string;
50
+ /** True when the run was stopped because `<think>` hit `maxThinkingTokens`. */
51
+ thinkingBudgetExceeded?: boolean;
52
+ timing: {
53
+ ttftMs?: number;
54
+ durationMs: number;
55
+ };
56
+ }
57
+
58
+ /** Rough token estimate (~4 chars/token) — same heuristic the context budget uses. */
59
+ function approxTokens(chars: number): number {
60
+ return Math.ceil(chars / 4);
34
61
  }
35
62
 
36
63
  /**
@@ -42,15 +69,41 @@ export async function consumeRun(
42
69
  run: CompletionRunLike,
43
70
  handlers: StreamHandlers = {},
44
71
  ): Promise<ConsumedTurn> {
72
+ const now = handlers.now ?? Date.now;
73
+ const startedAt = now();
74
+ let firstTokenAt: number | undefined;
45
75
  let streamed = '';
76
+ let thinkingChars = 0;
77
+ let budgetExceeded = false;
46
78
  for await (const event of run.events) {
47
79
  if (event.type === 'contentDelta' && typeof event.text === 'string') {
80
+ if (firstTokenAt === undefined && event.text.length > 0) firstTokenAt = now();
48
81
  streamed += event.text;
49
82
  handlers.onToken?.(event.text);
50
83
  } else if (event.type === 'thinkingDelta' && typeof event.text === 'string') {
84
+ if (firstTokenAt === undefined && event.text.length > 0) firstTokenAt = now();
51
85
  handlers.onThinking?.(event.text);
86
+ if (handlers.maxThinkingTokens !== undefined && !budgetExceeded) {
87
+ thinkingChars += event.text.length;
88
+ if (approxTokens(thinkingChars) >= handlers.maxThinkingTokens) {
89
+ budgetExceeded = true;
90
+ handlers.onThinkingBudgetExceeded?.();
91
+ // Stop forwarding; the host cancels the run, so `final` resolves
92
+ // (stopReason 'cancelled') with whatever was produced so far.
93
+ break;
94
+ }
95
+ }
52
96
  }
53
97
  }
54
98
  const final = await run.final;
55
- return { ...finalToTurn(final, streamed), requestId: run.requestId };
99
+ const finishedAt = now();
100
+ return {
101
+ ...finalToTurn(final, streamed),
102
+ requestId: run.requestId,
103
+ thinkingBudgetExceeded: budgetExceeded,
104
+ timing: {
105
+ ...(firstTokenAt === undefined ? {} : { ttftMs: Math.max(0, firstTokenAt - startedAt) }),
106
+ durationMs: Math.max(0, finishedAt - startedAt),
107
+ },
108
+ };
56
109
  }
@@ -54,6 +54,11 @@ describe('extractBuyAsset (deterministic Tier-0)', () => {
54
54
  it('handles comma grouping in the amount', () => {
55
55
  expect(extractBuyAsset('buy 1,000 usdt')).toEqual({ asset: 'USDT', asset_amount: 1000 });
56
56
  });
57
+ it('parses an article/filler between the verb and amount ("buy a 100 usdt channel")', () => {
58
+ expect(extractBuyAsset('buy a 100 usdt channel')).toEqual({ asset: 'USDT', asset_amount: 100 });
59
+ expect(extractBuyAsset('get a 100 usdt inbound channel')).toEqual({ asset: 'USDT', asset_amount: 100 });
60
+ expect(extractBuyAsset('buy and sell 100 usdt')).toBeNull(); // "and" is not filler
61
+ });
57
62
  it('null for a swap (a named source asset ⇒ swap owns it)', () => {
58
63
  expect(extractBuyAsset('buy 0.001 btc with usdt')).toBeNull();
59
64
  expect(extractBuyAsset('swap 10 usdt for btc')).toBeNull();
@@ -48,13 +48,16 @@ const num = (s?: string): number | undefined => {
48
48
  /** Thousands separators, locale-independent (deterministic for tests). */
49
49
  const commas = (n: number): string => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
50
50
 
51
- /** "buy 100 usdt" / "get me 50 xaut" / "i want 200 usdt" / "purchase 10 xaut". */
51
+ /** "buy 100 usdt" / "get me 50 xaut" / "buy a 100 usdt channel" / "purchase 10 xaut". */
52
52
  export function extractBuyAsset(text: string): Record<string, unknown> | null {
53
53
  const t = text.trim();
54
54
  if (NOT_BUY.test(t) || HAS_SOURCE.test(t)) return null;
55
55
  if (!RGB_ASSET.test(t)) return null;
56
- // buy/get/want/acquire/purchase [me] <amount> <asset>
57
- const m = t.match(/\b(?:buy|get|acquire|want|purchase|onboard|need)\b(?:\s+me)?\s+([\d.,]+)\s*([a-z]+)/i);
56
+ // buy/get/want/acquire/purchase [me|a|an|some|new]* <amount> <asset>
57
+ // Filler words (the article in "buy A 100 usdt channel") must not break extraction.
58
+ const m = t.match(
59
+ /\b(?:buy|get|acquire|want|purchase|onboard|need)\b(?:\s+(?:me|a|an|some|new)\b)*\s+([\d.,]+)\s*([a-z]+)/i,
60
+ );
58
61
  if (!m) return null;
59
62
  const asset = normAsset(m[2]);
60
63
  const amount = num(m[1]);
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { flashnetSwapRecipe } from './flashnet-swap.js';
3
+
4
+ describe('flashnetSwapRecipe — selection', () => {
5
+ it('claims swap/buy/sell phrasings with a Flashnet/Spark cue', () => {
6
+ expect(flashnetSwapRecipe.match!('swap 10000 sats with usdb')).toBe(true);
7
+ expect(flashnetSwapRecipe.match!('swap 5000 sats to usdb on flashnet')).toBe(true);
8
+ expect(flashnetSwapRecipe.match!('buy usdb with 1000 sats')).toBe(true);
9
+ expect(flashnetSwapRecipe.match!('exchange btc for usdb on spark')).toBe(true);
10
+ expect(flashnetSwapRecipe.match!('sell 5 usdb')).toBe(true);
11
+ });
12
+
13
+ it('does NOT claim RGB swaps (USDT / XAUT belong to kaleidoswap-atomic)', () => {
14
+ expect(flashnetSwapRecipe.match!('swap 100k sats to usdt')).toBe(false);
15
+ expect(flashnetSwapRecipe.match!('convert btc to xaut')).toBe(false);
16
+ expect(flashnetSwapRecipe.match!('sell 10 usdt for sats')).toBe(false);
17
+ });
18
+
19
+ it('does NOT claim a bare swap without a Flashnet/Spark cue', () => {
20
+ expect(flashnetSwapRecipe.match!('swap 100000 sats')).toBe(false);
21
+ expect(flashnetSwapRecipe.match!('exchange some bitcoin')).toBe(false);
22
+ });
23
+
24
+ it('does NOT claim commerce / receive / channel phrasings', () => {
25
+ expect(flashnetSwapRecipe.match!('buy a gift card with btc')).toBe(false);
26
+ expect(flashnetSwapRecipe.match!('create an invoice for 1000 sats')).toBe(false);
27
+ expect(flashnetSwapRecipe.match!('buy a usdb channel on flashnet')).toBe(false);
28
+ });
29
+
30
+ it('does NOT claim educational questions', () => {
31
+ expect(flashnetSwapRecipe.match!('how does a flashnet swap work?')).toBe(false);
32
+ expect(flashnetSwapRecipe.match!('what is usdb?')).toBe(false);
33
+ expect(flashnetSwapRecipe.match!('explain flashnet')).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe('flashnetSwapRecipe — shape', () => {
38
+ it('extracts the 4 swap slots', () => {
39
+ expect(flashnetSwapRecipe.slots.map((s) => s.name).sort()).toEqual([
40
+ 'amount', 'amount_side', 'from_asset', 'to_asset',
41
+ ]);
42
+ const required = flashnetSwapRecipe.slots.filter((s) => s.required).map((s) => s.name).sort();
43
+ expect(required).toEqual(['from_asset', 'to_asset']);
44
+ });
45
+
46
+ it('forces the model to do slot extraction (not the deterministic regex)', () => {
47
+ expect(flashnetSwapRecipe.forceModelExtract).toBe(true);
48
+ });
49
+
50
+ it('runs the canonical 2-step + final chain (list_pools → simulate → execute)', () => {
51
+ expect(flashnetSwapRecipe.steps.map((s) => s.tool)).toEqual([
52
+ 'flashnet_list_pools',
53
+ 'flashnet_simulate_swap',
54
+ ]);
55
+ expect(flashnetSwapRecipe.final.tool).toBe('flashnet_execute_swap');
56
+ });
57
+
58
+ it('confident only when both assets + amount are extracted', () => {
59
+ expect(flashnetSwapRecipe.confident!({})).toBe(false);
60
+ expect(flashnetSwapRecipe.confident!({ from_asset: 'BTC' })).toBe(false);
61
+ expect(flashnetSwapRecipe.confident!({ from_asset: 'BTC', to_asset: 'USDB' })).toBeFalsy();
62
+ expect(flashnetSwapRecipe.confident!({ from_asset: 'BTC', to_asset: 'USDB', amount: 1000 })).toBe(true);
63
+ });
64
+
65
+ it('has a recipe-level confirm gate (single approval covers the chain)', () => {
66
+ expect(typeof flashnetSwapRecipe.confirm).toBe('function');
67
+ });
68
+ });
69
+
70
+ describe('flashnetSwapRecipe — direction is LITERAL', () => {
71
+ it('simulate step uses from_asset as asset_in (the spent leg) by construction', () => {
72
+ const simStep = flashnetSwapRecipe.steps.find((s) => s.tool === 'flashnet_simulate_swap')!;
73
+ // Build a minimal ctx as runRecipe would: pools result with a BTC/USDB
74
+ // pool, slots saying from=BTC, to=USDB, amount=10000. The args function
75
+ // must put BTC on asset_in and amount=10000 on amount_in — the previous
76
+ // model-driven inversion bug becomes impossible.
77
+ const ctx: any = {
78
+ slots: { from_asset: 'BTC', to_asset: 'USDB', amount: 10000 },
79
+ results: {
80
+ pools: {
81
+ pools: [{
82
+ pool_id: 'pool-xyz',
83
+ asset_a_address: '0e6354aaaa', asset_a_symbol: 'USDB',
84
+ asset_b_address: '020202bbbb', asset_b_symbol: 'BTC',
85
+ }],
86
+ },
87
+ },
88
+ };
89
+ const args = simStep.args(ctx) as any;
90
+ expect(args.pool_id).toBe('pool-xyz');
91
+ // The pool already stores BTC's address (the SDK constant `020202…`);
92
+ // the recipe passes it through directly so the adapter doesn't have to
93
+ // re-resolve the "BTC" ticker.
94
+ expect(args.asset_in_address).toBe('020202bbbb'); // BTC, from the pool
95
+ expect(args.asset_out_address).toBe('0e6354aaaa'); // USDB by symbol
96
+ expect(args.amount_in).toBe('10000');
97
+ });
98
+
99
+ it('execute step computes min_amount_out from the simulated value (not the raw value)', () => {
100
+ const ctx: any = {
101
+ slots: { from_asset: 'BTC', to_asset: 'USDB', amount: 10000 },
102
+ results: {
103
+ pools: { pools: [{ pool_id: 'p', asset_a_address: 'a', asset_a_symbol: 'USDB', asset_b_address: 'b', asset_b_symbol: 'BTC' }] },
104
+ sim: { amount_out: '1472', execution_price: '0.1472' },
105
+ },
106
+ };
107
+ const args = flashnetSwapRecipe.final.args(ctx) as any;
108
+ // Default 50 bps = 0.5% slippage tolerance → floor(1472 * 0.995) = 1464.
109
+ expect(args.min_amount_out).toBe('1464');
110
+ expect(args.max_slippage_bps).toBe(50);
111
+ // BTC (from_asset) → pool's BTC-side address `b`.
112
+ expect(args.asset_in_address).toBe('b');
113
+ });
114
+ });
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Built-in "swap on Flashnet" recipe — Spark-native AMM, deterministic chain.
3
+ *
4
+ * A Flashnet swap on a small model was fragile when left to the agentic loop:
5
+ * the model had to discover the pool, get the asset addresses right, pick the
6
+ * correct simulate DIRECTION (asset_in = what the user spends), compute
7
+ * `min_amount_out` from slippage tolerance, and then thread all of that into
8
+ * execute. With Qwen3-1.7B that often produced an inverted simulate (token →
9
+ * BTC instead of BTC → token), and on the follow-up "yes" the skill context
10
+ * was lost and the model called `flashnet_simulate_swap({})` with no args.
11
+ *
12
+ * "swap 10000 sats with usdb"
13
+ * ↓ 1 model inference (slot extraction: from/to/amount/amount_side)
14
+ * flashnet_list_pools ← discover the right pool deterministically
15
+ * ↓
16
+ * flashnet_simulate_swap ← quote (read-only)
17
+ * ↓ [ONE confirmation gate — shows the real quote in plain English]
18
+ * flashnet_execute_swap ← settle (the spend)
19
+ *
20
+ * The whole chain runs with 1 LLM inference total (slot extraction), same
21
+ * pattern as `kaleidoswapAtomicRecipe`. The host's `flashnet_list_pools` is
22
+ * side-agnostic and labels symbols, so picking the right pool from the
23
+ * extracted asset pair is cheap and reliable.
24
+ *
25
+ * Slippage: default `max_slippage_bps = 50` (0.5%). The runner computes
26
+ * `min_amount_out = floor(amount_out × (1 − bps/10000))` from the simulate
27
+ * result before execute — never trusts the simulated value as-is.
28
+ *
29
+ * Asset taxonomy guard: matches ONLY when a Flashnet cue is present
30
+ * (flashnet/usdb/spark), so RGB swaps (USDT/XAUT) still go to the
31
+ * KaleidoSwap atomic recipe. The two recipes are disjoint.
32
+ */
33
+
34
+ import type { Recipe, RecipeContext } from './types.js';
35
+ import { extractSwap } from './swap.js';
36
+
37
+ // Flashnet cue — same set the kaleidoswap-atomic recipe defers ON, so the
38
+ // two are disjoint by construction.
39
+ const FLASHNET_CUE = /\b(flashnet|usdb|spark)\b/i;
40
+ // Spark-token tickers that imply a Flashnet swap even without "flashnet" in
41
+ // the text. Keep this list small and Spark-specific — never includes RGB
42
+ // assets (USDT, XAUT, gold).
43
+ const SPARK_TOKEN = /\b(usdb)\b/i;
44
+ // Generic swap-intent verbs that pair with an asset name. "buy/sell/get" join
45
+ // the swap intent only when an ASSET is named (so "buy a gift card" doesn't
46
+ // route here — that's bitrefill territory).
47
+ const SWAP_VERB = /\b(swap|exchange|convert|trade)\b/i;
48
+ const BUY_VERB = /\b(buy|sell|get|purchase|acquire)\b/i;
49
+ const ASSET = /\b(btc|bitcoin|sats?|usdb)\b/i;
50
+ const NON_SWAP = /\b(gift\s?card|top-?up|esim|voucher|invoice|address|channel|inbound|liquidity|lsps?\b)\b/i;
51
+
52
+ const FLASHNET_INTENT = (t: string) => {
53
+ // Educational questions go to the agentic skill (which can call list_pools
54
+ // for an honest read).
55
+ if (/\b(why|how|what|when|explain|tell\s+me|do\s+I\s+need|should\s+I|can\s+I)\b/i.test(t)) return false;
56
+ // Must have a Flashnet/Spark cue OR name a Spark-native token like USDB,
57
+ // so this never grabs an RGB swap (those go to kaleidoswap-atomic).
58
+ if (!FLASHNET_CUE.test(t) && !SPARK_TOKEN.test(t)) return false;
59
+ if (NON_SWAP.test(t)) return false;
60
+ if (SWAP_VERB.test(t)) return true;
61
+ if (BUY_VERB.test(t) && ASSET.test(t)) return true;
62
+ return false;
63
+ };
64
+
65
+ // ── Shape helpers (decoupled from the SDK's exact field names; the host
66
+ // adapter normalizes responses to these). ────────────────────────────────
67
+ interface PoolRow {
68
+ pool_id: string;
69
+ asset_a_address: string;
70
+ asset_b_address: string;
71
+ asset_a_symbol?: string;
72
+ asset_b_symbol?: string;
73
+ curve_type?: string;
74
+ tvl_asset_b?: string;
75
+ fee_bps?: number;
76
+ }
77
+ interface ListPoolsResult { pools: PoolRow[]; total_count?: number }
78
+ interface SimulateResult {
79
+ amount_out?: string;
80
+ execution_price?: string;
81
+ price_impact_pct?: string;
82
+ fee_paid_asset_in?: string;
83
+ warning?: string;
84
+ }
85
+ interface ExecuteResult {
86
+ accepted?: boolean;
87
+ request_id?: string;
88
+ amount_out?: string;
89
+ execution_price?: string;
90
+ }
91
+
92
+ /**
93
+ * Resolve the asset address pair from a pool row + the user's requested
94
+ * (from_asset, to_asset) symbols. The pool stores ONE order (typically the
95
+ * non-BTC token on side A, BTC on side B), but the user can phrase the swap
96
+ * either direction.
97
+ *
98
+ * Strategy — deduce the unknown side from the known one:
99
+ * 1. Match each leg to a pool side by symbol or by the BTC-ticker family.
100
+ * 2. If one leg resolves to a side, the other leg MUST be the opposite side
101
+ * (a pool only has two assets). This is the case that prevented an
102
+ * earlier "asset_in == asset_out" bug on regtest, where the non-BTC
103
+ * side carries no symbol.
104
+ * 3. Only as a last resort fall back to the "BTC on side B, token on side
105
+ * A" default — true for almost every regtest pool we've seen.
106
+ */
107
+ function isBtcTicker(s: string): boolean {
108
+ return s === 'BTC' || s === 'SATS' || s === 'BITCOIN';
109
+ }
110
+
111
+ function resolveLegAddresses(pool: PoolRow, from: string, to: string): { fromAddr: string; toAddr: string } {
112
+ const f = (from ?? '').toUpperCase();
113
+ const t = (to ?? '').toUpperCase();
114
+ const aSym = (pool.asset_a_symbol ?? '').toUpperCase();
115
+ const bSym = (pool.asset_b_symbol ?? '').toUpperCase();
116
+
117
+ // Which side (if any) does each leg map to?
118
+ const sideOf = (sym: string): 'a' | 'b' | undefined => {
119
+ if (sym === aSym && aSym) return 'a';
120
+ if (sym === bSym && bSym) return 'b';
121
+ return undefined;
122
+ };
123
+
124
+ let fSide = sideOf(f);
125
+ let tSide = sideOf(t);
126
+
127
+ // 2. Deduce the unknown side from the known one.
128
+ if (!fSide && tSide) fSide = tSide === 'a' ? 'b' : 'a';
129
+ if (!tSide && fSide) tSide = fSide === 'a' ? 'b' : 'a';
130
+
131
+ // 3. Last-resort default — only when NEITHER side resolved. BTC on side B
132
+ // is the canonical layout for the Spark/Flashnet pools we've observed.
133
+ if (!fSide && !tSide) {
134
+ fSide = isBtcTicker(f) ? 'b' : 'a';
135
+ tSide = fSide === 'a' ? 'b' : 'a';
136
+ }
137
+
138
+ const addr = (side: 'a' | 'b'): string => (side === 'a' ? pool.asset_a_address : pool.asset_b_address);
139
+ return { fromAddr: addr(fSide!), toAddr: addr(tSide!) };
140
+ }
141
+
142
+ /** Compute min_amount_out = floor(amount_out × (1 − bps/10000)). String-safe. */
143
+ function computeMinAmountOut(amountOut: string | undefined, slippageBps: number): string {
144
+ if (!amountOut) return '0';
145
+ try {
146
+ const out = BigInt(String(amountOut).replace(/[^\d]/g, ''));
147
+ const num = BigInt(10_000 - Math.max(0, Math.min(5_000, slippageBps)));
148
+ const min = (out * num) / 10_000n;
149
+ return min.toString();
150
+ } catch {
151
+ return '0';
152
+ }
153
+ }
154
+
155
+ const DEFAULT_SLIPPAGE_BPS = 50;
156
+
157
+ export const flashnetSwapRecipe: Recipe = {
158
+ name: 'flashnet-swap',
159
+ description:
160
+ "Swap on Flashnet (Spark-native AMM): list pool → simulate → confirm once → execute. The user's Spark wallet IS the swap account.",
161
+ match: (t) => FLASHNET_INTENT(t),
162
+ triggers: ['flashnet', 'usdb', 'swap', 'exchange', 'convert', 'trade'],
163
+ slots: [
164
+ { name: 'from_asset', type: 'string', description: 'Asset the user SPENDS (BTC / USDB). "swap 10000 sats with usdb" → from_asset=BTC. "sell 1 usdb" → from_asset=USDB.', required: true },
165
+ { name: 'to_asset', type: 'string', description: 'Asset the user GETS (BTC / USDB). "swap 10000 sats with usdb" → to_asset=USDB. "buy USDB with sats" → to_asset=USDB.', required: true },
166
+ { name: 'amount', type: 'number', description: 'The numeric amount the user named. e.g. "swap 10000 sats with usdb" → amount=10000. Always in the asset on `amount_side`.' },
167
+ { name: 'amount_side', type: 'string', description: "Which leg the amount is denominated in: 'from' (the spent asset) or 'to' (the received asset). Default 'from'. 'buy 10 usdb with sats' → 'to'." },
168
+ ],
169
+ // extractSwap is the same regex extractor the KaleidoSwap atomic recipe
170
+ // uses; it returns {from_asset, to_asset, amount, amount_side?}. With
171
+ // forceModelExtract=true the runner ignores the det result and always asks
172
+ // the model — but the det extraction still feeds the Funnel's pre-filter so
173
+ // bare "buy/sell" with a Spark asset routes here even before the model runs.
174
+ extract: extractSwap,
175
+ forceModelExtract: true,
176
+ confident: (s) => !!s.from_asset && !!s.to_asset && !!s.amount,
177
+ steps: [
178
+ // 1. Discover the pool. Side-agnostic on the host side; one row is enough.
179
+ {
180
+ tool: 'flashnet_list_pools',
181
+ as: 'pools',
182
+ args: (ctx) => ({
183
+ asset_a: String(ctx.slots.from_asset ?? '').toUpperCase(),
184
+ asset_b: String(ctx.slots.to_asset ?? '').toUpperCase(),
185
+ sort: 'TVL_DESC',
186
+ limit: 5,
187
+ }),
188
+ },
189
+ // 2. Quote the swap. Direction is LITERAL: asset_in = what the user
190
+ // spends (`from_asset`), asset_out = what they get (`to_asset`). The
191
+ // runner — not the model — assembles this, so the inverted-direction
192
+ // bug is impossible by construction.
193
+ {
194
+ tool: 'flashnet_simulate_swap',
195
+ as: 'sim',
196
+ args: (ctx) => {
197
+ const r = ctx.results.pools as ListPoolsResult | undefined;
198
+ const pool = r?.pools?.[0];
199
+ if (!pool) throw new Error(`No Flashnet pool found for ${ctx.slots.from_asset} ↔ ${ctx.slots.to_asset}.`);
200
+ const { fromAddr, toAddr } = resolveLegAddresses(pool, String(ctx.slots.from_asset ?? ''), String(ctx.slots.to_asset ?? ''));
201
+ return {
202
+ pool_id: pool.pool_id,
203
+ asset_in_address: fromAddr,
204
+ asset_out_address: toAddr,
205
+ amount_in: String(ctx.slots.amount ?? ''),
206
+ };
207
+ },
208
+ },
209
+ ],
210
+ // 3. Settle. Confirmation-gated at the recipe level (see runner.ts).
211
+ // min_amount_out is computed here — never the raw simulated value — so a
212
+ // moving pool can't fill at a worse price than `slippage_bps` allows.
213
+ final: {
214
+ tool: 'flashnet_execute_swap',
215
+ as: 'exec',
216
+ args: (ctx) => {
217
+ const r = ctx.results.pools as ListPoolsResult | undefined;
218
+ const pool = r?.pools?.[0];
219
+ const sim = ctx.results.sim as SimulateResult | undefined;
220
+ if (!pool || !sim) throw new Error('Flashnet swap: missing pool or simulation result.');
221
+ const { fromAddr, toAddr } = resolveLegAddresses(pool, String(ctx.slots.from_asset ?? ''), String(ctx.slots.to_asset ?? ''));
222
+ return {
223
+ pool_id: pool.pool_id,
224
+ asset_in_address: fromAddr,
225
+ asset_out_address: toAddr,
226
+ amount_in: String(ctx.slots.amount ?? ''),
227
+ min_amount_out: computeMinAmountOut(sim.amount_out, DEFAULT_SLIPPAGE_BPS),
228
+ max_slippage_bps: DEFAULT_SLIPPAGE_BPS,
229
+ };
230
+ },
231
+ },
232
+ // ONE confirmation, fired after simulate / before execute, with real numbers.
233
+ confirm: (ctx: RecipeContext) => {
234
+ const sim = ctx.results.sim as SimulateResult | undefined;
235
+ const from = fmtAmount(ctx.slots.amount, String(ctx.slots.from_asset ?? ''));
236
+ const to = fmtAmount(sim?.amount_out, String(ctx.slots.to_asset ?? ''));
237
+ const impact = sim?.price_impact_pct ? ` · ${sim.price_impact_pct} price impact` : '';
238
+ const warn = sim?.warning ? ` · ${sim.warning}` : '';
239
+ return `Swap ${from} → ~${to} on Flashnet (slippage cap ${DEFAULT_SLIPPAGE_BPS / 100}%)${impact}${warn}. Proceed?`;
240
+ },
241
+ summary: (ctx) => {
242
+ const sim = ctx.results.sim as SimulateResult | undefined;
243
+ const exec = ctx.results.exec as ExecuteResult | undefined;
244
+ const from = fmtAmount(ctx.slots.amount, String(ctx.slots.from_asset ?? ''));
245
+ const got = fmtAmount(exec?.amount_out ?? sim?.amount_out, String(ctx.slots.to_asset ?? ''));
246
+ if (exec?.accepted === false) {
247
+ return `Flashnet swap rejected: ${(exec as any)?.error ?? 'unknown error'}.`;
248
+ }
249
+ return `Flashnet swap submitted: ${from} → ${got}. request_id=${exec?.request_id ?? '?'}.`;
250
+ },
251
+ };
252
+
253
+ /**
254
+ * Format an amount + asset for user display. BTC is rendered as "X,XXX sats"
255
+ * (BTC is the asset, sats is the unit; the on-the-wire amount is already in
256
+ * sats so no conversion is needed). Anything else is rendered as
257
+ * "N TICKER" with thousand-separators.
258
+ */
259
+ function fmtAmount(amount: unknown, asset: string): string {
260
+ const t = (asset ?? '').toUpperCase();
261
+ const n = Number(amount);
262
+ if (!Number.isFinite(n)) return `${amount ?? '?'} ${t}`;
263
+ const sep = n.toLocaleString('en-US');
264
+ if (t === 'BTC' || t === 'SATS' || t === 'BITCOIN') return `${sep} sats`;
265
+ return `${sep} ${t}`;
266
+ }
@@ -44,7 +44,7 @@ function buildStubs(captured: { name: string; args: any }[]) {
44
44
  ]),
45
45
  new InProcessToolSource('rln', [
46
46
  tool('rln_get_node_info', { pubkey: '03c31dae' }),
47
- tool('rln_whitelist_swap', { ok: true }, /* spend */ true),
47
+ tool('rln_atomic_taker', { ok: true }, /* spend */ true),
48
48
  ]),
49
49
  ]);
50
50
  }
@@ -66,6 +66,27 @@ describe('kaleidoswapAtomicRecipe — selection', () => {
66
66
  it('does not trigger on a balance question', () => {
67
67
  expect(kaleidoswapAtomicRecipe.match!('what is my balance')).toBe(false);
68
68
  });
69
+ it('DEFERS to Flashnet when a Flashnet/Spark cue is present (venue split)', () => {
70
+ // These belong to the agentic flashnet-swaps skill, not the KaleidoSwap
71
+ // maker recipe — so the recipe must NOT claim them.
72
+ expect(kaleidoswapAtomicRecipe.match!('swap 10000 sats with asset of your choice in flashnet')).toBe(false);
73
+ expect(kaleidoswapAtomicRecipe.match!('swap 5000 sats to usdb')).toBe(false);
74
+ expect(kaleidoswapAtomicRecipe.match!('swap btc to usdb on spark')).toBe(false);
75
+ });
76
+ it('does NOT claim a bare swap with no venue/asset cue (falls to agentic)', () => {
77
+ // Ambiguous — a swap always needs a target asset; let the skill tier ask
78
+ // or pick the connected venue rather than grabbing it for the maker.
79
+ expect(kaleidoswapAtomicRecipe.match!('swap 100000 sats')).toBe(false);
80
+ expect(kaleidoswapAtomicRecipe.match!('exchange some bitcoin')).toBe(false);
81
+ });
82
+ it('still claims swaps that name an RGB/maker asset', () => {
83
+ expect(kaleidoswapAtomicRecipe.match!('swap 100000 sats to usdt')).toBe(true);
84
+ expect(kaleidoswapAtomicRecipe.match!('convert btc to xaut')).toBe(true);
85
+ });
86
+ it('does not turn portfolio review or explicit no-trade language into a swap', () => {
87
+ expect(kaleidoswapAtomicRecipe.match!('review my portfolio allocation but do not trade')).toBe(false);
88
+ expect(kaleidoswapAtomicRecipe.match!('analyze my holdings without trading')).toBe(false);
89
+ });
69
90
  });
70
91
 
71
92
  describe('kaleidoswapAtomicRecipe — forceModelExtract (less deterministic slot parsing)', () => {
@@ -130,7 +151,7 @@ describe('kaleidoswapAtomicRecipe — full chain', () => {
130
151
  'kaleidoswap_get_quote',
131
152
  'kaleidoswap_atomic_init',
132
153
  'rln_get_node_info',
133
- 'rln_whitelist_swap',
154
+ 'rln_atomic_taker',
134
155
  'kaleidoswap_atomic_execute',
135
156
  ]);
136
157
  });
@@ -157,7 +178,7 @@ describe('kaleidoswapAtomicRecipe — full chain', () => {
157
178
  provider: refusingProvider, tools, onConfirm: async () => ({ approved: true }),
158
179
  slots: { from_asset: 'USDT', to_asset: 'BTC', amount: 10, amount_side: 'from' },
159
180
  });
160
- const whitelist = captured.find((c) => c.name === 'rln_whitelist_swap')!;
181
+ const whitelist = captured.find((c) => c.name === 'rln_atomic_taker')!;
161
182
  expect(whitelist.args).toEqual({ swapstring: 'SWAP/abc/def' });
162
183
  const exe = captured.find((c) => c.name === 'kaleidoswap_atomic_execute')!;
163
184
  expect(exe.args).toEqual({
@@ -12,7 +12,7 @@
12
12
  * ↓ [ONE confirmation gate — shows the real quote numbers]
13
13
  * kaleidoswap_atomic_init ← MAKER locks the swap → swapstring, payment_hash
14
14
  * rln_get_node_info ← NODE read pubkey (= taker_pubkey)
15
- * rln_whitelist_swap ← NODE accept the swapstring
15
+ * rln_atomic_taker ← NODE whitelist the swapstring (taker accepts)
16
16
  * kaleidoswap_atomic_execute ← MAKER settle (final)
17
17
  *
18
18
  * `forceModelExtract` ensures the model is always consulted for slot parsing
@@ -34,23 +34,38 @@
34
34
  import type { Recipe, RecipeContext } from './types.js';
35
35
  import { extractSwap } from './swap.js';
36
36
 
37
- // Fire on swap intent "swap/exchange/convert/trade", or "buy/sell/get" when a
38
- // crypto asset is named (so "buy one usdt" routes here, but "buy a gift card"
39
- // does not). PRICE / rate / "how much" questions are read-only and go to
40
- // `kaleidoswapPriceRecipe` instead keep them out of this match.
41
- const ASSET = /\b(btc|bitcoin|sats?|usdt|tether|xaut|gold)\b/i;
37
+ // KaleidoSwap is a BTC↔RGB ATOMIC swap venue (maker + RLN node). It is NOT the
38
+ // only swap venue anymore Flashnet (Spark-native AMM, BTC↔Spark tokens like
39
+ // USDB) is a sibling, handled by the agentic `flashnet-swaps` skill. The Funnel
40
+ // runs recipes BEFORE skills, so a greedy "any swap word" match here would
41
+ // monopolize every swap and starve Flashnet. To let both coexist, this recipe
42
+ // only claims swaps that point at ITS venue:
43
+ // - names an RGB/maker asset or the venue itself (RGB_CUE), AND
44
+ // - does NOT name a Flashnet/Spark cue (FLASHNET_CUE → defer to the skill).
45
+ // A bare "swap" with no venue cue falls through to the agentic tier, where the
46
+ // skill selector disambiguates (or the model asks).
47
+ const RGB_CUE = /\b(usdt|tether|xaut|gold|rgb|kaleidoswap|kaleido|atomic)\b/i;
48
+ const FLASHNET_CUE = /\b(flashnet|usdb|spark)\b/i;
42
49
  const SWAP_INTENT = (t: string) => {
43
50
  // Explanatory / educational questions → route to RAG-backed agentic answer,
44
51
  // not the deterministic spend chain.
45
52
  if (/\b(why|how|what|when|explain|tell\s+me|do\s+I\s+need|should\s+I|can\s+I)\b/i.test(t)) return false;
46
- if (/\b(swap|exchange|convert|trade)\b/i.test(t)) return true;
47
- if (
53
+ // Flashnet owns its venue — defer to the flashnet-swaps skill.
54
+ if (FLASHNET_CUE.test(t)) return false;
55
+ // Portfolio analysis belongs to the portfolio skill, which reads balances
56
+ // and targets before suggesting any action. A mention of "trade" in a review
57
+ // request—especially "do not trade"—must never become an immediate swap.
58
+ if (/\b(portfolio|allocation|holdings|rebalance|rebalancing)\b/i.test(t)) return false;
59
+ if (/\b(?:do\s+not|don't|without|no)\s+(?:place\s+(?:a\s+)?)?(?:trade|swap|buy|sell|trading)\b/i.test(t)) return false;
60
+ const swapVerb = /\b(swap|exchange|convert|trade)\b/i.test(t);
61
+ const buyVerb =
48
62
  /\b(buy|sell|get|purchase|acquire)\b/i.test(t) &&
49
- ASSET.test(t) &&
50
63
  // Exclude commerce / receive / LSPS1 channel-order phrasings that share
51
64
  // the buy/get verb. "Buy a USDT channel" is a channel order, not a swap.
52
- !/\b(gift\s?card|top-?up|esim|voucher|invoice|address|channel|inbound|liquidity|lsps?\b)\b/i.test(t)
53
- ) return true;
65
+ !/\b(gift\s?card|top-?up|esim|voucher|invoice|address|channel|inbound|liquidity|lsps?\b)\b/i.test(t);
66
+ // Only claim the swap when an RGB/maker asset (or the venue) is named, so a
67
+ // bare/ambiguous "swap" or a Flashnet-asset swap doesn't get grabbed here.
68
+ if (swapVerb || buyVerb) return RGB_CUE.test(t);
54
69
  return false;
55
70
  };
56
71
 
@@ -62,7 +77,7 @@ interface QuoteResult {
62
77
  to_amount_display?: string;
63
78
  fee_display?: string;
64
79
  }
65
- interface InitResult { swapstring?: string; payment_hash?: string }
80
+ interface InitResult { swapstring?: string; payment_hash?: string; atomic_id?: string }
66
81
  interface NodeInfo { pubkey?: string }
67
82
 
68
83
  export const kaleidoswapAtomicRecipe: Recipe = {
@@ -72,10 +87,10 @@ export const kaleidoswapAtomicRecipe: Recipe = {
72
87
  match: (t) => SWAP_INTENT(t),
73
88
  triggers: ['swap', 'exchange', 'convert', 'trade', 'buy', 'sell'],
74
89
  slots: [
75
- { name: 'from_asset', type: 'string', description: 'Asset to spend (BTC / USDT / XAUT)', required: true },
76
- { name: 'to_asset', type: 'string', description: 'Asset to receive (BTC / USDT / XAUT)', required: true },
77
- { name: 'amount', type: 'number', description: 'The amount the user named' },
78
- { name: 'amount_side', type: 'string', description: "Which leg the amount is on: 'from' (sell/swap) or 'to' (buy)" },
90
+ { name: 'from_asset', type: 'string', description: 'Asset to spend (BTC / USDT / XAUT). Example: "swap 10 usdt to btc" → from_asset=USDT', required: true },
91
+ { name: 'to_asset', type: 'string', description: 'Asset to receive (BTC / USDT / XAUT). Example: "buy 1 usdt" → to_asset=USDT', required: true },
92
+ { name: 'amount', type: 'number', description: 'The amount the user named (in from_asset units for sell, to_asset for buy). E.g. "buy 1 usdt" amount=1; "swap 100000 sats" amount=100000' },
93
+ { name: 'amount_side', type: 'string', description: "Which leg the amount is on: 'from' (sell/swap) or 'to' (buy). Use examples in descriptions and 'buy X Y' means to_asset." },
79
94
  ],
80
95
  // Keep the fast `extract` for the Funnel's cheap pre-filter (so "buy 1 usdt"
81
96
  // reliably enters the recipe branch instead of falling to free agentic).
@@ -122,10 +137,11 @@ export const kaleidoswapAtomicRecipe: Recipe = {
122
137
  as: 'node',
123
138
  args: () => ({}),
124
139
  },
125
- // 4. NODE: whitelist the maker's swapstring (accept the swap). Ungated —
126
- // covered by the single confirm above.
140
+ // 4. NODE: the taker whitelists the maker's swapstring (accept the swap).
141
+ // Exposed by kaleido-mcp as `rln_atomic_taker` (calls rln.whitelistSwap).
142
+ // Ungated — covered by the single confirm above.
127
143
  {
128
- tool: 'rln_whitelist_swap',
144
+ tool: 'rln_atomic_taker',
129
145
  as: 'whitelist',
130
146
  args: (ctx) => {
131
147
  const init = ctx.results.init as InitResult | undefined;
@@ -158,6 +174,9 @@ export const kaleidoswapAtomicRecipe: Recipe = {
158
174
  const q = ctx.results.quote as QuoteResult | undefined;
159
175
  const from = q?.from_amount_display ?? `${ctx.slots.amount} ${ctx.slots.from_asset}`;
160
176
  const to = q?.to_amount_display ?? String(ctx.slots.to_asset);
161
- return `Swap submitted: ${from} → ${to}. Settling now ask me to check the status.`;
177
+ const init = ctx.results.init as InitResult | undefined;
178
+ const id = init?.atomic_id || init?.payment_hash || '?';
179
+ return `remember: atomic swap atomic_id=${id} (for later kaleidoswap_atomic_status checks).
180
+ Swap submitted: ${from} → ${to}. To check status later, call: kaleidoswap_atomic_status(atomic_id=${id}). Say "check my swap status" and I will recall + poll automatically.`;
162
181
  },
163
182
  };