solana-traderclaw 1.0.82 → 1.0.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -348,7 +348,6 @@ Pay-as-you-go or Basic tier is required for the read-only social intel tools.
348
348
  ### Safety
349
349
  | Tool | Description |
350
350
  |------|-------------|
351
- | `solana_killswitch` | Toggle emergency kill switch |
352
351
  | `solana_killswitch_status` | Check kill switch state |
353
352
 
354
353
  ### Wallet
@@ -29,7 +29,7 @@
29
29
  agentId: "main",
30
30
  sessionTarget: "isolated",
31
31
  delivery: { mode: "none" },
32
- message: "CRON_JOB: strategy_evolution\n\nStep 1: Call solana_journal_summary to get aggregate performance stats (win rate, avg PnL, trade count). If fewer than 20 closed trades since the last strategy evolution, skip weight updates but still run pattern detection.\n\nStep 2: Call solana_memory_search for 'strategy_evolution' — find last 3 evolution cycle results. Call solana_memory_search for 'strategy_drift_warning' — find drift warnings since last evolution. Call solana_memory_search for 'pre_trade_rationale' — recent decision patterns.\n\nStep 3: Run Recurring Pattern Detection — search for learning_entry tags, group by area, check linked chains (3+ = confirmed pattern), investigate drift warnings.\n\nStep 4: Call solana_strategy_state to read current feature weights.\n\nStep 5: Call solana_trades to get recent closed trades. Apply ADL checks (direction consistency, weight velocity, reversion check).\n\nStep 6: Compute proposed weight changes. Score each with VFM (Frequency + Failure Reduction + Self-Cost). Only apply changes scoring >= 3/5.\n\nStep 7: Verify guardrails: maxDeltaOk, sumWeightsOk, minTradesOk, floorCapOk. If all pass, call solana_strategy_update with incremented version.\n\nStep 8: Run Named Pattern Recognition — search for winning trade clusters, catalog new patterns, evolve existing ones.\n\nStep 9: Evaluate discovery filter performance. Log all results via solana_memory_write with tags: strategy_evolution, vfm_scorecard, pattern_detection, named_pattern.\n\nFORMATTING RULES:\n- Every token reference MUST use SYMBOL (full_CA) format.\n- Do not execute trades. Do not ask questions.",
32
+ message: "CRON_JOB: strategy_evolution\n\nStep 1: Call solana_journal_summary to get aggregate performance stats (win rate, avg PnL, trade count). If fewer than 10 closed trades since the last strategy evolution, skip weight updates but still run pattern detection.\n\nStep 2: Call solana_memory_search for 'strategy_evolution' — find last 3 evolution cycle results. Call solana_memory_search for 'strategy_drift_warning' — find drift warnings since last evolution. Call solana_memory_search for 'pre_trade_rationale' — recent decision patterns.\n\nStep 3: Run Recurring Pattern Detection — search for learning_entry tags, group by area, check linked chains (3+ = confirmed pattern), investigate drift warnings.\n\nStep 4: Call solana_strategy_state to read current feature weights.\n\nStep 5: Call solana_trades to get recent closed trades. Apply ADL checks (direction consistency, weight velocity, reversion check).\n\nStep 6: Compute proposed weight changes. Score each with VFM (Frequency + Failure Reduction + Self-Cost). Only apply changes scoring >= 3/5.\n\nStep 7: Verify guardrails: maxDeltaOk, sumWeightsOk, minTradesOk, floorCapOk. If all pass, call solana_strategy_update with incremented version.\n\nStep 8: Run Named Pattern Recognition — search for winning trade clusters, catalog new patterns, evolve existing ones.\n\nStep 9: Evaluate discovery filter performance. Log all results via solana_memory_write with tags: strategy_evolution, vfm_scorecard, pattern_detection, named_pattern.\n\nFORMATTING RULES:\n- Every token reference MUST use SYMBOL (full_CA) format.\n- Do not execute trades. Do not ask questions.",
33
33
  enabled: true
34
34
  },
35
35
  {
@@ -53,8 +53,14 @@ var IntelligenceLab = class {
53
53
  } else {
54
54
  candidates.push(full);
55
55
  }
56
- if (candidates.length > 5e3) {
57
- candidates.splice(0, candidates.length - 5e3);
56
+ const MAX_CANDIDATES = 200;
57
+ if (candidates.length > MAX_CANDIDATES) {
58
+ const labeled = candidates.filter((c) => c.outcome);
59
+ const unlabeled = candidates.filter((c) => !c.outcome);
60
+ const keepUnlabeled = Math.max(0, MAX_CANDIDATES - labeled.length);
61
+ const trimmed = [...labeled, ...unlabeled.slice(-keepUnlabeled)];
62
+ candidates.length = 0;
63
+ candidates.push(...trimmed);
58
64
  }
59
65
  writeJsonFile(this.candidatesFile(), candidates);
60
66
  return existing >= 0 ? candidates[existing] : full;
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  } from "./chunk-PIZZXNMQ.js";
22
22
  import {
23
23
  IntelligenceLab
24
- } from "./chunk-C24QA3MQ.js";
24
+ } from "./chunk-FBS5FGW2.js";
25
25
  import {
26
26
  scrubUntrustedText
27
27
  } from "./chunk-AI6MTHUN.js";
