balchemy 0.1.22 → 0.1.24

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.
@@ -4,7 +4,7 @@ import { AgentLoop, connectMcp } from "@balchemyai/agent-sdk";
4
4
  import { ChatAgent } from "./ChatAgent.js";
5
5
  import { buildSetupRequiredMessage, getInitialSetupStep, isSetupReady, parseNetworkSelection, parseSetupStatusSnapshot, } from "./setup-guidance.js";
6
6
  import { buildStrategyUpdateArgs } from "./session-sync.js";
7
- import { loadAgent } from "../agent-store.js";
7
+ import { loadAgent, saveAgent } from "../agent-store.js";
8
8
  /** Truncate verbose API errors (429 JSON blobs, stack traces) to a readable one-liner. */
9
9
  function truncateError(raw) {
10
10
  // Extract HTTP status code if present
@@ -53,6 +53,23 @@ function parseJsonObject(text) {
53
53
  return null;
54
54
  }
55
55
  }
56
+ function parseJsonObjectLoose(text) {
57
+ const direct = parseJsonObject(text);
58
+ if (direct)
59
+ return direct;
60
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
61
+ if (fenced?.[1]) {
62
+ const parsed = parseJsonObject(fenced[1].trim());
63
+ if (parsed)
64
+ return parsed;
65
+ }
66
+ const first = text.indexOf("{");
67
+ const last = text.lastIndexOf("}");
68
+ if (first >= 0 && last > first) {
69
+ return parseJsonObject(text.slice(first, last + 1));
70
+ }
71
+ return null;
72
+ }
56
73
  function asRecord(value) {
57
74
  return typeof value === "object" && value !== null && !Array.isArray(value)
58
75
  ? value
@@ -108,6 +125,25 @@ function mergeWallets(...groups) {
108
125
  }
109
126
  return wallets;
110
127
  }
128
+ function mergeLatestWallets(...groups) {
129
+ const latest = new Map();
130
+ for (const group of groups) {
131
+ for (const wallet of group) {
132
+ latest.set(wallet.chain, wallet);
133
+ }
134
+ }
135
+ const ordered = [];
136
+ const solana = latest.get("solana");
137
+ const base = latest.get("base");
138
+ if (solana)
139
+ ordered.push(solana);
140
+ if (base)
141
+ ordered.push(base);
142
+ return ordered;
143
+ }
144
+ function walletAddressLabel(chain) {
145
+ return chain === "solana" ? "Solana trading wallet" : "Base trading wallet";
146
+ }
111
147
  function readRecords(value) {
112
148
  if (!Array.isArray(value))
113
149
  return [];
@@ -278,6 +314,67 @@ function parseTradeLimits(value) {
278
314
  ...(maxTradeUsd !== undefined && Number.isFinite(maxTradeUsd) ? { maxTradeUsd } : {}),
279
315
  };
280
316
  }
317
+ function readStringArray(value) {
318
+ if (!Array.isArray(value))
319
+ return [];
320
+ return value.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : []);
321
+ }
322
+ function parseStrategyReviewResponse(text) {
323
+ const parsed = parseJsonObjectLoose(text);
324
+ if (!parsed)
325
+ return null;
326
+ const ready = parsed.ready === true;
327
+ const summary = firstString(parsed.summary, parsed.strategy, parsed.normalizedStrategy) ?? "";
328
+ const followUp = firstString(parsed.followUp, parsed.follow_up, parsed.question) ?? "";
329
+ const missing = readStringArray(parsed.missing);
330
+ const limits = parseTradeLimits(summary);
331
+ const maxTradeSol = firstNumber(parsed, ["maxTradeSol", "max_trade_sol"]) ?? limits.maxTradeSol;
332
+ const maxTradeUsd = firstNumber(parsed, ["maxTradeUsd", "max_trade_usd"]) ?? limits.maxTradeUsd;
333
+ return {
334
+ ready,
335
+ summary,
336
+ followUp,
337
+ missing,
338
+ ...(maxTradeSol !== undefined ? { maxTradeSol } : {}),
339
+ ...(maxTradeUsd !== undefined ? { maxTradeUsd } : {}),
340
+ };
341
+ }
342
+ function fallbackStrategyReview(chain, notes) {
343
+ const summary = notes.join("\n").trim();
344
+ const limits = parseTradeLimits(summary);
345
+ const lower = summary.toLowerCase();
346
+ const missing = [];
347
+ if (chain === "solana" && limits.maxTradeSol === undefined) {
348
+ missing.push("max SOL per trade");
349
+ }
350
+ if (chain === "base" && limits.maxTradeUsd === undefined) {
351
+ missing.push("max USD/USDC per trade");
352
+ }
353
+ if (!/(entry|filter|hacim|volume|mcap|market cap|holder|liquidity|giris|giriş)/i.test(summary)) {
354
+ missing.push("entry filters");
355
+ }
356
+ if (!/(stop|loss|zarar|sl\b)/i.test(lower)) {
357
+ missing.push("stop loss");
358
+ }
359
+ if (!/(take profit|profit|kar|sat|sell|tp\b|x\b)/i.test(lower)) {
360
+ missing.push("take profit");
361
+ }
362
+ if (!/(max.*position|position|pozisyon|concurrent|ayn[ıi] anda)/i.test(lower)) {
363
+ missing.push("max open positions");
364
+ }
365
+ if (!/(avoid|kaçın|kacin|yasak|alma|blacklist|tokenleri|categories|kategori)/i.test(lower)) {
366
+ missing.push("avoid rules");
367
+ }
368
+ return {
369
+ ready: summary.length >= 20 && missing.length === 0,
370
+ summary,
371
+ missing,
372
+ followUp: missing.length > 0
373
+ ? `I still need this before live setup: ${missing.join(", ")}. Send only the missing parts; I will merge them.`
374
+ : "",
375
+ ...limits,
376
+ };
377
+ }
281
378
  function formatChains(chains) {
282
379
  return chains.map((chain) => chain === "solana" ? "Solana" : "Base").join(" + ");
283
380
  }
