solana-traderclaw 1.0.147 → 1.0.148
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/config/gateway-v1-upgraded.json5 +2 -2
- package/config/gateway-v1.json5 +2 -2
- package/dist/{chunk-S2DLZKMQ.js → chunk-QIURFAOS.js} +56 -5
- package/dist/index.js +86 -18
- package/dist/src/bitquery-ws.js +1 -1
- package/openclaw.plugin.json +2 -0
- package/package.json +1 -1
- package/skills/solana-trader/SKILL.md +15 -4
- package/skills/solana-trader/refs/cron-jobs.md +15 -6
- package/skills/solana-trader/refs/trade-execution.md +2 -2
- package/skills/solana-trader/refs/ws-subscription-health.md +40 -0
- package/skills/solana-trader/workspace/TOOLS.md +9 -7
|
@@ -80,11 +80,11 @@
|
|
|
80
80
|
// ── Portfolio Maintenance ───────────────────────────────────────
|
|
81
81
|
{
|
|
82
82
|
id: "subscription-cleanup",
|
|
83
|
-
schedule: "15 */
|
|
83
|
+
schedule: "15 */2 * * *",
|
|
84
84
|
agentId: "main",
|
|
85
85
|
sessionTarget: "isolated",
|
|
86
86
|
delivery: { mode: "announce", channel: "last", bestEffort: true },
|
|
87
|
-
message: "CRON_JOB: subscription_cleanup\n\
|
|
87
|
+
message: "CRON_JOB: subscription_cleanup\n\nGOAL: Keep Bitquery subscriptions healthy — one realtimeTokenPricesSolana per OPEN position, total active subs UNDER 20. Do NOT touch alpha (solana_alpha_unsubscribe forbidden).\n\nSTEP 1 — Baseline:\n solana_runtime_status → note alphaStream.subscribed + bitqueryStream\n solana_positions → open position CAs (mint addresses)\n solana_bitquery_subscriptions → active subs + connectedWebSocketClients (if AUTH_SCOPE_MISSING, log and stop)\n\nSTEP 2 — Compute targets:\n expectedSubs = count(open positions)\n healthy TCP clients to orchestrator WS ≈ 1–2 (not 10+)\n per-client Bitquery sub cap = 20 (OPENCLAW_WS_MAX_SUBS_PER_CLIENT)\n\nSTEP 3 — Reconcile:\n Each active subscription where variables.token NOT in open CAs → solana_bitquery_unsubscribe({ subscriptionId })\n Each open CA missing realtimeTokenPricesSolana → solana_bitquery_subscribe({ templateKey: \"realtimeTokenPricesSolana\", variables: { token: \"<CA>\" }, agentId: \"main\" })\n Subs nearing 24h expiry or subscription_expiring → solana_bitquery_subscription_reopen\n\nSTEP 4 — Escalate if still unhealthy after reconcile:\n If reported activeSubscriptionCount > 20 OR subscribe errors WS_PER_KEY_LIMIT / WS_SUBSCRIPTION_LIMIT_REACHED OR absurd connectedWebSocketClients:\n solana_memory_write tag subscription_cleanup severity CRITICAL — gateway restart may be required to drop leaked TCP sockets (cron cannot restart the gateway).\n\nSTEP 5 — Report:\n solana_memory_write tag subscription_cleanup — before/after: openPositions, activeSubs, connectedWebSocketClients, orphansRemoved, subsAdded, reopenedCount.",
|
|
88
88
|
enabled: true
|
|
89
89
|
},
|
|
90
90
|
|
package/config/gateway-v1.json5
CHANGED
|
@@ -69,11 +69,11 @@
|
|
|
69
69
|
// ── Portfolio Maintenance ───────────────────────────────────────
|
|
70
70
|
{
|
|
71
71
|
id: "subscription-cleanup",
|
|
72
|
-
schedule: "15 */
|
|
72
|
+
schedule: "15 */2 * * *",
|
|
73
73
|
agentId: "main",
|
|
74
74
|
sessionTarget: "isolated",
|
|
75
75
|
delivery: { mode: "announce", channel: "last", bestEffort: true },
|
|
76
|
-
message: "CRON_JOB: subscription_cleanup\n\
|
|
76
|
+
message: "CRON_JOB: subscription_cleanup\n\nGOAL: Keep Bitquery subscriptions healthy — one realtimeTokenPricesSolana per OPEN position, total active subs UNDER 20. Do NOT touch alpha (solana_alpha_unsubscribe forbidden).\n\nSTEP 1 — Baseline:\n solana_runtime_status → note alphaStream.subscribed + bitqueryStream\n solana_positions → open position CAs (mint addresses)\n solana_bitquery_subscriptions → active subs + connectedWebSocketClients (if AUTH_SCOPE_MISSING, log and stop)\n\nSTEP 2 — Compute targets:\n expectedSubs = count(open positions)\n healthy TCP clients to orchestrator WS ≈ 1–2 (not 10+)\n per-client Bitquery sub cap = 20 (OPENCLAW_WS_MAX_SUBS_PER_CLIENT)\n\nSTEP 3 — Reconcile:\n Each active subscription where variables.token NOT in open CAs → solana_bitquery_unsubscribe({ subscriptionId })\n Each open CA missing realtimeTokenPricesSolana → solana_bitquery_subscribe({ templateKey: \"realtimeTokenPricesSolana\", variables: { token: \"<CA>\" }, agentId: \"main\" })\n Subs nearing 24h expiry or subscription_expiring → solana_bitquery_subscription_reopen\n\nSTEP 4 — Escalate if still unhealthy after reconcile:\n If reported activeSubscriptionCount > 20 OR subscribe errors WS_PER_KEY_LIMIT / WS_SUBSCRIPTION_LIMIT_REACHED OR absurd connectedWebSocketClients:\n solana_memory_write tag subscription_cleanup severity CRITICAL — gateway restart may be required to drop leaked TCP sockets (cron cannot restart the gateway).\n\nSTEP 5 — Report:\n solana_memory_write tag subscription_cleanup — before/after: openPositions, activeSubs, connectedWebSocketClients, orphansRemoved, subsAdded, reopenedCount.",
|
|
77
77
|
enabled: true
|
|
78
78
|
},
|
|
79
79
|
|
|
@@ -62,23 +62,72 @@ var BitqueryStreamManager = class {
|
|
|
62
62
|
async unsubscribe(subscriptionId) {
|
|
63
63
|
this.activeSubscriptions.delete(subscriptionId);
|
|
64
64
|
if (!this.ws || this.ws.readyState !== 1) {
|
|
65
|
+
this.disconnectIfIdle();
|
|
65
66
|
return { unsubscribed: true };
|
|
66
67
|
}
|
|
67
68
|
return new Promise((resolve) => {
|
|
69
|
+
const finish = () => {
|
|
70
|
+
resolve({ unsubscribed: true });
|
|
71
|
+
this.disconnectIfIdle();
|
|
72
|
+
};
|
|
68
73
|
const timeout = setTimeout(() => {
|
|
69
74
|
this.pendingUnsubscribes.delete(subscriptionId);
|
|
70
|
-
|
|
75
|
+
finish();
|
|
71
76
|
}, 1e4);
|
|
72
|
-
this.pendingUnsubscribes.set(subscriptionId, { resolve, timeout });
|
|
77
|
+
this.pendingUnsubscribes.set(subscriptionId, { resolve: finish, timeout });
|
|
73
78
|
try {
|
|
74
79
|
this.ws.send(JSON.stringify({ type: "bitquery_unsubscribe", subscriptionId }));
|
|
75
80
|
} catch {
|
|
76
81
|
clearTimeout(timeout);
|
|
77
82
|
this.pendingUnsubscribes.delete(subscriptionId);
|
|
78
|
-
|
|
83
|
+
finish();
|
|
79
84
|
}
|
|
80
85
|
});
|
|
81
86
|
}
|
|
87
|
+
/** Best-effort: notify orchestrator before gateway reconnect / dispose clears local mux. Sequential to avoid reorder races on the FIFO pending queue. */
|
|
88
|
+
async unsubscribeAll() {
|
|
89
|
+
const ids = [...this.activeSubscriptions.keys()];
|
|
90
|
+
let errors = 0;
|
|
91
|
+
for (const id of ids) {
|
|
92
|
+
try {
|
|
93
|
+
await this.unsubscribe(id);
|
|
94
|
+
} catch {
|
|
95
|
+
errors += 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.disconnectIfIdle();
|
|
99
|
+
return { attempted: ids.length, errors };
|
|
100
|
+
}
|
|
101
|
+
/** WebSocket OPEN (TCP connected), not necessarily post-auth Bitquery-ready. */
|
|
102
|
+
isWebsocketOpen() {
|
|
103
|
+
return this.ws !== null && this.ws.readyState === 1;
|
|
104
|
+
}
|
|
105
|
+
getActiveTokens() {
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const sub of this.activeSubscriptions.values()) {
|
|
108
|
+
const t = sub.variables?.token;
|
|
109
|
+
if (typeof t === "string" && t.trim()) out.push(t.trim());
|
|
110
|
+
}
|
|
111
|
+
return [...new Set(out)];
|
|
112
|
+
}
|
|
113
|
+
getActiveSubscriptionIds() {
|
|
114
|
+
return [...this.activeSubscriptions.keys()];
|
|
115
|
+
}
|
|
116
|
+
getStats() {
|
|
117
|
+
const ls = this.config.lifetimeState;
|
|
118
|
+
return {
|
|
119
|
+
lifetimeConnectCount: ls?.lifetimeConnectCount ?? 0,
|
|
120
|
+
reconnectAttempt: this.reconnectAttempt,
|
|
121
|
+
intentionalClose: this.intentionalClose,
|
|
122
|
+
websocketOpen: this.isWebsocketOpen(),
|
|
123
|
+
authenticated: this.authenticated,
|
|
124
|
+
activeSubscriptionCount: this.activeSubscriptions.size
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
bumpLifetimeConnect() {
|
|
128
|
+
const ls = this.config.lifetimeState;
|
|
129
|
+
if (ls) ls.lifetimeConnectCount += 1;
|
|
130
|
+
}
|
|
82
131
|
/** Close the WS if no active subscriptions remain. */
|
|
83
132
|
disconnectIfIdle() {
|
|
84
133
|
if (this.activeSubscriptions.size === 0) {
|
|
@@ -190,6 +239,7 @@ var BitqueryStreamManager = class {
|
|
|
190
239
|
ws.on("open", () => {
|
|
191
240
|
clearTimeout(connectTimeout);
|
|
192
241
|
this.reconnectAttempt = 0;
|
|
242
|
+
this.bumpLifetimeConnect();
|
|
193
243
|
this.log("info", "Connected");
|
|
194
244
|
pingInterval = setInterval(() => {
|
|
195
245
|
if (!this.ws || this.ws.readyState !== 1) return;
|
|
@@ -274,7 +324,7 @@ var BitqueryStreamManager = class {
|
|
|
274
324
|
if (pending) {
|
|
275
325
|
clearTimeout(pending.timeout);
|
|
276
326
|
this.pendingUnsubscribes.delete(subscriptionId);
|
|
277
|
-
pending.resolve(
|
|
327
|
+
pending.resolve();
|
|
278
328
|
}
|
|
279
329
|
this.disconnectIfIdle();
|
|
280
330
|
break;
|
|
@@ -286,6 +336,7 @@ var BitqueryStreamManager = class {
|
|
|
286
336
|
"WS_SUBSCRIBE_VALIDATION_ERROR",
|
|
287
337
|
"BITQUERY_SUBSCRIPTION_TEMPLATE_NOT_FOUND",
|
|
288
338
|
"WS_SUBSCRIPTION_LIMIT_REACHED",
|
|
339
|
+
"WS_PER_KEY_LIMIT",
|
|
289
340
|
"WS_BRIDGE_UNAVAILABLE"
|
|
290
341
|
].includes(code)) {
|
|
291
342
|
const pending = this.pendingSubscribeQueue.shift();
|
|
@@ -309,7 +360,7 @@ var BitqueryStreamManager = class {
|
|
|
309
360
|
this.pendingSubscribeQueue = [];
|
|
310
361
|
for (const [, pending] of this.pendingUnsubscribes) {
|
|
311
362
|
clearTimeout(pending.timeout);
|
|
312
|
-
pending.resolve(
|
|
363
|
+
pending.resolve();
|
|
313
364
|
}
|
|
314
365
|
this.pendingUnsubscribes.clear();
|
|
315
366
|
}
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "./chunk-GX4HA22L.js";
|
|
23
23
|
import {
|
|
24
24
|
BitqueryStreamManager
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-QIURFAOS.js";
|
|
26
26
|
import {
|
|
27
27
|
shouldSyncGatewayCredentials
|
|
28
28
|
} from "./chunk-R24UDHQG.js";
|
|
@@ -784,6 +784,9 @@ var SOLANA_TRADER_ALPHA_BUFFER_SINGLETON_KEY = Symbol.for(
|
|
|
784
784
|
var SOLANA_TRADER_ALPHA_LIFETIME_SINGLETON_KEY = Symbol.for(
|
|
785
785
|
"openclaw.solana-trader.alpha-lifetime.v1"
|
|
786
786
|
);
|
|
787
|
+
var SOLANA_TRADER_BITQUERY_LIFETIME_SINGLETON_KEY = Symbol.for(
|
|
788
|
+
"openclaw.solana-trader.bitquery-lifetime.v1"
|
|
789
|
+
);
|
|
787
790
|
var __solanaTraderAlphaSingletonHolder = globalThis;
|
|
788
791
|
function getOrCreateAlphaBuffer() {
|
|
789
792
|
const existing = __solanaTraderAlphaSingletonHolder[SOLANA_TRADER_ALPHA_BUFFER_SINGLETON_KEY];
|
|
@@ -803,6 +806,13 @@ function getOrCreateAlphaLifetimeState() {
|
|
|
803
806
|
__solanaTraderAlphaSingletonHolder[SOLANA_TRADER_ALPHA_LIFETIME_SINGLETON_KEY] = fresh;
|
|
804
807
|
return fresh;
|
|
805
808
|
}
|
|
809
|
+
function getOrCreateBitqueryLifetimeState() {
|
|
810
|
+
const existing = __solanaTraderAlphaSingletonHolder[SOLANA_TRADER_BITQUERY_LIFETIME_SINGLETON_KEY];
|
|
811
|
+
if (existing) return existing;
|
|
812
|
+
const fresh = { lifetimeConnectCount: 0 };
|
|
813
|
+
__solanaTraderAlphaSingletonHolder[SOLANA_TRADER_BITQUERY_LIFETIME_SINGLETON_KEY] = fresh;
|
|
814
|
+
return fresh;
|
|
815
|
+
}
|
|
806
816
|
var __solanaTraderStatusQueriesMd = (() => {
|
|
807
817
|
try {
|
|
808
818
|
const distPath = fileURLToPath(import.meta.url);
|
|
@@ -2064,7 +2074,7 @@ ${notes}
|
|
|
2064
2074
|
});
|
|
2065
2075
|
api.registerTool({
|
|
2066
2076
|
name: "trade_size_limit_get",
|
|
2067
|
-
description: "Read the per-wallet maximum
|
|
2077
|
+
description: "Read the effective per-wallet **maximum buy** size in SOL from `GET /api/wallet/max-trade-size` (merges `buyAmounts.maxBuyAmountSol` with legacy `maxTradeSizeSol`). There is no platform default cap when unset \u2014 use the response before sizing buys.",
|
|
2068
2078
|
parameters: Type.Object({}),
|
|
2069
2079
|
execute: wrapExecute(
|
|
2070
2080
|
"trade_size_limit_get",
|
|
@@ -2073,7 +2083,7 @@ ${notes}
|
|
|
2073
2083
|
});
|
|
2074
2084
|
api.registerTool({
|
|
2075
2085
|
name: "trade_size_limit_set",
|
|
2076
|
-
description: "Set the
|
|
2086
|
+
description: "Set the maximum buy size in SOL via `PUT /api/wallet/max-trade-size`. The server also mirrors this into `buyAmounts.maxBuyAmountSol` and sets `buyAmountEnforcement` to `hard` when it was `off`, so executes clamp to this max without denying the trade.",
|
|
2077
2087
|
parameters: Type.Object({
|
|
2078
2088
|
maxTradeSizeSol: Type.Number({ exclusiveMinimum: 0 })
|
|
2079
2089
|
}),
|
|
@@ -2085,6 +2095,44 @@ ${notes}
|
|
|
2085
2095
|
})
|
|
2086
2096
|
)
|
|
2087
2097
|
});
|
|
2098
|
+
const buyAmountSolOptional = Type.Union([
|
|
2099
|
+
Type.Number({ exclusiveMinimum: 0 }),
|
|
2100
|
+
Type.Null()
|
|
2101
|
+
]);
|
|
2102
|
+
api.registerTool({
|
|
2103
|
+
name: "buy_amount_policy_get",
|
|
2104
|
+
description: "Read `buyAmountEnforcement` and `buyAmounts` (fixed / min / max buy in SOL) from the wallet trading policy \u2014 same settings as the dashboard **Buy amount** card. Use when the user enforces sizing so you do not fight precheck/execute clamps.",
|
|
2105
|
+
parameters: Type.Object({}),
|
|
2106
|
+
execute: wrapExecute("buy_amount_policy_get", async () => {
|
|
2107
|
+
const data = await get(`/api/wallet/trading-policy?walletId=${walletId}`);
|
|
2108
|
+
return {
|
|
2109
|
+
buyAmountEnforcement: data.buyAmountEnforcement ?? "off",
|
|
2110
|
+
buyAmounts: data.buyAmounts ?? {}
|
|
2111
|
+
};
|
|
2112
|
+
})
|
|
2113
|
+
});
|
|
2114
|
+
api.registerTool({
|
|
2115
|
+
name: "buy_amount_policy_set",
|
|
2116
|
+
description: "Update buy sizing policy (`PATCH /api/wallet/trading-policy`). `hard` clamps each buy to fixed or min/max SOL without denying for size alone. `soft` only adds warnings; requested size still runs. Pass `null` for a bound to clear it.",
|
|
2117
|
+
parameters: Type.Object({
|
|
2118
|
+
buyAmountEnforcement: Type.Optional(
|
|
2119
|
+
Type.Union([Type.Literal("off"), Type.Literal("soft"), Type.Literal("hard")])
|
|
2120
|
+
),
|
|
2121
|
+
fixedBuyAmountSol: Type.Optional(buyAmountSolOptional),
|
|
2122
|
+
minBuyAmountSol: Type.Optional(buyAmountSolOptional),
|
|
2123
|
+
maxBuyAmountSol: Type.Optional(buyAmountSolOptional)
|
|
2124
|
+
}),
|
|
2125
|
+
execute: wrapExecute("buy_amount_policy_set", async (_id, params) => {
|
|
2126
|
+
const body = { walletId };
|
|
2127
|
+
if (params.buyAmountEnforcement !== void 0) body.buyAmountEnforcement = params.buyAmountEnforcement;
|
|
2128
|
+
const buyAmounts = {};
|
|
2129
|
+
if (params.fixedBuyAmountSol !== void 0) buyAmounts.fixedBuyAmountSol = params.fixedBuyAmountSol;
|
|
2130
|
+
if (params.minBuyAmountSol !== void 0) buyAmounts.minBuyAmountSol = params.minBuyAmountSol;
|
|
2131
|
+
if (params.maxBuyAmountSol !== void 0) buyAmounts.maxBuyAmountSol = params.maxBuyAmountSol;
|
|
2132
|
+
if (Object.keys(buyAmounts).length) body.buyAmounts = buyAmounts;
|
|
2133
|
+
return patch("/api/wallet/trading-policy", body);
|
|
2134
|
+
})
|
|
2135
|
+
});
|
|
2088
2136
|
api.registerTool({
|
|
2089
2137
|
name: "risk_management_set_default",
|
|
2090
2138
|
description: "Save per-wallet default exit parameters (`tpExits`, `slExits`, `trailingStop.levels`) applied on buys that omit risk fields. Same shape as trade execute exit payloads.",
|
|
@@ -2471,9 +2519,11 @@ ${notes}
|
|
|
2471
2519
|
})
|
|
2472
2520
|
)
|
|
2473
2521
|
});
|
|
2522
|
+
const bitqueryLifetimeState = getOrCreateBitqueryLifetimeState();
|
|
2474
2523
|
const bitqueryStreamManager = new BitqueryStreamManager({
|
|
2475
2524
|
wsUrl: orchestratorUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/ws",
|
|
2476
2525
|
walletId,
|
|
2526
|
+
lifetimeState: bitqueryLifetimeState,
|
|
2477
2527
|
getAccessToken: () => sessionManager.getAccessToken(),
|
|
2478
2528
|
logger: {
|
|
2479
2529
|
info: (msg) => api.logger.info(`[solana-trader] ${msg}`),
|
|
@@ -2482,10 +2532,16 @@ ${notes}
|
|
|
2482
2532
|
}
|
|
2483
2533
|
});
|
|
2484
2534
|
__solanaTraderDisposers.push(() => {
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2535
|
+
void (async () => {
|
|
2536
|
+
try {
|
|
2537
|
+
await bitqueryStreamManager.unsubscribeAll();
|
|
2538
|
+
} catch {
|
|
2539
|
+
}
|
|
2540
|
+
try {
|
|
2541
|
+
bitqueryStreamManager.close();
|
|
2542
|
+
} catch {
|
|
2543
|
+
}
|
|
2544
|
+
})();
|
|
2489
2545
|
});
|
|
2490
2546
|
api.registerTool({
|
|
2491
2547
|
name: "solana_bitquery_subscribe",
|
|
@@ -3032,18 +3088,30 @@ ${notes}
|
|
|
3032
3088
|
});
|
|
3033
3089
|
api.registerTool({
|
|
3034
3090
|
name: "solana_runtime_status",
|
|
3035
|
-
description: "Return plugin runtime diagnostics
|
|
3091
|
+
description: "Return plugin runtime diagnostics: startup-gate cache, alpha stream (lifetime stats vs current socket), Bitquery mux (lifetime connect count, websocket/auth flags, tracked subscription IDs + mint tokens vs orchestrator diagnostics from solana_bitquery_subscriptions), and latest forwarding probe result. Use subscription_cleanup cron baseline.",
|
|
3036
3092
|
parameters: Type.Object({}),
|
|
3037
|
-
execute: wrapExecute("solana_runtime_status", async () =>
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3093
|
+
execute: wrapExecute("solana_runtime_status", async () => {
|
|
3094
|
+
const bitqueryStats = bitqueryStreamManager.getStats();
|
|
3095
|
+
const wsOpen = bitqueryStreamManager.isWebsocketOpen();
|
|
3096
|
+
return {
|
|
3097
|
+
startupGate: startupGateState,
|
|
3098
|
+
alphaStream: {
|
|
3099
|
+
subscribed: alphaStreamManager.isSubscribed(),
|
|
3100
|
+
ingestionStale: alphaStreamManager.isIngestionStale(),
|
|
3101
|
+
stats: alphaStreamManager.getStats(),
|
|
3102
|
+
bufferSize: alphaBuffer.getBufferSize()
|
|
3103
|
+
},
|
|
3104
|
+
bitqueryStream: {
|
|
3105
|
+
stats: bitqueryStats,
|
|
3106
|
+
connected: wsOpen && bitqueryStats.authenticated,
|
|
3107
|
+
websocketOpen: wsOpen,
|
|
3108
|
+
activeSubscriptionCount: bitqueryStats.activeSubscriptionCount,
|
|
3109
|
+
activeSubscriptionIds: bitqueryStreamManager.getActiveSubscriptionIds(),
|
|
3110
|
+
activeTokens: bitqueryStreamManager.getActiveTokens()
|
|
3111
|
+
},
|
|
3112
|
+
lastForwardProbe: lastForwardProbeState
|
|
3113
|
+
};
|
|
3114
|
+
})
|
|
3047
3115
|
});
|
|
3048
3116
|
api.registerTool({
|
|
3049
3117
|
name: "solana_state_save",
|
package/dist/src/bitquery-ws.js
CHANGED
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-traderclaw",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.148",
|
|
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",
|
|
@@ -75,12 +75,22 @@ Configured by the user on the **Buy Strategy** page. Checks token metrics before
|
|
|
75
75
|
|
|
76
76
|
**Agent impact:** When `hard`, if you try to buy a token outside user bounds, the orchestrator returns a denial. Do not retry with the same token. Report the bound that was exceeded.
|
|
77
77
|
|
|
78
|
-
###
|
|
78
|
+
### Buy Amount Policy (`buyAmountEnforcement`)
|
|
79
|
+
|
|
80
|
+
Configured on the **Buy Strategy** page (SOL sizing). This is separate from **buy filters** (market cap, liquidity, etc.).
|
|
79
81
|
|
|
80
|
-
|
|
82
|
+
| Mode | Behavior |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `off` | No discretionary buy-amount policy. **Exception:** if the user previously set only legacy `maxTradeSizeSol` / `trade_size_limit_set` with no `buyAmounts` keys, the orchestrator still clamps buys to that maximum. |
|
|
85
|
+
| `soft` | Buy runs at your requested `sizeSol`, but precheck/execute include **warnings** in `metadata.reasons` when fixed/min/max bounds are violated. |
|
|
86
|
+
| `hard` | Buy **size is clamped** to the configured fixed SOL amount, or bounded by min/max SOL. Trades are **not** denied for sizing alone. Precheck returns `metadata.cappedSizeSol`; when it changed, `metadata.buyAmountAdjusted` is true and execute may include `policySizing`. |
|
|
87
|
+
|
|
88
|
+
**Tools:** `buy_amount_policy_get`, `buy_amount_policy_set`, and `trade_size_limit_get` / `trade_size_limit_set` (max SOL, mirrored into `buyAmounts`).
|
|
89
|
+
|
|
90
|
+
### Soft-Enforced Limits (size reduction, not denial)
|
|
81
91
|
|
|
82
|
-
- **
|
|
83
|
-
-
|
|
92
|
+
- **Buy amount `hard` mode** and **legacy max trade size** clamp executed buy size. Treat precheck `metadata.cappedSizeSol` as authoritative when present; align execute `sizeSol` with that value to avoid policy drift.
|
|
93
|
+
- Older notes about automatic halving from top-10 concentration or silent `maxPositionUsd` caps may not match every response — **always trust `solana_trade_precheck` metadata** over static tables.
|
|
84
94
|
|
|
85
95
|
### Risk Exit Enforcement (`riskEnforcement`)
|
|
86
96
|
|
|
@@ -719,3 +729,4 @@ All decision making, evaluation, and learning MUST use SOL-based values.
|
|
|
719
729
|
| `bitquery-schema.md` | Bitquery v2 EAP schema reference |
|
|
720
730
|
| `query-catalog.md` | Bitquery query template catalog |
|
|
721
731
|
| `websocket-streaming.md` | WebSocket message contract and subscription lifecycle |
|
|
732
|
+
| `refs/ws-subscription-health.md` | Cron + mux health, 20-cap, leaked-socket escalation |
|
|
@@ -95,13 +95,22 @@ At start of every cron job, check whether sufficient new data exists since last
|
|
|
95
95
|
|
|
96
96
|
## Job: `subscription_cleanup`
|
|
97
97
|
|
|
98
|
-
**Schedule:** Every
|
|
98
|
+
**Schedule:** Every 2 hours, offset by 15 min (`15 */2 * * *`) — ~12 runs/day
|
|
99
99
|
|
|
100
|
-
**Purpose:**
|
|
100
|
+
**Purpose:** Keep Bitquery subscription count healthy — remove orphaned subscriptions, reopen expiring streams, reconcile against **open positions**, and guard the **under-20 logical subscription cap** per client. Helps avoid `WS_SUBSCRIPTION_LIMIT_REACHED`; **never** unsubscribes alpha.
|
|
101
101
|
|
|
102
|
-
**Tools:** `solana_positions`, `solana_bitquery_subscriptions`, `solana_bitquery_unsubscribe`, `solana_bitquery_subscription_reopen`, `solana_memory_write`
|
|
102
|
+
**Tools:** `solana_runtime_status`, `solana_positions`, `solana_bitquery_subscriptions`, `solana_bitquery_subscribe`, `solana_bitquery_unsubscribe`, `solana_bitquery_subscription_reopen`, `solana_memory_write`
|
|
103
103
|
|
|
104
|
-
**Workflow:**
|
|
104
|
+
**Workflow:**
|
|
105
|
+
1. `solana_runtime_status` → local alpha + Bitquery mux snapshot (`bitqueryStream`).
|
|
106
|
+
2. `solana_positions` → open mint addresses.
|
|
107
|
+
3. `solana_bitquery_subscriptions` → orchestrator diagnostics (subscriber counts / connected websocket clients — field names mirror API response).
|
|
108
|
+
4. Unsubscribe mismatched streams → subscribe missing `realtimeTokenPricesSolana` with **`agentId` matching the cron job's agent**.
|
|
109
|
+
5. `solana_bitquery_subscription_reopen` for subs nearing TTL.
|
|
110
|
+
6. If limits persist (`WS_PER_KEY_LIMIT`, stalled counts, absurd client fan-out) log **CRITICAL** via `solana_memory_write` — leaked TCP sockets may require a **manual gateway restart** (outside agent tools).
|
|
111
|
+
7. Memory tag **`subscription_cleanup`** with before / after telemetry.
|
|
112
|
+
|
|
113
|
+
See also: `refs/ws-subscription-health.md`.
|
|
105
114
|
|
|
106
115
|
**Configuration:**
|
|
107
116
|
- Model: Haiku (mechanical — match subs to positions, unsubscribe orphans)
|
|
@@ -209,9 +218,9 @@ At start of every cron job, check whether sufficient new data exists since last
|
|
|
209
218
|
| 2 | `trust-refresh` | `0 */8 * * *` | 3 | Haiku | off | on | none |
|
|
210
219
|
| 3 | `meta-rotation` | `30 */8 * * *` | 3 | Sonnet | off | on | announce/last |
|
|
211
220
|
| 4 | `strategy-evolution` | `0 6 * * *` | 1 | Sonnet | **on** | **off** | announce/last |
|
|
212
|
-
| 5 | `subscription-cleanup` | `15 */
|
|
221
|
+
| 5 | `subscription-cleanup` | `15 */2 * * *` | ~12 | Haiku | off | on | announce/last |
|
|
213
222
|
| 6 | `daily-performance-report` | `0 4 * * *` | 1 | Sonnet | off | **off** | announce/telegram |
|
|
214
223
|
| 7 | `intelligence-lab-eval` | `0 16 * * *` | 1 | Sonnet | **on** | **off** | none |
|
|
215
224
|
| 8 | `memory-trim` | `0 3 * * *` | 1 | Haiku | off | on | none |
|
|
216
225
|
| 9 | `balance-watchdog` | `0 */2 * * *` | 12 | Haiku | off | on | announce/telegram |
|
|
217
|
-
| | **Total** | | **
|
|
226
|
+
| | **Total** | | **40** | | | | |
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
Call `solana_trade_precheck` with your intended trade parameters.
|
|
6
6
|
|
|
7
7
|
- **If `approved: false` with hard denials:** STOP. Do not trade. Journal the denial reason.
|
|
8
|
-
- **If approved with
|
|
9
|
-
- **If approved cleanly:** Proceed to execute.
|
|
8
|
+
- **If approved with clamps or sizing notes:** When precheck sets `metadata.cappedSizeSol`, use it as the executed buy size for the matching `trade_execute`. If `metadata.buyAmountAdjusted` is true (or execute returns `policySizing`), the orchestrator changed your requested `sizeSol` (buy-amount policy **hard** mode or legacy max). For buy-amount **soft** mode, you may keep your requested size but review `metadata.reasons`.
|
|
9
|
+
- **If approved cleanly:** Proceed to execute with the intended size (still reconcile with `cappedSizeSol` when the server provides one).
|
|
10
10
|
|
|
11
11
|
**Non-negotiable:** Never override hard denials. Never argue with the policy engine.
|
|
12
12
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# WebSocket + subscription health
|
|
2
|
+
|
|
3
|
+
Use this alongside `websocket-streaming.md` when diagnosing Bitquery churn, orphaned subscriptions, or `WS_PER_KEY_LIMIT` / subscription-cap errors.
|
|
4
|
+
|
|
5
|
+
## Two connection types
|
|
6
|
+
|
|
7
|
+
1. **Alpha stream** — one WebSocket carrying buffered `alpha_signal` traffic. Managed by `solana_alpha_subscribe`; the plugin watchdog reconnects automatically. Cron and subscription cleanup **must never** call `solana_alpha_unsubscribe` unless deliberately shutting alpha down.
|
|
8
|
+
|
|
9
|
+
2. **Bitquery mux** — one WebSocket from the gateway plugin carries many **logical subscriptions** (`bitquery_subscribe` / `bitquery_unsubscribe`). The orchestrator multiplexes upstream Bitquery streams. Steady state is typically **one** plugin Bitquery client + **one** alpha client (**~2 TCP connections**) to TraderClaw, not one socket per mint.
|
|
10
|
+
|
|
11
|
+
## Health signals
|
|
12
|
+
|
|
13
|
+
**Good**
|
|
14
|
+
|
|
15
|
+
- Orchestrator diagnostics (`solana_bitquery_subscriptions`): Bitquery subscriber counts aligned with **open positions** (usually one `realtimeTokenPricesSolana` per mint you hold).
|
|
16
|
+
- Active Bitquery subscriptions **below 20** per client (`OPENCLAW_WS_MAX_SUBS_PER_CLIENT` default — see `websocket-streaming.md`).
|
|
17
|
+
- Plugin `solana_runtime_status.bitqueryStream` matches intuition: sane `activeSubscriptionCount`, tokens match `solana_positions` mints.
|
|
18
|
+
|
|
19
|
+
**Bad**
|
|
20
|
+
|
|
21
|
+
- Subscriptions still listed for mints **without** an open position (heartbeat Step 7 missed or failed).
|
|
22
|
+
- `activeSubscriptionCount` **at or above 20**, or subscribe errors **`WS_SUBSCRIPTION_LIMIT_REACHED`**.
|
|
23
|
+
- **`WS_PER_KEY_LIMIT`** or very high **TCP** connection counts to `api.traderclaw.ai` (**25** API-key ceiling) despite few logical subs — indicates **orphan TCP / leaked clients** across plugin lifecycle. Unsubscribe tools alone cannot always drop leaked sockets.
|
|
24
|
+
|
|
25
|
+
## What `subscription_cleanup` does
|
|
26
|
+
|
|
27
|
+
Scheduled job **`subscription_cleanup`** (OpenClaw cron store, id `subscription-cleanup`):
|
|
28
|
+
|
|
29
|
+
- Baseline via `solana_runtime_status`, `solana_positions`, `solana_bitquery_subscriptions`.
|
|
30
|
+
- Unsubscribe Bitquery subscriptions whose token is **not** in open positions (`solana_bitquery_unsubscribe`).
|
|
31
|
+
- Subscribe **`realtimeTokenPricesSolana`** for any open CA missing coverage; pass **`agentId` matching your trading agent** (job agent id — `main` on V1 installs).
|
|
32
|
+
- Reopen nearing-expiry subscriptions (`solana_bitquery_subscription_reopen`).
|
|
33
|
+
- Writes memory tag **`subscription_cleanup`** with before/after metrics.
|
|
34
|
+
|
|
35
|
+
Escalate with **CRITICAL** in memory when limits persist after reconcile; **restart the gateway** is the corrective action for leaked TLS sockets (`systemctl --user restart openclaw-gateway`).
|
|
36
|
+
|
|
37
|
+
## Operational rule of thumb
|
|
38
|
+
|
|
39
|
+
- **Logical Bitquery subscriptions** should mirror **open positions** (typically one price stream per held mint). Keep count **below 20** (`OPENCLAW_WS_MAX_SUBS_PER_CLIENT`).
|
|
40
|
+
- **Outbound TLS sessions** from the gateway toward TraderClaw should stay near **two** — one mux for Bitquery, one for Alpha — unless you know you intentionally run multiple clients.
|
|
@@ -27,7 +27,7 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
|
|
|
27
27
|
| `solana_gateway_credentials_set` | Register gateway URL and token | When gateway is not registered (startup) |
|
|
28
28
|
| `solana_gateway_forward_probe` | Test forwarding path health | Startup sequence; when events stop arriving |
|
|
29
29
|
|
|
30
|
-
### Wallet & Capital (
|
|
30
|
+
### Wallet & Capital (13)
|
|
31
31
|
| Tool | Purpose | When to Call |
|
|
32
32
|
|---|---|---|
|
|
33
33
|
| `solana_wallets` | List all wallets | Startup; when user asks about wallets |
|
|
@@ -39,8 +39,10 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
|
|
|
39
39
|
| `solana_killswitch_status` | Check kill switch state | Step 0 every heartbeat |
|
|
40
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 |
|
|
41
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 |
|
|
42
|
-
| `trade_size_limit_get` | Read max **buy** size (SOL)
|
|
43
|
-
| `trade_size_limit_set` | Set max
|
|
42
|
+
| `trade_size_limit_get` | Read effective max **buy** size (SOL); merges legacy max with `buyAmounts.max` | Before every buy or when user asks about size limits |
|
|
43
|
+
| `trade_size_limit_set` | Set max buy SOL (also mirrors into buy-amount max + hard enforcement when off) | When user asks to change the per-order cap |
|
|
44
|
+
| `buy_amount_policy_get` | Read `buyAmountEnforcement` and fixed/min/max buy SOL | Before sizing when user may clamp or warn on amount |
|
|
45
|
+
| `buy_amount_policy_set` | Update buy amount policy (off/soft/hard + bounds) | When user configures buy sizing on the Buy Strategy card |
|
|
44
46
|
| `position_risk_management_update` | Adjust TP/SL/trailing **numbers** on an open position (same level count) | After entry when refining exits without removing levels |
|
|
45
47
|
|
|
46
48
|
### Scanning & Discovery (4)
|
|
@@ -120,9 +122,9 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
|
|
|
120
122
|
| `solana_bitquery_catalog` | Run pre-built template | Step 2: MANDATORY for FRESH tokens (first100Buyers, devHoldings); Step 2: website metadata check for tokens >0.60 |
|
|
121
123
|
| `solana_bitquery_query` | Run custom raw GraphQL | When no catalog template fits your query need |
|
|
122
124
|
| `solana_bitquery_subscribe` | Subscribe to real-time stream | Step 5: after every successful buy (realtimeTokenPricesSolana); startup for discovery streams |
|
|
123
|
-
| `solana_bitquery_unsubscribe` | Unsubscribe from stream | Step 7: after every exit; `subscription_cleanup` cron
|
|
124
|
-
| `solana_bitquery_subscriptions` | List active subscriptions | Step 1: check for buffered events; `subscription_cleanup` cron |
|
|
125
|
-
| `solana_bitquery_subscription_reopen` | Renew expiring subscription | `subscription_cleanup` cron
|
|
125
|
+
| `solana_bitquery_unsubscribe` | Unsubscribe from stream | Step 7: after every exit; `subscription_cleanup` cron (`refs/ws-subscription-health.md`) |
|
|
126
|
+
| `solana_bitquery_subscriptions` | List active subscriptions | Step 1: check for buffered events; `subscription_cleanup` cron diagnostics |
|
|
127
|
+
| `solana_bitquery_subscription_reopen` | Renew expiring subscription | `subscription_cleanup` cron (24h TTL) |
|
|
126
128
|
|
|
127
129
|
### Memory & State (13)
|
|
128
130
|
| Tool | Purpose | When to Call |
|
|
@@ -191,7 +193,7 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
|
|
|
191
193
|
### Runtime (3)
|
|
192
194
|
| Tool | Purpose | When to Call |
|
|
193
195
|
|---|---|---|
|
|
194
|
-
| `solana_runtime_status` | Plugin runtime health | Diagnostics;
|
|
196
|
+
| `solana_runtime_status` | Plugin runtime health (alpha + Bitquery mux) | Diagnostics; `subscription_cleanup` cron baseline (`refs/ws-subscription-health.md`) |
|
|
195
197
|
| `solana_agent_sessions` | List agent sessions | Diagnostics; when checking session state |
|
|
196
198
|
| `solana_classify_deployer_risk` | Deployer risk (alias) | Same as `solana_compute_deployer_risk` — use either |
|
|
197
199
|
|