@@ -1142,6 +1142,33 @@ var solanaTraderPlugin = {
1142
1142
  fs.writeFileSync(filePath, entries.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf-8");
1143
1143
  return entries.length;
1144
1144
  };
1145
+ const volatileStateKeys = /* @__PURE__ */ new Set(["lastHeartbeat", "lastHeartbeatAt"]);
1146
+ const renderSummaryBullets = (key, val) => {
1147
+ if (key === "lastCycleSummary" && val && typeof val === "object") {
1148
+ const s = val;
1149
+ const bullets = ["## Last Cycle Summary", ""];
1150
+ if (s.capitalSol !== void 0) bullets.push(`- **Capital SOL:** ${s.capitalSol}`);
1151
+ if (s.openPositions !== void 0) bullets.push(`- **Open Positions:** ${s.openPositions}`);
1152
+ if (s.signalsProcessed !== void 0) bullets.push(`- **Signals Processed:** ${s.signalsProcessed}`);
1153
+ if (s.topWatch) bullets.push(`- **Top Watch:** ${s.topWatch}`);
1154
+ if (s.xApiStatus) bullets.push(`- **X API Status:** ${s.xApiStatus}`);
1155
+ if (s.bitqueryStatus) bullets.push(`- **Bitquery Status:** ${s.bitqueryStatus}`);
1156
+ const rendered = Object.keys(s).filter((k) => !["capitalSol", "openPositions", "signalsProcessed", "topWatch", "xApiStatus", "bitqueryStatus"].includes(k));
1157
+ for (const rk of rendered.slice(0, 5)) {
1158
+ const rv = s[rk];
1159
+ const disp = typeof rv === "object" ? JSON.stringify(rv) : String(rv);
1160
+ bullets.push(`- **${rk}:** ${disp.length > 120 ? disp.slice(0, 120) + "\u2026" : disp}`);
1161
+ }
1162
+ bullets.push("");
1163
+ return bullets;
1164
+ }
1165
+ return [];
1166
+ };
1167
+ const formatStateValue = (val) => {
1168
+ if (val === null || val === void 0) return String(val);
1169
+ if (typeof val === "object") return JSON.stringify(val);
1170
+ return String(val);
1171
+ };
1145
1172
  const generateMemoryMd = (aid, stateObj) => {
1146
1173
  const lines = [
1147
1174
  `# ${aid} \u2014 Durable Memory`,
@@ -1160,7 +1187,7 @@ var solanaTraderPlugin = {
1160
1187
  if (state.walletId) identity.push(`- **Wallet:** ${state.walletId}`);
1161
1188
  if (state.mode) identity.push(`- **Mode:** ${state.mode}`);
1162
1189
  if (state.strategyVersion) identity.push(`- **Strategy Version:** ${state.strategyVersion}`);
1163
- if (state.regime) identity.push(`- **Regime:** ${state.regime}`);
1190
+ if (state.regime) identity.push(`- **Regime:** ${formatStateValue(state.regime)}`);
1164
1191
  if (state.maxPositions) identity.push(`- **Max Positions:** ${state.maxPositions}`);
1165
1192
  if (state.maxPositionSizeSol) identity.push(`- **Max Position Size:** ${state.maxPositionSizeSol} SOL`);
1166
1193
  if (identity.length > 0) {
@@ -1192,13 +1219,24 @@ var solanaTraderPlugin = {
1192
1219
  const rc = state.regimeCanary;
1193
1220
  lines.push("## Regime Canary", "", `- **Regime:** ${rc.regime || "unknown"}`, `- **Detected At:** ${rc.detectedAt || "unknown"}`, "");
1194
1221
  }
1195
- const excludeKeys = /* @__PURE__ */ new Set(["tier", "walletId", "mode", "strategyVersion", "regime", "maxPositions", "maxPositionSizeSol", "defenseMode", "killSwitchActive", "watchlist", "permanentLearnings", "regimeCanary"]);
1196
- const otherKeys = Object.keys(state).filter((k) => !excludeKeys.has(k));
1197
- if (otherKeys.length > 0) {
1222
+ const structuredKeys = /* @__PURE__ */ new Set(["tier", "walletId", "mode", "strategyVersion", "regime", "maxPositions", "maxPositionSizeSol", "defenseMode", "killSwitchActive", "watchlist", "permanentLearnings", "regimeCanary"]);
1223
+ const summaryKeys = /* @__PURE__ */ new Set(["lastCycleSummary"]);
1224
+ const otherKeys = Object.keys(state).filter((k) => !structuredKeys.has(k) && !volatileStateKeys.has(k));
1225
+ const summaryRendered = [];
1226
+ const remainingKeys = [];
1227
+ for (const key of otherKeys) {
1228
+ if (summaryKeys.has(key)) {
1229
+ summaryRendered.push(...renderSummaryBullets(key, state[key]));
1230
+ } else {
1231
+ remainingKeys.push(key);
1232
+ }
1233
+ }
1234
+ if (summaryRendered.length > 0) lines.push(...summaryRendered);
1235
+ if (remainingKeys.length > 0) {
1198
1236
  lines.push("## Other State Keys", "");
1199
- for (const key of otherKeys.slice(0, 30)) {
1237
+ for (const key of remainingKeys.slice(0, 30)) {
1200
1238
  const val = state[key];
1201
- const display = typeof val === "object" ? JSON.stringify(val) : String(val);
1239
+ const display = formatStateValue(val);
1202
1240
  lines.push(`- **${key}:** ${display.length > 200 ? display.slice(0, 200) + "\u2026" : display}`);
1203
1241
  }
1204
1242
  lines.push("");
@@ -1721,15 +1759,6 @@ ${notes}
1721
1759
  })
1722
1760
  )
1723
1761
  });
1724
- api.registerTool({
1725
- name: "solana_killswitch_status",
1726
- description: "Check the current kill switch state \u2014 whether it's enabled and in what mode.",
1727
- parameters: Type.Object({}),
1728
- execute: wrapExecute(
1729
- "solana_killswitch_status",
1730
- async () => get(`/api/killswitch/status?walletId=${walletId}`)
1731
- )
1732
- });
1733
1762
  api.registerTool({
1734
1763
  name: "solana_capital_status",
1735
1764
  description: "Get your current capital status \u2014 SOL balance, open position count, unrealized/realized PnL, daily notional used, daily loss, and effective limits. **PnL:** for Solana wallets, `totalUnrealizedPnl` / `totalRealizedPnl` / `totalPnl` are returned in SOL-native units.",
@@ -1954,34 +1983,6 @@ ${notes}
1954
1983
  })
1955
1984
  )
1956
1985
  });