@@ -319,9 +416,11 @@ export class AgentBridge {
319
416
  pendingLoopConfig = null;
320
417
  setupPollTimer = null;
321
418
  setupFlow = null;
419
+ knownWallets = [];
322
420
  constructor(config, setters) {
323
421
  this.config = config;
324
422
  this.setters = setters;
423
+ this.knownWallets = this.loadStoredWallets();
325
424
  // Replay-protected fetch for MCP calls
326
425
  this.replayFetch = async (url, init) => {
327
426
  const headers = new Headers(init?.headers);
@@ -435,6 +534,7 @@ export class AgentBridge {
435
534
  provider: resolveProviderLabel(this.config.llmProvider, this.config.llmBaseUrl),
436
535
  model: this.config.llmModel,
437
536
  }));
537
+ this.syncKnownWalletsToStatus();
438
538
  // Input is ready now. Setup must be deterministic; do not let an LLM drive it.
439
539
  if (setupComplete) {
440
540
  void this.greet(true);
@@ -452,7 +552,7 @@ export class AgentBridge {
452
552
  }
453
553
  try {
454
554
  const prompt = "Check my portfolio and status, then greet me. Tell me my balance, wallets, and current strategy. Keep it brief and do not narrate tool calls.";
455
- const reply = await this.chatAgent.chat(prompt, (name, result) => {
555
+ const reply = await this.chatAgent.chat(this.withRuntimeContext(prompt), (name, result) => {
456
556
  this.applyToolResult(name, result);
457
557
  if (name !== "setup_agent") {
458
558
  this.addSystemMessage(`Tool: ${name}`);
@@ -498,7 +598,7 @@ export class AgentBridge {
498
598
  if (!this.chatAgent)
499
599
  return;
500
600
  try {
501
- const reply = await this.chatAgent.chat(text, (name, result) => {
601
+ const reply = await this.chatAgent.chat(this.withRuntimeContext(text), (name, result) => {
502
602
  this.applyToolResult(name, result);
503
603
  this.addSystemMessage(`Tool: ${name}`);
504
604
  }, (preview) => this.setters.confirmTrade(preview));
@@ -542,6 +642,8 @@ export class AgentBridge {
542
642
  const step = getInitialSetupStep(snapshot);
543
643
  this.setupFlow = {
544
644
  step,
645
+ rootWalletBound: snapshot.developerWalletBound === true,
646
+ ...(snapshot.rootWalletKind ? { rootWalletKind: snapshot.rootWalletKind } : {}),
545
647
  ...(snapshot.selectedChains && snapshot.selectedChains.length > 0 ? { selectedChains: snapshot.selectedChains } : {}),
546
648
  };
547
649
  this.addAgentMessage(`${buildSetupRequiredMessage(snapshot)}\n\n${this.setupPromptFor(this.setupFlow.step)}`);
@@ -555,19 +657,85 @@ export class AgentBridge {
555
657
  case "networks":
556
658
  return "Which networks should this agent trade on: Solana, Base, or both?";
557
659
  case "solana-recovery-wallet":
558
- return "Paste your Solana recovery/withdrawal wallet address. This is where SOL/SPL withdrawals go, and it is separate from the Solana trading wallet I create for execution.";
660
+ return "Paste your Solana developer/recovery wallet address. For Solana-only setup this becomes the root master-key wallet; for Base+Solana it is the Solana withdrawal/recovery wallet. It is separate from the Solana trading wallet I create for execution.";
559
661
  case "solana-recovery-wallet-confirm":
560
- return "Paste the same Solana recovery/withdrawal wallet again to confirm it.";
662
+ return "Paste the same Solana wallet again to confirm it.";
561
663
  case "slippage":
562
664
  return "Set slippage in percent or bps. Examples: 1% = 100 bps, 3% = 300 bps, 5% = 500 bps.";
563
665
  case "strategy":
564
- return "Send the hard limits and strategy for this chain. Include max trade size, entry filters, stop loss, take profit, max positions, and tokens/categories to avoid.";
666
+ return "Describe the strategy in natural language. I will review it with AI, ask for missing risk details, and only then ask you to confirm.";
565
667
  case "strategy-confirm":
566
668
  return "Confirm with 'yes'/'evet', or say 'no'/'hayir' to rewrite the strategy.";
567
669
  case "subscriptions":
568
670
  return "Create monitoring subscriptions now? For Solana new launches I can enable launch monitoring. Answer yes/evet or no/hayir.";
569
671
  }
570
672
  }
673
+ setupPromptForStrategyChain(chain) {
674
+ const maxTrade = chain === "solana"
675
+ ? "max SOL per trade"
676
+ : "max USD/USDC per trade";
677
+ return [
678
+ `${chainTitle(chain)} strategy:`,
679
+ "Describe what you want the agent to trade and how strict it must be.",
680
+ `Include ${maxTrade}, entry filters, stop loss, take profit, max open positions, and avoid rules.`,
681
+ "You can write it messy; I will ask follow-ups if anything critical is missing.",
682
+ ].join("\n");
683
+ }
684
+ async createTradingWalletsForSetup(chains) {
685
+ const walletLines = [];
686
+ for (const chain of chains) {
687
+ const structured = await this.callSetupAgent({ action: "create_wallet", chain });
688
+ const wallet = readWallet({ ...structured, chain });
689
+ if (wallet)
690
+ this.upsertWallet(wallet);
691
+ const address = wallet?.address ?? firstString(structured.address, asRecord(structured.wallet)?.address);
692
+ walletLines.push(`${chainTitle(chain)} trading wallet: ${address ?? "(created)"}`);
693
+ }
694
+ return walletLines;
695
+ }
696
+ selectedChainsForSetup(flow) {
697
+ if (flow.selectedChains && flow.selectedChains.length > 0) {
698
+ return flow.selectedChains;
699
+ }
700
+ return [...new Set(this.knownWallets.map((wallet) => wallet.chain))];
701
+ }
702
+ async reviewStrategyWithAi(chain, selectedChains, notes) {
703
+ const fallback = fallbackStrategyReview(chain, notes);
704
+ if (!this.chatAgent)
705
+ return fallback;
706
+ const unit = chain === "solana" ? "SOL" : "USD/USDC";
707
+ const systemPrompt = [
708
+ "You are the strategy-coaching layer inside Balchemy CLI setup.",
709
+ "You do not call tools. You only decide whether the user's strategy is safe enough to configure.",
710
+ "Return strict JSON only. No markdown.",
711
+ "Required JSON shape:",
712
+ '{"ready":boolean,"summary":"string","missing":["string"],"followUp":"string","maxTradeSol":number|null,"maxTradeUsd":number|null}',
713
+ `Current chain: ${chainTitle(chain)}. Required max trade unit: ${unit}.`,
714
+ `Selected chains: ${formatChains(selectedChains)}.`,
715
+ "A ready strategy must include max trade size, entry filters, stop loss, take profit, max open positions, and avoid/blacklist rules.",
716
+ "If anything critical is missing, ready=false and followUp must ask only for the missing items in the same language as the user.",
717
+ "If ready=true, summary must be a faithful normalized version of all user notes with chain-specific limits preserved.",
718
+ ].join("\n");
719
+ const userMessage = [
720
+ `Chain: ${chainTitle(chain)}`,
721
+ "User notes:",
722
+ notes.map((note, index) => `${index + 1}. ${note}`).join("\n"),
723
+ ].join("\n\n");
724
+ try {
725
+ const response = await this.chatAgent.completeText(systemPrompt, userMessage);
726
+ const parsed = parseStrategyReviewResponse(response);
727
+ if (!parsed)
728
+ return fallback;
729
+ if (!parsed.ready && !parsed.followUp)
730
+ return fallback;
731
+ if (parsed.ready && parsed.summary.trim().length < 20)
732
+ return fallback;
733
+ return parsed;
734
+ }
735
+ catch (_error) {
736
+ return fallback;
737
+ }
738
+ }
571
739
  async handleSetupInput(text) {
572
740
  const flow = this.setupFlow;
573
741
  if (!flow)
@@ -586,7 +754,11 @@ export class AgentBridge {
586
754
  if (flow.step === "developer-wallet-confirm") {
587
755
  const wallet = text.trim();
588
756
  if (!flow.developerWallet || wallet.toLowerCase() !== flow.developerWallet.toLowerCase()) {
589
- this.setupFlow = { step: "developer-wallet" };
757
+ this.setupFlow = {
758
+ ...flow,
759
+ step: "developer-wallet",
760
+ developerWallet: undefined,
761
+ };
590
762
  this.addAgentMessage("The confirmation did not match. Start again by pasting the Base/EVM 0x developer wallet.");
591
763
  return;
592
764
  }
@@ -596,10 +768,50 @@ export class AgentBridge {
596
768
  walletAddressConfirm: wallet,
597
769
  });
598
770
  const masterKey = extractMasterKey(structured);
599
- this.setupFlow = { step: "networks" };
771
+ if (masterKey) {
772
+ this.persistMasterKey(masterKey);
773
+ }
774
+ if (flow.selectedChains && flow.selectedChains.length > 0) {
775
+ this.setupFlow = {
776
+ ...flow,
777
+ step: "networks",
778
+ rootWalletBound: true,
779
+ rootWalletKind: "evm",
780
+ };
781
+ const walletLines = await this.createTradingWalletsForSetup(flow.selectedChains);
782
+ const needsSolanaWallet = flow.selectedChains.includes("solana") && !flow.solanaRecoveryWallet;
783
+ this.setupFlow = {
784
+ ...flow,
785
+ step: needsSolanaWallet ? "solana-recovery-wallet" : "slippage",
786
+ rootWalletBound: true,
787
+ rootWalletKind: "evm",
788
+ };
789
+ this.addAgentMessage(`${masterKey ? `Developer wallet bound.
790
+
791
+ Master key saved encrypted locally and shown here so you can copy it externally:
792
+ ${masterKey}
793
+
794
+ It is shown once in real environments.` : "Developer wallet bound."}
795
+
796
+ Copyable trading wallet addresses:
797
+ ${walletLines.join("\n")}
798
+
799
+ ${this.setupPromptFor(needsSolanaWallet ? "solana-recovery-wallet" : "slippage")}`);
800
+ return;
801
+ }
802
+ this.setupFlow = { step: "networks", rootWalletBound: true, rootWalletKind: "evm" };
600
803
  this.addAgentMessage(masterKey
601
- ? `Developer wallet bound.\n\nCopy and store this master key now:\n${masterKey}\n\nIt is shown once in real environments.\n\n${this.setupPromptFor("networks")}`
602
- : `Developer wallet bound.\n\n${this.setupPromptFor("networks")}`);
804
+ ? `Developer wallet bound.
805
+
806
+ Master key saved encrypted locally and shown here so you can copy it externally:
807
+ ${masterKey}
808
+
809
+ It is shown once in real environments.
810
+
811
+ ${this.setupPromptFor("networks")}`
812
+ : `Developer wallet bound.
813
+
814
+ ${this.setupPromptFor("networks")}`);
603
815
  return;
604
816
  }
605
817
  if (flow.step === "networks") {
@@ -608,17 +820,37 @@ export class AgentBridge {
608
820
  this.addAgentMessage("Choose Solana, Base, or both.");
609
821
  return;
610
822
  }
611
- const walletLines = [];
612
- for (const chain of chains) {
613
- const structured = await this.callSetupAgent({ action: "create_wallet", chain });
614
- const address = firstString(structured.address, asRecord(structured.wallet)?.address);
615
- walletLines.push(`${chainTitle(chain)} trading wallet: ${address ?? "(created)"}`);
823
+ if (!flow.rootWalletBound) {
824
+ if (chains.includes("base")) {
825
+ this.setupFlow = { ...flow, step: "developer-wallet", selectedChains: chains };
826
+ this.addAgentMessage(`Trading networks selected: ${formatChains(chains)}.
827
+
828
+ ${this.setupPromptFor("developer-wallet")}`);
829
+ return;
830
+ }
831
+ this.setupFlow = { ...flow, step: "solana-recovery-wallet", selectedChains: chains };
832
+ this.addAgentMessage(`Trading networks selected: ${formatChains(chains)}.
833
+
834
+ ${this.setupPromptFor("solana-recovery-wallet")}`);
835
+ return;
836
+ }
837
+ if (flow.rootWalletKind === "solana" && chains.includes("base")) {
838
+ this.addAgentMessage("This agent is bound with a Solana-only root wallet. Base trading requires a Base/EVM developer wallet, so choose Solana for this setup or start a new Base/both setup.");
839
+ return;
616
840
  }
841
+ const walletLines = await this.createTradingWalletsForSetup(chains);
842
+ const needsSolanaWallet = chains.includes("solana") && !flow.solanaRecoveryWallet;
617
843
  this.setupFlow = {
618
- step: chains.includes("solana") ? "solana-recovery-wallet" : "slippage",
844
+ ...flow,
845
+ step: needsSolanaWallet ? "solana-recovery-wallet" : "slippage",
619
846
  selectedChains: chains,
620
847
  };
621
- this.addAgentMessage(`Trading networks configured: ${formatChains(chains)}.\n\nCopyable trading wallet addresses:\n${walletLines.join("\n")}\n\n${this.setupPromptFor(chains.includes("solana") ? "solana-recovery-wallet" : "slippage")}`);
848
+ this.addAgentMessage(`Trading networks configured: ${formatChains(chains)}.
849
+
850
+ Copyable trading wallet addresses:
851
+ ${walletLines.join("\n")}
852
+
853
+ ${this.setupPromptFor(needsSolanaWallet ? "solana-recovery-wallet" : "slippage")}`);
622
854
  return;
623
855
  }
624
856
  if (flow.step === "solana-recovery-wallet") {
@@ -637,11 +869,55 @@ export class AgentBridge {
637
869
  this.setupFlow = {
638
870
  step: "solana-recovery-wallet",
639
871
  ...(flow.developerWallet ? { developerWallet: flow.developerWallet } : {}),
872
+ ...(flow.rootWalletBound !== undefined ? { rootWalletBound: flow.rootWalletBound } : {}),
873
+ ...(flow.rootWalletKind ? { rootWalletKind: flow.rootWalletKind } : {}),
640
874
  ...(flow.selectedChains ? { selectedChains: flow.selectedChains } : {}),
641
875
  };
642
876
  this.addAgentMessage("The Solana wallet confirmation did not match. Paste the Solana recovery/withdrawal wallet again.");
643
877
  return;
644
878
  }
879
+ if (!flow.rootWalletBound) {
880
+ const structured = await this.callSetupAgent({
881
+ action: "bind_solana_root_wallet",
882
+ solanaWalletAddress: flow.solanaRecoveryWallet,
883
+ solanaWalletAddressConfirm: wallet,
884
+ });
885
+ const masterKey = extractMasterKey(structured);
886
+ if (masterKey) {
887
+ this.persistMasterKey(masterKey);
888
+ }
889
+ const selectedChains = flow.selectedChains && flow.selectedChains.length > 0 ? flow.selectedChains : ["solana"];
890
+ this.setupFlow = {
891
+ ...flow,
892
+ step: "networks",
893
+ solanaRecoveryWallet: wallet,
894
+ rootWalletBound: true,
895
+ rootWalletKind: "solana",
896
+ selectedChains,
897
+ };
898
+ const walletLines = await this.createTradingWalletsForSetup(selectedChains);
899
+ this.setupFlow = {
900
+ ...flow,
901
+ step: "slippage",
902
+ solanaRecoveryWallet: wallet,
903
+ rootWalletBound: true,
904
+ rootWalletKind: "solana",
905
+ selectedChains,
906
+ };
907
+ this.addAgentMessage(`Solana developer/recovery wallet confirmed:
908
+ ${wallet}
909
+
910
+ ${masterKey ? `Master key saved encrypted locally and shown here so you can copy it externally:
911
+ ${masterKey}
912
+
913
+ It is shown once in real environments.
914
+
915
+ ` : ""}Copyable trading wallet addresses:
916
+ ${walletLines.join("\n")}
917
+
918
+ ${this.setupPromptFor("slippage")}`);
919
+ return;
920
+ }
645
921
  try {
646
922
  await this.callSetupAgent({
647
923
  action: "bind_solana_developer_wallet",
@@ -653,7 +929,10 @@ export class AgentBridge {
653
929
  this.addSystemMessage(`Solana recovery wallet stored locally for this setup. MCP did not bind it directly: ${err instanceof Error ? err.message : String(err)}`);
654
930
  }
655
931
  this.setupFlow = { ...flow, step: "slippage", solanaRecoveryWallet: wallet };
656
- this.addAgentMessage(`Solana recovery/withdrawal wallet confirmed:\n${wallet}\n\n${this.setupPromptFor("slippage")}`);
932
+ this.addAgentMessage(`Solana recovery/withdrawal wallet confirmed:
933
+ ${wallet}
934
+
935
+ ${this.setupPromptFor("slippage")}`);
657
936
  return;
658
937
  }
659
938
  if (flow.step === "slippage") {
@@ -663,33 +942,83 @@ export class AgentBridge {
663
942
  return;
664
943
  }
665
944
  await this.callSetupAgent({ action: "configure_slippage", slippageBps: bps });
666
- const selectedChains = flow.selectedChains ?? ["solana"];
945
+ const selectedChains = this.selectedChainsForSetup(flow);
946
+ if (selectedChains.length === 0) {
947
+ this.setupFlow = { ...flow, step: "networks" };
948
+ this.addAgentMessage(`I need the selected trading networks before strategy setup.\n\n${this.setupPromptFor("networks")}`);
949
+ return;
950
+ }
667
951
  const currentStrategyChain = nextStrategyChain(selectedChains, {}) ?? selectedChains[0];
668
952
  this.setupFlow = { ...flow, step: "strategy", slippageBps: bps, currentStrategyChain, chainStrategies: {} };
669
- this.addAgentMessage(`Slippage set to ${bps} bps.\n\n${chainTitle(currentStrategyChain)} strategy:\n${this.setupPromptFor("strategy")}`);
953
+ this.addAgentMessage(`Slippage set to ${bps} bps.\n\n${this.setupPromptForStrategyChain(currentStrategyChain)}`);
670
954
  return;
671
955
  }
672
956
  if (flow.step === "strategy") {
673
- const strategyText = text.trim();
674
- if (strategyText.length < 20) {
675
- this.addAgentMessage("The strategy is too short. Include limits, entries, exits, max positions, and avoid rules.");
957
+ const strategyNote = text.trim();
958
+ const selectedChainsForFlow = this.selectedChainsForSetup(flow);
959
+ const chain = flow.currentStrategyChain ?? selectedChainsForFlow[0];
960
+ if (!chain) {
961
+ this.setupFlow = { ...flow, step: "networks" };
962
+ this.addAgentMessage(`I need the selected trading networks before strategy setup.\n\n${this.setupPromptFor("networks")}`);
963
+ return;
964
+ }
965
+ if (strategyNote.length < 8) {
966
+ this.addAgentMessage("That is too short for live setup. Add trade size, entry filters, exits, max positions, or avoid rules.");
967
+ return;
968
+ }
969
+ const previousNotes = flow.strategyNotes?.[chain] ?? [];
970
+ const notes = [...previousNotes, strategyNote];
971
+ const selectedChains = selectedChainsForFlow.length > 0 ? selectedChainsForFlow : [chain];
972
+ const strategyNotes = {
973
+ ...(flow.strategyNotes ?? {}),
974
+ [chain]: notes,
975
+ };
976
+ this.setupFlow = { ...flow, strategyNotes };
977
+ const review = await this.reviewStrategyWithAi(chain, selectedChains, notes);
978
+ if (!review.ready) {
979
+ const missingLine = review.missing.length > 0 ? `\n\nMissing: ${review.missing.join(", ")}` : "";
980
+ this.addAgentMessage(`${review.followUp || "I need a little more detail before this can go live."}${missingLine}`);
676
981
  return;
677
982
  }
983
+ const strategyText = review.summary.trim() || notes.join("\n");
984
+ const parsedLimits = parseTradeLimits(strategyText);
985
+ const maxTradeSol = chain === "solana"
986
+ ? review.maxTradeSol ?? parsedLimits.maxTradeSol ?? flow.maxTradeSol
987
+ : flow.maxTradeSol;
988
+ const maxTradeUsd = chain === "base"
989
+ ? review.maxTradeUsd ?? parsedLimits.maxTradeUsd ?? flow.maxTradeUsd
990
+ : flow.maxTradeUsd;
678
991
  const chainStrategies = {
679
992
  ...(flow.chainStrategies ?? {}),
680
- ...(flow.currentStrategyChain ? { [flow.currentStrategyChain]: strategyText } : {}),
993
+ [chain]: strategyText,
681
994
  };
682
- const selectedChains = flow.selectedChains ?? (flow.currentStrategyChain ? [flow.currentStrategyChain] : ["solana"]);
683
995
  const nextChain = nextStrategyChain(selectedChains, chainStrategies);
684
996
  if (nextChain) {
685
- this.setupFlow = { ...flow, chainStrategies, currentStrategyChain: nextChain };
686
- this.addAgentMessage(`${chainTitle(flow.currentStrategyChain ?? selectedChains[0])} strategy saved.\n\n${chainTitle(nextChain)} strategy:\n${this.setupPromptFor("strategy")}`);
997
+ this.setupFlow = {
998
+ ...flow,
999
+ chainStrategies,
1000
+ currentStrategyChain: nextChain,
1001
+ strategyNotes,
1002
+ ...(maxTradeSol !== undefined ? { maxTradeSol } : {}),
1003
+ ...(maxTradeUsd !== undefined ? { maxTradeUsd } : {}),
1004
+ };
1005
+ this.addAgentMessage(`${chainTitle(chain)} strategy drafted with AI review.\n\n${this.setupPromptForStrategyChain(nextChain)}`);
687
1006
  return;
688
1007
  }
689
1008
  const combinedStrategy = buildCombinedStrategy({ ...flow, chainStrategies });
690
- const limits = parseTradeLimits(combinedStrategy);
691
- this.setupFlow = { ...flow, ...limits, chainStrategies, step: "strategy-confirm", strategyText: combinedStrategy };
692
- this.addAgentMessage(`Review before live configuration:\n\n${combinedStrategy}\n\nMax SOL/trade: ${limits.maxTradeSol ?? "not specified"}\nMax USD/trade: ${limits.maxTradeUsd ?? "not specified"}\n\n${this.setupPromptFor("strategy-confirm")}`);
1009
+ const combinedLimits = parseTradeLimits(combinedStrategy);
1010
+ const finalMaxTradeSol = maxTradeSol ?? combinedLimits.maxTradeSol;
1011
+ const finalMaxTradeUsd = maxTradeUsd ?? combinedLimits.maxTradeUsd;
1012
+ this.setupFlow = {
1013
+ ...flow,
1014
+ chainStrategies,
1015
+ strategyNotes,
1016
+ step: "strategy-confirm",
1017
+ strategyText: combinedStrategy,
1018
+ ...(finalMaxTradeSol !== undefined ? { maxTradeSol: finalMaxTradeSol } : {}),
1019
+ ...(finalMaxTradeUsd !== undefined ? { maxTradeUsd: finalMaxTradeUsd } : {}),
1020
+ };
1021
+ this.addAgentMessage(`AI-reviewed strategy draft:\n\n${combinedStrategy}\n\nMax SOL/trade: ${finalMaxTradeSol ?? "not specified"}\nMax USD/trade: ${finalMaxTradeUsd ?? "not specified"}\n\n${this.setupPromptFor("strategy-confirm")}`);
693
1022
  return;
694
1023
  }
695
1024
  if (flow.step === "strategy-confirm") {
@@ -698,12 +1027,14 @@ export class AgentBridge {
698
1027
  step: "strategy",
699
1028
  ...(flow.developerWallet ? { developerWallet: flow.developerWallet } : {}),
700
1029
  ...(flow.solanaRecoveryWallet ? { solanaRecoveryWallet: flow.solanaRecoveryWallet } : {}),
1030
+ ...(flow.rootWalletBound !== undefined ? { rootWalletBound: flow.rootWalletBound } : {}),
1031
+ ...(flow.rootWalletKind ? { rootWalletKind: flow.rootWalletKind } : {}),
701
1032
  ...(flow.selectedChains ? { selectedChains: flow.selectedChains } : {}),
702
1033
  ...(flow.slippageBps !== undefined ? { slippageBps: flow.slippageBps } : {}),
703
1034
  chainStrategies: {},
704
- currentStrategyChain: (flow.selectedChains ?? ["solana"])[0],
1035
+ currentStrategyChain: this.selectedChainsForSetup(flow)[0] ?? "solana",
705
1036
  };
706
- this.addAgentMessage(this.setupPromptFor("strategy"));
1037
+ this.addAgentMessage(this.setupPromptForStrategyChain(this.selectedChainsForSetup(flow)[0] ?? "solana"));
707
1038
  return;
708
1039
  }
709
1040
  if (!isAffirmative(text)) {
@@ -712,7 +1043,7 @@ export class AgentBridge {
712
1043
  }
713
1044
  if (!flow.strategyText) {
714
1045
  this.setupFlow = { ...flow, step: "strategy" };
715
- this.addAgentMessage(this.setupPromptFor("strategy"));
1046
+ this.addAgentMessage(this.setupPromptForStrategyChain(flow.currentStrategyChain ?? this.selectedChainsForSetup(flow)[0] ?? "solana"));
716
1047
  return;
717
1048
  }
718
1049
  await this.callSetupAgent({
@@ -729,7 +1060,7 @@ export class AgentBridge {
729
1060
  }
730
1061
  if (flow.step === "subscriptions") {
731
1062
  if (isAffirmative(text)) {
732
- const chains = flow.selectedChains ?? ["solana"];
1063
+ const chains = this.selectedChainsForSetup(flow);
733
1064
  if (chains.includes("solana")) {
734
1065
  await this.mcp.callTool("create_subscription", {
735
1066
  type: "new_token_launch",
@@ -777,18 +1108,16 @@ export class AgentBridge {
777
1108
  return;
778
1109
  if (name === "setup_agent") {
779
1110
  const structured = asRecord(parsed.structured) ?? parsed;
780
- const wallets = collectWallets(structured);
781
- if (wallets.length > 0) {
782
- this.setters.setStatus((prev) => ({
783
- ...prev,
784
- wallets: mergeWallets(prev.wallets, wallets),
785
- }));
786
- }
787
1111
  if (structured.action === "create_wallet") {
788
1112
  const chain = normalizeChain(structured.chain);
789
- const address = typeof structured.address === "string" ? structured.address : undefined;
790
- if (chain && address) {
791
- this.upsertWallet({ chain, address });
1113
+ const wallet = chain ? readWallet({ ...structured, chain }) : null;
1114
+ if (wallet) {
1115
+ this.upsertWallet(wallet);
1116
+ }
1117
+ }
1118
+ else if (structured.action === "get_status" || structured.action === "status") {
1119
+ for (const wallet of collectWallets(structured)) {
1120
+ this.upsertWallet(wallet);
792
1121
  }
793
1122
  }
794
1123
  return;
@@ -810,20 +1139,81 @@ export class AgentBridge {
810
1139
  ?? (data ? firstNumber(data, ["totalValueUsd", "portfolioValueUsd", "portfolio_value_usd", "total_value_usd", "valueUsd"]) : undefined)
811
1140
  ?? summed.balanceUsd;
812
1141
  const positions = collectPositions(data ?? snapshot);
1142
+ for (const wallet of wallets) {
1143
+ this.upsertWallet(wallet);
1144
+ }
813
1145
  this.setters.setStatus((prev) => ({
814
1146
  ...prev,
815
1147
  ...(balanceSol !== undefined ? { balanceSol } : {}),
816
1148
  ...(balanceUsd !== undefined ? { balanceUsd } : {}),
817
- ...(wallets.length > 0 ? { wallets: mergeWallets(prev.wallets, wallets) } : {}),
818
1149
  ...(positions ? { activeTrades: positions } : {}),
819
1150
  }));
820
1151
  }
821
1152
  upsertWallet(wallet) {
1153
+ this.knownWallets = mergeLatestWallets(this.knownWallets, [wallet]);
1154
+ this.persistWallet(wallet);
822
1155
  this.setters.setStatus((prev) => {
823
1156
  const wallets = prev.wallets.filter((existing) => existing.chain !== wallet.chain);
824
1157
  return { ...prev, wallets: [...wallets, wallet] };
825
1158
  });
826
1159
  }
1160
+ loadStoredWallets() {
1161
+ const agent = loadAgent();
1162
+ if (!agent || agent.publicId !== this.config.publicId)
1163
+ return [];
1164
+ const wallets = [];
1165
+ if (agent.wallets?.solana)
1166
+ wallets.push({ chain: "solana", address: agent.wallets.solana });
1167
+ if (agent.wallets?.base)
1168
+ wallets.push({ chain: "base", address: agent.wallets.base });
1169
+ return wallets;
1170
+ }
1171
+ syncKnownWalletsToStatus() {
1172
+ if (this.knownWallets.length === 0)
1173
+ return;
1174
+ this.setters.setStatus((prev) => ({
1175
+ ...prev,
1176
+ wallets: mergeLatestWallets(prev.wallets, this.knownWallets),
1177
+ }));
1178
+ }
1179
+ persistActiveAgent(update) {
1180
+ const agent = loadAgent();
1181
+ if (!agent || agent.publicId !== this.config.publicId)
1182
+ return;
1183
+ saveAgent(update(agent));
1184
+ }
1185
+ persistMasterKey(masterKey) {
1186
+ this.persistActiveAgent((agent) => ({
1187
+ ...agent,
1188
+ masterKey,
1189
+ }));
1190
+ }
1191
+ persistWallet(wallet) {
1192
+ this.persistActiveAgent((agent) => {
1193
+ const wallets = { ...(agent.wallets ?? {}) };
1194
+ wallets[wallet.chain] = wallet.address;
1195
+ return {
1196
+ ...agent,
1197
+ wallets,
1198
+ };
1199
+ });
1200
+ }
1201
+ buildRuntimeContext() {
1202
+ if (this.knownWallets.length === 0)
1203
+ return "";
1204
+ return [
1205
+ "Known Balchemy runtime context from local encrypted CLI state:",
1206
+ ...this.knownWallets.map((wallet) => `${walletAddressLabel(wallet.chain)}: ${wallet.address}`),
1207
+ "Funding rule: fund the Solana trading wallet with SOL. Fund the Base trading wallet with ETH on Base for gas and Base-chain capital as required.",
1208
+ "If the user asks where to fund, answer from these addresses.",
1209
+ ].join("\n");
1210
+ }
1211
+ withRuntimeContext(userMessage) {
1212
+ const context = this.buildRuntimeContext();
1213
+ if (!context)
1214
+ return userMessage;
1215
+ return `${context}\n\nUser message:\n${userMessage}`;
1216
+ }
827
1217
  async ensureDefaultSubscriptions() {
828
1218
  if (!this.config.autoSeedSubscriptions) {
829
1219
  return;