1957
- api.registerTool({
1958
- name: "solana_wallet_token_balance",
1959
- description: "Get the on-chain SPL token balance (uiAmount \u2014 source of truth) for a specific mint in your trading wallet. Returns the token amount, decimals, and USD value estimate. Use to verify actual holdings when position balances seem inconsistent.",
1960
- parameters: Type.Object({
1961
- tokenAddress: Type.String({ description: "Solana token mint address to check balance for" })
1962
- }),
1963
- execute: wrapExecute(
1964
- "solana_wallet_token_balance",
1965
- async (_id, params) => post("/api/wallet/token-balance", { tokenAddress: params.tokenAddress })
1966
- )
1967
- });
1968
- api.registerTool({
1969
- name: "solana_sweep_dead_tokens",
1970
- description: "Sell 100% of open positions where unrealizedReturnPct \u2264 -maxLossPct to cut losses and reclaim SOL. NOT a dust/rent sweeper \u2014 this sells actual positions that are down beyond recovery. Use in dead_money_sweep cron or manual loss-cutting.",
1971
- parameters: Type.Object({
1972
- maxLossPct: Type.Optional(Type.Number({ description: "Maximum loss percentage threshold \u2014 positions down more than this % are sold (default: 80)" })),
1973
- slippageBps: Type.Optional(Type.Number({ description: "Slippage in basis points for the sell orders (default: server default)" })),
1974
- dryRun: Type.Optional(Type.Boolean({ description: "If true, return positions that would be sold without executing. Default: false" }))
1975
- }),
1976
- execute: wrapExecute(
1977
- "solana_sweep_dead_tokens",
1978
- async (_id, params) => post("/api/wallet/sweep-dead-tokens", {
1979
- maxLossPct: params.maxLossPct,
1980
- slippageBps: params.slippageBps,
1981
- dryRun: params.dryRun
1982
- })
1983
- )
1984
- });
1985
1986
  api.registerTool({
1986
1987
  name: "solana_trades",
1987
1988
  description: "List your trade history with pagination. Returns executed trades with details like token, side, size, PnL, and timestamp.",
@@ -2449,6 +2450,29 @@ ${notes}
2449
2450
  const capitalOnly = failed === 1 && steps.find((s) => !s.ok)?.step === "solana_capital_status";
2450
2451
  startupGateState = { ok: allOk, ts: Date.now(), steps };
2451
2452
  const k = config.apiKey && String(config.apiKey).trim() || null;
2453
+ if (allOk || capitalOnly) {
2454
+ try {
2455
+ const effectiveAgentId = config.agentId || "main";
2456
+ const stateFilePath = path.join(stateDir, `${effectiveAgentId}.json`);
2457
+ const existing = readJsonFile(stateFilePath);
2458
+ const existingState = existing?.state && typeof existing.state === "object" ? existing.state : {};
2459
+ const alphaStep = steps.find((s) => s.step === "solana_alpha_subscribe" && s.ok);
2460
+ const tier = alphaStep?.details?.tier;
2461
+ const seedFields = {};
2462
+ if (!existingState.walletId && walletId) seedFields.walletId = walletId;
2463
+ if (!existingState.tier && tier) seedFields.tier = tier;
2464
+ if (!existingState.mode) seedFields.mode = "HARDENED";
2465
+ if (!existingState.strategyVersion) seedFields.strategyVersion = "1.0.0";
2466
+ if (Object.keys(seedFields).length > 0) {
2467
+ const merged = { ...existingState, ...seedFields };
2468
+ writeJsonFile(stateFilePath, { agentId: effectiveAgentId, state: merged, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2469
+ writeMemoryMd(effectiveAgentId, merged);
2470
+ api.logger.info(`[solana-trader] Seeded identity fields into state: ${Object.keys(seedFields).join(", ")}`);
2471
+ }
2472
+ } catch (seedErr) {
2473
+ api.logger.warn(`[solana-trader] Failed to seed identity fields: ${seedErr instanceof Error ? seedErr.message : String(seedErr)}`);
2474
+ }
2475
+ }
2452
2476
  return {
2453
2477
  ok: allOk,
2454
2478
  ts: Date.now(),
@@ -2598,6 +2622,49 @@ ${notes}
2598
2622
  parameters: Type.Object({}),
2599
2623
  execute: wrapExecute("solana_system_status", async () => get("/api/system/status"))
2600
2624
  });
2625
+ api.registerTool({
2626
+ name: "solana_storage_status",
2627
+ description: "Check local VPS disk usage and plugin storage health. Reports disk free/total, daily log count + size, candidates count, session directory size. Use in heartbeat Step 0 or risk_audit to detect disk pressure before it causes silent failures.",
2628
+ parameters: Type.Object({}),
2629
+ execute: wrapExecute("solana_storage_status", async () => {
2630
+ const os = await import("os");
2631
+ const result = {};
2632
+ try {
2633
+ const stat = fs.statfsSync(workspaceRoot);
2634
+ const totalGB = Math.round(stat.bsize * stat.blocks / 1024 ** 3 * 100) / 100;
2635
+ const freeGB = Math.round(stat.bsize * stat.bavail / 1024 ** 3 * 100) / 100;
2636
+ const usedPct = Math.round((1 - freeGB / totalGB) * 100);
2637
+ result.disk = { totalGB, freeGB, usedPct, warning: usedPct > 85, critical: usedPct > 95 };
2638
+ } catch {
2639
+ result.disk = { error: "unable to read disk stats" };
2640
+ }
2641
+ try {
2642
+ const logFiles = fs.readdirSync(memoryDir).filter((f) => f.endsWith(".md"));
2643
+ let totalBytes = 0;
2644
+ for (const f of logFiles) {
2645
+ try {
2646
+ totalBytes += fs.statSync(path.join(memoryDir, f)).size;
2647
+ } catch {
2648
+ }
2649
+ }
2650
+ result.dailyLogs = { count: logFiles.length, totalKB: Math.round(totalBytes / 1024) };
2651
+ } catch {
2652
+ result.dailyLogs = { count: 0, totalKB: 0 };
2653
+ }
2654
+ try {
2655
+ const candidatesPath = intelligenceLab.exportDataset("json");
2656
+ const parsed = JSON.parse(candidatesPath);
2657
+ const labeled = Array.isArray(parsed) ? parsed.filter((c) => c.outcome).length : 0;
2658
+ const total = Array.isArray(parsed) ? parsed.length : 0;
2659
+ result.candidates = { total, labeled, unlabeled: total - labeled };
2660
+ } catch {
2661
+ result.candidates = { total: 0, labeled: 0, unlabeled: 0 };
2662
+ }
2663
+ result.memoryRAM = { rssKB: Math.round(process.memoryUsage().rss / 1024), heapUsedKB: Math.round(process.memoryUsage().heapUsed / 1024) };
2664
+ result.uptime = { systemHours: Math.round(os.uptime() / 3600 * 10) / 10, processHours: Math.round(process.uptime() / 3600 * 10) / 10 };
2665
+ return result;
2666
+ })
2667
+ });
2601
2668
  api.registerTool({
2602
2669
  name: "solana_startup_gate",
2603
2670
  description: "Run the mandatory startup sequence and return deterministic pass/fail results per step. Optionally auto-fixes gateway credentials if gatewayBaseUrl and gatewayToken are present in plugin config. On full pass, includes welcomeMessage. If the only failed step is solana_capital_status (e.g. capital API error), still includes welcomeMessage so the user gets onboarding text; check welcomeNote in that case.",
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceLab
3
- } from "../chunk-C24QA3MQ.js";
3
+ } from "../chunk-FBS5FGW2.js";
4
4
  import "../chunk-JO3BXAUQ.js";
5
5
  export {
6
6
  IntelligenceLab
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-traderclaw",
3
- "version": "1.0.82",
3
+ "version": "1.0.84",
4
4
  "description": "TraderClaw V1-Upgraded — Solana trading for OpenClaw with intelligence lab, tool envelopes, prompt scrubbing, read-only X social intel, and split skill docs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -215,6 +215,8 @@ HARDENED range: +100–300%. DEGEN range: +200–500%. `percent` = price increas
215
215
 
216
216
  **Pre-trade journal FIRST** — call `solana_memory_write` with tag `pre_trade_rationale` BEFORE executing. Also call `solana_decision_log` to record the decision with confidence, sizing rationale, and risk factors.
217
217
 
218
+ **Source attribution (mandatory):** The `pre_trade_rationale` memory entry MUST include `source: "<how you found this token>"` — one of: `alpha_signal:<source_name>`, `scan_launches`, `scan_hot_pairs`, `bitquery_subscription`, `manual`, `watchlist`. This is required for source trust scoring and strategy evolution to correlate wins/losses with discovery channels.
219
+
218
220
  **Prior history check (mandatory):**
219
221
  ```
220
222
  solana_memory_by_token({ tokenAddress: "CA" })
@@ -308,22 +310,31 @@ Execute exits via `solana_trade_execute` with `side: "sell"`.
308
310
 
309
311
  Partial exits → "🔴 PARTIAL EXIT (50%): SYMBOL (CA)"
310
312
 
311
- **Post-exit mandatory actions:**
313
+ **Post-exit mandatory actions (ALL required — skipping any is a critical violation):**
312
314
 
313
315
  1. Call `solana_trade_review` for each closed position.
314
316
 
315
- 2. Label the outcome for intelligence lab learning:
317
+ 2. **LABEL THE OUTCOME CRITICAL, DO NOT SKIP:**
316
318
  ```
317
319
  solana_candidate_label_outcome({ id: "CA", outcome: "win|loss|skip|dead_money", pnlPct: X.XX, holdingHours: H })
318
320
  ```
319
- This is how the intelligence lab learns. Every exit MUST be labeled.
321
+ The intelligence lab CANNOT learn without labeled outcomes. An unlabeled exit is wasted data. If you skip this call, the entire learning pipeline is broken — strategy evolution, model evaluation, and replay all depend on labeled candidates.
322
+
323
+ 3. **LEARNING ENTRY — REQUIRED after every loss or dead_money exit:**
324
+ ```
325
+ solana_memory_write({
326
+ content: "LEARNING ENTRY: LRN-YYYYMMDD-NNN\nPriority: P2\nArea: <area_tag>\nWHAT HAPPENED: <1 sentence>\nWHY IT WENT WRONG: <root cause>\nEVIDENCE: token CA, entry price, exit price, hold time\nSUGGESTED ADJUSTMENT: <what to change>",
327
+ tags: ["learning_entry", "learning_entry_<area>"]
328
+ })
329
+ ```
330
+ Losses without learning entries are the #1 reason the strategy fails to evolve. Check `refs/review-learning.md` for the full entry format and area tags.
320
331
 
321
- 3. Unsubscribe from Bitquery stream for the exited token:
332
+ 4. Unsubscribe from Bitquery stream for the exited token:
322
333
  ```
323
334
  solana_bitquery_unsubscribe({ subscriptionId: "<id>" })
324
335
  ```
325
336
 
326
- 4. If this was an alpha-sourced trade, check and record source accuracy:
337
+ 5. If this was an alpha-sourced trade, check and record source accuracy:
327
338
  ```
328
339
  solana_alpha_history({ tokenAddress: "CA", limit: 5 })
329
340
  ```
@@ -339,6 +350,11 @@ Log the source's accuracy via `solana_memory_write` with tag `source_reputation`
339
350
  - `solana_team_bulletin_post` with tag `position_update` — post current portfolio state
340
351
  - `solana_context_snapshot_write` — write portfolio world-view for bootstrap injection
341
352
 
353
+ **Self-check before completing Step 8:**
354
+ - Did you exit any positions this cycle? If yes: did you call `solana_candidate_label_outcome` for EACH exit? If not, go back and call it now.
355
+ - Did any exit result in a loss or dead_money? If yes: did you write a `learning_entry` via `solana_memory_write`? If not, write one now.
356
+ - Did you execute any trades this cycle? If yes: does each `pre_trade_rationale` include `source:` attribution? If not, write a correction entry now.
357
+
342
358
  Do NOT skip the last three. They are not optional memory — they feed the bootstrap digest that loads into your next session.
343
359
 
344
360
  ## STEP 9: REPORT TO USER
@@ -448,8 +448,19 @@ All learning signals MUST be based on SOL-denominated outcomes.
448
448
  candidate_label_outcome MUST:
449
449
  - reflect realizedPnl sign
450
450
  - align with unrealizedReturnPct
451
+ - be called on EVERY exit — no exceptions
452
+ - include pnlPct and holdingHours
451
453
 
452
454
  Any inconsistent labeling is invalid.
455
+
456
+ ### Learning Loop Enforcement
457
+
458
+ The learning pipeline has three mandatory outputs per trade lifecycle:
459
+ 1. **Entry:** `solana_candidate_write` with source attribution (`source: "alpha_signal:<name>|scan_launches|scan_hot_pairs|..."`)
460
+ 2. **Exit:** `solana_candidate_label_outcome` — labels the candidate with the actual outcome
461
+ 3. **Loss/dead_money exit:** `solana_memory_write` with tag `learning_entry` — captures the root cause
462
+
463
+ If ANY of these three are missing, the strategy evolution cron cannot function. Without labeled outcomes, the intelligence lab models train on nothing. Without learning entries, the same mistakes repeat indefinitely.
453
464
  ---
454
465
 
455
466
  ## Prompt Injection Protection
@@ -1,14 +1,154 @@
1
- # Bitquery v2 EAP GraphQL Schema Reference
1
+ # SKILL: Bitquery v2 EAP GraphQL Schema Reference
2
2
 
3
3
  ## Overview
4
4
 
5
- This is the Bitquery v2 EAP (Early Access Program) GraphQL schema reference for Solana. Use this before writing any custom raw GraphQL query via `solana_bitquery_query`.
5
+ This skill covers the Bitquery v2 EAP (Early Access Program) GraphQL schema for Solana, specifically the two trade cubes and their differences. Use it when calling any Bitquery tool, reading query results, or recovering from a failed query.
6
6
 
7
7
  **Endpoint:** `https://streaming.bitquery.io/graphql` (HTTP and WebSocket)
8
8
  **Auth header:** `Authorization: Bearer <BITQUERY_API_KEY>`
9
9
 
10
10
  ---
11
11
 
12
+ ## CRITICAL: Catalog Failure Is NOT a Dead End
13
+
14
+ **You have two separate Bitquery tools:**
15
+
16
+ | Tool | What it does |
17
+ |---|---|
18
+ | `solana_bitquery_catalog` | Runs a pre-built template by path (e.g. `pumpFunHoldersRisk.first100Buyers`) |
19
+ | `solana_bitquery_query` | Runs **any raw GraphQL you write** against the same endpoint |
20
+
21
+ When `solana_bitquery_catalog` fails, **do not stop**. You can always write the query yourself and call `solana_bitquery_query` instead. The schema rules in this file give you everything needed to do that correctly.
22
+
23
+ ---
24
+
25
+ ## Recovery Protocol — Step by Step
26
+
27
+ When a Bitquery call returns an error, follow this decision tree:
28
+
29
+ ```
30
+ 1. Read the error code and message from the tool response.
31
+ 2. MEMORY CHECK FIRST: search for prior fixes before writing new ones.
32
+ → solana_memory_search({ query: "bitquery_fix_applied <template_path_or_error_snippet>", limit: 3 })
33
+ → If a matching fix exists, apply it directly — skip to step 6.
34
+ 3. Classify the error (see table below).
35
+ 4. If SCHEMA error → apply the fix from "Common Schema Errors → Fix Map" below
36
+ → call solana_bitquery_query with corrected GraphQL.
37
+ 5. If OPERATIONAL error → retry with backoff (max 2 retries) or skip this data.
38
+ 6. If AUTH error → report to user; do not retry.
39
+ 7. MEMORY WRITE: after any recovery attempt, record the result:
40
+ → Success: solana_memory_write with tag "bitquery_fix_applied"
41
+ → Failure: solana_memory_write with tag "bitquery_recovery_failed"
42
+ → Repeated failure on same template: solana_memory_write with tag "bitquery_template_broken"
43
+ ```
44
+
45
+ ### Memory Integration for Recovery
46
+
47
+ **Search before recovering** — always check if you've fixed this error before:
48
+ ```
49
+ solana_memory_search({ query: "bitquery_fix_applied", limit: 5 })
50
+ ```
51
+ Prior fixes contain the exact corrected query and variables that worked. Reusing them is faster and more reliable than re-deriving the fix.
52
+
53
+ **Write after recovering** — record every fix attempt:
54
+ ```
55
+ // Successful fix:
56
+ solana_memory_write({
57
+ content: "Bitquery fix: <template_path> failed with '<error_message>'. Fixed by switching cube from DEXTrades to DEXTradeByTokens. Working query: <corrected_query>. Variables: <variables>.",
58
+ tags: ["bitquery_fix_applied"]
59
+ })
60
+
61
+ // Failed recovery:
62
+ solana_memory_write({
63
+ content: "Bitquery recovery failed: <template_path> error '<error_message>'. Tried: <what_was_attempted>. Result: still failing.",
64
+ tags: ["bitquery_recovery_failed"]
65
+ })
66
+
67
+ // Template consistently broken (2+ failures):
68
+ solana_memory_write({
69
+ content: "Bitquery template broken: <template_path> consistently fails with '<error_pattern>'. Recommend using solana_bitquery_query with custom GraphQL instead.",
70
+ tags: ["bitquery_template_broken"]
71
+ })
72
+ ```
73
+
74
+ This memory loop prevents the agent from re-deriving the same fix every session and creates a growing knowledge base of working query patterns.
75
+
76
+ ### Error Classification
77
+
78
+ | Response indicator | Class | Action |
79
+ |---|---|---|
80
+ | `code: "BITQUERY_QUERY_FAILED"` + message contains `Cannot query field` | SCHEMA | Fix the field/cube in your query (see Fix Map) |
81
+ | `code: "BITQUERY_QUERY_FAILED"` + message contains `Unknown argument` | SCHEMA | Remove unsupported argument (e.g. `groupBy`) |
82
+ | `code: "BITQUERY_QUERY_FAILED"` + message contains `Variable ... is never used` | SCHEMA | Remove the undeclared variable from the query signature |
83
+ | `code: "BITQUERY_QUERY_FAILED"` + message contains `Argument ... has invalid value` | SCHEMA | Fix the WHERE filter shape (wrong cube or wrong field path) |
84
+ | `code: "BITQUERY_QUERY_FAILED"` + message contains `context deadline exceeded` or `aborted` | OPERATIONAL — TIMEOUT | Add `Block: { Time: { since: $since } }` to WHERE; retry once |
85
+ | `code: "BITQUERY_QUERY_FAILED"` + HTTP 429 | OPERATIONAL — RATE LIMIT | Wait 5s and retry once; if still failing, skip and note |
86
+ | `code: "BITQUERY_QUERY_FAILED"` + HTTP 500 | OPERATIONAL — SERVER | Retry once after 3s; if still failing, skip this data point |
87
+ | `code: "BITQUERY_API_KEY_MISSING"` | AUTH | Report to user — no retry possible |
88
+ | Response `ok: false` + no `errors[]` and no HTTP status | NETWORK | Retry once; if failing, skip |
89
+ | Response `ok: true`, `data` present but all arrays are empty | NOT AN ERROR | The query is valid — the token/time window has no data |
90
+
91
+ ### Writing a Custom Query to Replace a Failed Catalog Call
92
+
93
+ 1. Call `solana_bitquery_templates` to find the closest existing template and read its operation text as a starting point.
94
+ 2. Read the error message. Match it to the Fix Map table below.
95
+ 3. Apply the fix (change cube, fix field path, remove bad argument, etc.).
96
+ 4. Call `solana_bitquery_query` with:
97
+ - `query`: your corrected GraphQL string
98
+ - `variables`: the same variables you were going to pass
99
+
100
+ **Example — catalog call fails with "Cannot query field `Currency` on DEXTrades":**
101
+ ```
102
+ // Original catalog call (fails):
103
+ solana_bitquery_catalog({ templatePath: "pumpFunTradesLiquidity.latestTradesByToken", variables: { token: "...", limit: 10 } })
104
+
105
+ // Error: Cannot query field "Currency" on type "Solana_DEXTrade_Fields_Trade"
106
+ // Fix: switch cube from DEXTrades → DEXTradeByTokens (Trade.Currency is valid there)
107
+
108
+ // Recovery call:
109
+ solana_bitquery_query({
110
+ query: `query LatestTradesByToken($token: String!, $limit: Int!) {
111
+ Solana {
112
+ DEXTradeByTokens(
113
+ where: { Trade: { Currency: { MintAddress: { is: $token } } } }
114
+ orderBy: { descending: Block_Time }
115
+ limit: { count: $limit }
116
+ ) {
117
+ Block { Time }
118
+ Trade { PriceInUSD Amount AmountInUSD Side { Type } }
119
+ }
120
+ }
121
+ }`,
122
+ variables: { token: "...", limit: 10 }
123
+ })
124
+ ```
125
+
126
+ ### When to Give Up vs When to Recover
127
+
128
+ **Always attempt recovery (1 retry with a corrected query):**
129
+ - Any SCHEMA error — you have the knowledge to fix it from this file.
130
+ - TIMEOUT — add a `since` filter and retry.
131
+
132
+ **Skip and continue without this data point:**
133
+ - Two consecutive failures on the same query path.
134
+ - AUTH errors.
135
+ - The data is supplementary (not required for the trade decision).
136
+
137
+ **Block the trade:**
138
+ - FRESH token analysis fails for `first100Buyers` AND `devHoldings` after recovery attempts — risk is unquantifiable.
139
+
140
+ ---
141
+
142
+ ## Catalog vs Live Schema — Why 400s Happen
143
+
144
+ A query path being registered in `OPENCLAW_QUERY_TEMPLATES` (e.g. `pumpFunHoldersRisk.first100Buyers`) only means the **path string** resolves to a GraphQL operation. It does **not** guarantee the operation is schema-valid.
145
+
146
+ When `runCatalogQuery` returns `ok: false` with HTTP 400 (or HTTP 200 with `errors[]`), the stored GraphQL text has a schema violation. Fix it in `SpyFly/Contexts/openClawAPI/bitQuery.js`.
147
+
148
+ The most common root causes are using `DEXTrades`-only fields (`Trade.Currency`, `Trade.Buyer`, `Trade.PriceInUSD`, `Trade.Side`, `Trade.AmountInUSD`) on the wrong cube, or using `groupBy` on `DEXTradeByTokens`.
149
+
150
+ ---
151
+
12
152
  ## The Two Trade Cubes
13
153
 
14
154
  Bitquery v2 has two Solana trade cubes with **fundamentally different `Trade` shapes**. Mixing them up causes `Cannot query field "X" on type "Solana_DEXTrade_Fields_Trade"` errors.
@@ -43,11 +183,11 @@ DEXTrades(...) {
43
183
  ```
44
184
 
45
185
  **WHERE filters in DEXTrades:**
46
- - Filter by Dex: `Trade: { Dex: { ProtocolName: { includes: "pump" } } }`
47
- - Filter by token (buy side): `Trade: { Buy: { Currency: { MintAddress: { is: $token } } } }`
48
- - Filter by signer: `Transaction: { Signer: { is: $wallet } }`
49
- - `Trade: { Currency: { MintAddress: ... } }` — **INVALID on DEXTrades**
50
- - `Trade: { Buyer: { is: $wallet } }` — **INVALID on DEXTrades**
186
+ - Filter by Dex: `Trade: { Dex: { ProtocolName: { includes: "pump" } } }`
187
+ - Filter by token (buy side): `Trade: { Buy: { Currency: { MintAddress: { is: $token } } } }`
188
+ - Filter by signer: `Transaction: { Signer: { is: $wallet } }`
189
+ - `Trade: { Currency: { MintAddress: ... } }` — **invalid on DEXTrades**
190
+ - `Trade: { Buyer: { is: $wallet } }` — **invalid on DEXTrades** — use `Transaction: { Signer: { is: $wallet } }`
51
191
 
52
192
  **Aggregate keys for DEXTrades:**
53
193
  - `sum(of: Trade_Buy_AmountInUSD)` — buy-side USD volume
@@ -79,17 +219,29 @@ DEXTradeByTokens(...) {
79
219
  ```
80
220
 
81
221
  **WHERE filters in DEXTradeByTokens:**
82
- - Filter by token: `Trade: { Currency: { MintAddress: { is: $token } } }`
83
- - Filter by side: `Trade: { Side: { Type: { is: buy } } }`
84
- - Filter by Dex: `Trade: { Dex: { ProtocolName: { includes: "pump" } } }`
222
+ - Filter by token: `Trade: { Currency: { MintAddress: { is: $token } } }`
223
+ - Filter by side: `Trade: { Side: { Type: { is: buy } } }`
224
+ - Filter by Dex: `Trade: { Dex: { ProtocolName: { includes: "pump" } } }`
85
225
 
86
226
  **Aggregate keys for DEXTradeByTokens:**
87
- - `sum(of: Trade_Side_AmountInUSD)` — total USD volume (NOT `Trade_AmountInUSD`)
227
+ - `sum(of: Trade_Side_AmountInUSD)` — total USD volume (use this, NOT `Trade_AmountInUSD`)
88
228
  - `sum(of: Trade_Amount)` — native token amount
89
- - `count(distinct: Transaction_Signer)` — unique traders (NOT `Trade_Buyer`)
229
+ - `count(distinct: Transaction_Signer)` — unique traders (use this, NOT `Trade_Buyer`)
90
230
  - `count(distinct: Transaction_Signer, if: {Trade: {Side: {Type: {is: buy}}}})` — unique buyers
91
- - `groupBy` is NOT supported; use time-bounded aggregate windows instead
92
- - If `groupBy` is removed, also remove unused variables (e.g. `$intervalSeconds`) from the operation signature
231
+ - `groupBy` is **not supported** on this cube in our current v2 endpoint profile
232
+ - If `groupBy` is removed from a query, remove now-unused variables (e.g. `$intervalSeconds`) from the operation signature and `variableShape` too.
233
+
234
+ **OHLC without groupBy:** return raw time-series rows (limit 500) and compute candles client-side:
235
+ ```graphql
236
+ DEXTradeByTokens(
237
+ where: {Block: {Time: {since: $since}}, Trade: {Currency: {MintAddress: {is: $token}}}}
238
+ orderBy: {ascending: Block_Time}
239
+ limit: {count: 500}
240
+ ) {
241
+ Block { Time }
242
+ Trade { PriceInUSD Amount AmountInUSD }
243
+ }
244
+ ```
93
245
 
94
246
  ---
95
247
 
@@ -150,6 +302,8 @@ BalanceUpdates(
150
302
  - `PostBalance` requires aggregation modifier: `PostBalance(maximum: Block_Slot)` to get the latest balance
151
303
  - `limitBy` key: use `BalanceUpdate_Account_Token_Owner` (not `BalanceUpdate_Address`)
152
304
  - WHERE path: `BalanceUpdate: { Account: { Token: { Owner: { is: $wallet } } } }`
305
+ - For lists of wallets: `Account: { Token: { Owner: { in: $holders } } }`
306
+ - `orderBy` on balance alias: use `BalanceUpdate_balance_maximum` (not `"balance"`)
153
307
 
154
308
  ---
155
309
 
@@ -157,6 +311,8 @@ BalanceUpdates(
157
311
 
158
312
  In `TokenSupplyUpdates`, the currency metadata field is `Uri` (camel-case), not `URI`.
159
313
 
314
+ Use:
315
+
160
316
  ```graphql
161
317
  TokenSupplyUpdates(
162
318
  where: {
@@ -172,9 +328,12 @@ TokenSupplyUpdates(
172
328
  }
173
329
  ```
174
330
 
331
+ Avoid:
332
+ - `Currency { ... URI }` ✗ (unknown field)
333
+
175
334
  ---
176
335
 
177
- ## Common Schema Errors and Fix Map
336
+ ## Common Schema Errors Fix Map
178
337
 
179
338
  | Error message | Root cause | Fix |
180
339
  |---|---|---|
@@ -183,16 +342,19 @@ TokenSupplyUpdates(
183
342
  | `Cannot query field "PriceInUSD" on type "Solana_DEXTrade_Fields_Trade"` | Using `Trade.PriceInUSD` on `DEXTrades` | Use `Trade.Buy.PriceInUSD` or `Trade.Sell.PriceInUSD` |
184
343
  | `Cannot query field "AmountInUSD" on type "Solana_DEXTrade_Fields_Trade"` | Using `Trade.AmountInUSD` on `DEXTrades` | Use `Trade.Buy.Amount` or switch to `DEXTradeByTokens` |
185
344
  | `Cannot query field "Buyer" on type "Solana_DEXTrade_Fields_Trade"` | Using `Trade.Buyer` on `DEXTrades` | Use `Trade.Buy.Account.Address` for output; `Transaction.Signer` for WHERE |
345
+ | `In field "Trade": In field "Buyer": Unknown field` | `Trade: { Buyer: { is: $wallet } }` in WHERE on DEXTrades | Use `Transaction: { Signer: { is: $wallet } }` |
186
346
  | `Cannot query field "Address" on type "Solana_BalanceUpdate"` | Using `BalanceUpdate.Address` | Use `BalanceUpdate.Account.Token.Owner` (SPL) or `BalanceUpdate.Account.Owner` (SOL) |
187
- | `Cannot query field "URI"` | Using uppercase `URI` in `TokenSupplyUpdate.Currency` | Use `Uri` |
188
- | `Unknown field` in `Instruction.Accounts.Address` | Using direct `Accounts.Address` in `Instructions.where` | Use `Accounts: { includes: { Address: { is: $token } } }` |
189
- | `Unknown argument "groupBy"` on `DEXTradeByTokens` | Attempting interval grouping | Remove `groupBy`; use aggregate windows over `Block.Time` ranges |
190
- | `Variable "$intervalSeconds" is never used` | Leftover variable after removing groupBy | Remove from query args and `variableShape` |
191
- | `Unexpected metric name or alias to order balance` | Ordering by non-existent alias | Order by concrete metric name (e.g. `BalanceUpdate_balance_maximum`) |
192
- | `Variable "$minCap" of type "Float!"` type mismatch | Comparator input type mismatch | Use `String` vars for `PostBalanceInUSD` filters |
193
- | `This operation was aborted` | Query exceeded timeout | Increase `options.timeoutMs` (e.g. 120000) and/or reduce scan window/limit |
347
+ | `Cannot query field "URI" on type "Solana_TokenSupplyUpdate_Fields_TokenSupplyUpdate_Currency"` | Using uppercase `URI` in `TokenSupplyUpdate.Currency` | Use `Uri` |
348
+ | `In field "Instruction" -> "Accounts" -> "Address": Unknown field` | Using direct `Accounts.Address` in `Instructions.where` | Use `Accounts: { includes: { Address: { is: $token } } }` |
349
+ | `Unknown argument "groupBy" on field "DEXTradeByTokens" of type "Solana"` | Attempting interval grouping on `DEXTradeByTokens` | Remove `groupBy`; return raw rows and build candles client-side |
350
+ | `Variable "$intervalSeconds" is never used in operation ...` | Query signature still includes interval variable after removing groupBy | Remove `$intervalSeconds` from query args and `variableShape` |
351
+ | `Variable "$wallet" is never used in operation ...` | Variable declared in operation but no field references it | Remove `$wallet` from query args and `variableShape` |
352
+ | `Unexpected metric name or alias to order balance ...` | Ordering by non-existent alias like `"balance"` | Order by concrete metric name returned by engine (e.g. `BalanceUpdate_balance_maximum`) |
353
+ | `Variable "$minCap" of type "Float!" used ... expecting type "String"` | Comparator input type mismatch in token supply filters | Use `String` vars for `PostBalanceInUSD` bound filters in this endpoint profile |
354
+ | `This operation was aborted` / `context deadline exceeded` | Query exceeded request timeout budget — often an unbounded Instructions scan | Add `Block: { Time: { since: $since } }` to the WHERE clause; increase `options.timeoutMs` if needed |
194
355
  | `Field "Trade_Buyer" not found` | Aggregate `count(distinct: Trade_Buyer)` | Use `count(distinct: Transaction_Signer)` |
195
- | `Field "Trade_AmountInUSD" not found` (DEXTradeByTokens) | Wrong aggregate key | Use `Trade_Side_AmountInUSD` |
356
+ | `Field "Trade_AmountInUSD" not found` (in DEXTradeByTokens) | Wrong aggregate key | Use `Trade_Side_AmountInUSD` |
357
+ | `calculate(expression: ...)` not recognized | Unsupported computed field expression | Remove `calculate`; compute derived values client-side instead |
196
358
 
197
359
  ---
198
360
 
@@ -225,15 +387,19 @@ DEXPools(
225
387
 
226
388
  ---
227
389
 
228
- ## Instructions Cube — Account Filters
390
+ ## Instructions Cube — Account Filters (Important)
229
391
 
230
- For `Solana.Instructions`, account matching in `where.Instruction.Accounts` must use `includes`, not direct `Address` equality.
392
+ For `Solana.Instructions`, account matching in `where.Instruction.Accounts` must use
393
+ `includes`, not direct `Address` equality.
394
+
395
+ Use:
231
396
 
232
397
  ```graphql
233
398
  Instructions(
234
399
  where: {
400
+ Block: { Time: { since: $since } }
235
401
  Instruction: {
236
- Program: { Name: { includes: "pump" } }
402
+ Program: { Name: { includes: "pump" }, Method: { includes: "create" } }
237
403
  Accounts: { includes: { Address: { is: $token } } }
238
404
  }
239
405
  Transaction: { Result: { Success: true } }
@@ -246,8 +412,13 @@ Instructions(
246
412
  ```
247
413
 
248
414
  Avoid:
249
- - `Accounts: { Address: { is: $token } }` (invalid shape)
250
- - Duplicate keys in one input object — combine into one: `Program: { Name: ..., Method: ... }`
415
+ - `Accounts: { Address: { is: $token } }` (invalid shape)
416
+
417
+ Also avoid duplicate keys in one input object:
418
+ - `Program: { Name: ... }` and a second `Program: { Method: ... }` in the same object is **invalid** — GraphQL only keeps the last key.
419
+ - Combine into one: `Program: { Name: ..., Method: ... }` ✓
420
+
421
+ **Always add a `Block.Time.since` filter to Instructions queries** — unbounded Instructions scans (especially with ascending orderBy to find creation events) time out consistently.
251
422
 
252
423
  ---
253
424
 
@@ -257,7 +428,38 @@ Avoid:
257
428
  - **DEX filter:** `Trade: { Dex: { ProtocolName: { includes: "pump" } } }`
258
429
  - **PumpSwap filter:** `Trade: { Dex: { ProtocolName: { includes: "pumpswap" } } }`
259
430
  - **Migration detection:** `Instructions` cube with `Program: { Method: { includes: "migrate" } }`
260
- - **Bonding curve progress:** Requires `DEXPools` with `Base.PostAmountInUSD`
431
+ - **Bonding curve progress:** Requires `DEXPools` with `Base.PostAmountInUSD` — exact threshold formula requires live testing
432
+ - **First buyers:** Use `DEXTrades` with `Trade: { Buy: { Currency: { MintAddress: { is: $token } } } }` and `orderBy: { ascending: Block_Time }` — output buyer address as `Trade.Buy.Account.Address`
433
+
434
+ ### Token Creation — Correct Method Filter
435
+
436
+ Filtering Instructions by `Name: {includes: "pump"}` alone returns **all** pump.fun interactions (buys, sells, fees). To target **only new token launches**, filter by the exact program address **and** method:
437
+
438
+ ```graphql
439
+ Instruction: {
440
+ Program: {
441
+ Address: {is: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"}
442
+ Method: {is: "create_v2"}
443
+ }
444
+ }
445
+ ```
446
+
447
+ In the result, `Accounts[0].Address` is the **token mint** and `Transaction.Signer` is the **dev wallet**. Not all pump.fun mints use the `pump` vanity suffix — do not filter by address suffix.
448
+
449
+ Known pump.fun method values (live-verified, 30-day sample of 2000 instructions):
450
+ | Method | Meaning |
451
+ |---|---|
452
+ | `create_v2` | New token launch — **use this for new token detection** |
453
+ | `CreateEvent` | CPI event emitted alongside `create_v2` (single account, less data) |
454
+ | `buy` / `buy_exact_sol_in` / `buy_exact_quote_in` | Buy trades |
455
+ | `sell` | Sell trade |
456
+ | `BuyEvent` / `SellEvent` / `TradeEvent` | CPI event logs for trades |
457
+ | `collect_creator_fee` / `CollectCreatorFeeEvent` | Creator fee collection |
458
+ | `extend_account` / `ExtendAccountEvent` | Account reallocation |
459
+ | `close_user_volume_accumulator` | Volume accumulator housekeeping |
460
+ | `sync_user_volume_accumulator` / `init_user_volume_accumulator` | Volume accumulator lifecycle |
461
+
462
+ **"Mayhem Mode" does not exist on-chain.** No instruction with `mayhem` in the method name has ever appeared in the pump.fun program. The three catalog templates for it (`trackMayhemModeRealtime`, `currentMayhemModeStatus`, `historicalMayhemModeStatus`) have been removed.
261
463
 
262
464
  ---
263
465
 
@@ -279,8 +481,8 @@ subscription PumpFunTrades($token: String) {
279
481
  Block { Time }
280
482
  Transaction { Signature }
281
483
  Trade {
282
- Buy { Currency { MintAddress Symbol } PriceInUSD }
283
- Sell { Currency { MintAddress Symbol } PriceInUSD }
484
+ Buy { Currency { MintAddress Symbol } PriceInUSD Amount }
485
+ Sell { Currency { MintAddress Symbol } }
284
486
  }
285
487
  }
286
488
  }
@@ -69,6 +69,16 @@ Used by Steps 0, 5.5, 8.5, 9.
69
69
  | `strategy_evolution` | Strategy evolution reasoning log (Step 9) |
70
70
  | `discovery_filter_evolution` | Discovery filter parameter updates (Step 9) |
71
71
 
72
+ ## Bitquery Recovery Tags
73
+
74
+ Used when Bitquery API calls fail and the agent applies recovery procedures.
75
+
76
+ | Tag | Purpose |
77
+ |---|---|
78
+ | `bitquery_fix_applied` | Successful Bitquery recovery — records error class, fix applied, and working query/variables |
79
+ | `bitquery_recovery_failed` | Recovery attempt failed — records error class, what was tried, and why it didn't work |
80
+ | `bitquery_template_broken` | Template identified as consistently broken — records template path, error pattern, suggested replacement |
81
+
72
82
  ## System Tags
73
83
 
74
84
  | Tag | Purpose |
@@ -88,3 +98,4 @@ Used by Steps 0, 5.5, 8.5, 9.
88
98
  | `coordinated_shill_detected` | Coordinated shill campaign detected |
89
99
  | `website_analyzed` | Website legitimacy analysis result |
90
100
  | `alpha_source_model` | Personal alpha source trust model |
101
+ | `memory_trim` | Memory trim cron results |
@@ -37,7 +37,6 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
37
37
  | `solana_wallet_token_balance` | On-chain SPL balance (POST) | Step 0 when position balance seems off; Step 6 for on-chain verification |
38
38
  | `solana_sweep_dead_tokens` | Batch-exit losing positions | `dead_money_sweep` cron; Step 0 when multiple dead positions found |
39
39
  | `solana_killswitch_status` | Check kill switch state | Step 0 every heartbeat |
40
- | `solana_killswitch` | Toggle kill switch (enabled + mode) | When consecutive loss limit hit; when user requests |
41
40
  | `risk_management_get_default` | Read per-wallet default TP/SL/trailing used when buys omit risk | Before relying on implicit defaults; after user asks about protection |
42
41
  | `risk_management_set_default` | Save per-wallet default exit plan for future buys | When user or policy wants custom defaults instead of platform system default |
43
42
  | `trade_size_limit_get` | Read max **buy** size (SOL) from wallet `limits` (default 1.5) | Before every buy or when user asks about size limits |