solana-traderclaw 1.0.19
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 +516 -0
- package/bin/gateway-persistence-linux.mjs +275 -0
- package/bin/installer-step-engine.mjs +1422 -0
- package/bin/llm-model-preference.mjs +136 -0
- package/bin/openclaw-trader.mjs +2624 -0
- package/bin/traderclaw.cjs +13 -0
- package/config/gateway-v1.json5 +121 -0
- package/dist/chunk-3UQIQJPQ.js +144 -0
- package/dist/chunk-3YPZOXWE.js +238 -0
- package/dist/chunk-RQZVD6TH.js +361 -0
- package/dist/chunk-T4YWGIIR.js +64 -0
- package/dist/index.js +2883 -0
- package/dist/src/alpha-buffer.js +6 -0
- package/dist/src/alpha-ws.js +6 -0
- package/dist/src/http-client.js +6 -0
- package/dist/src/session-manager.js +6 -0
- package/openclaw.plugin.json +104 -0
- package/package.json +60 -0
- package/skills/solana-trader/HEARTBEAT.md +51 -0
- package/skills/solana-trader/SKILL.md +2739 -0
- package/skills/solana-trader/bitquery-schema.md +303 -0
- package/skills/solana-trader/query-catalog.md +184 -0
- package/skills/solana-trader/refs/x-credentials.md +99 -0
- package/skills/solana-trader/websocket-streaming.md +265 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2883 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlphaBuffer
|
|
3
|
+
} from "./chunk-3UQIQJPQ.js";
|
|
4
|
+
import {
|
|
5
|
+
AlphaStreamManager
|
|
6
|
+
} from "./chunk-3YPZOXWE.js";
|
|
7
|
+
import {
|
|
8
|
+
orchestratorRequest
|
|
9
|
+
} from "./chunk-T4YWGIIR.js";
|
|
10
|
+
import {
|
|
11
|
+
SessionManager
|
|
12
|
+
} from "./chunk-RQZVD6TH.js";
|
|
13
|
+
|
|
14
|
+
// index.ts
|
|
15
|
+
import { Type } from "@sinclair/typebox";
|
|
16
|
+
|
|
17
|
+
// lib/x-client.mjs
|
|
18
|
+
import { createHmac, randomBytes } from "crypto";
|
|
19
|
+
var X_API_BASE = "https://api.twitter.com/2";
|
|
20
|
+
function percentEncode(str) {
|
|
21
|
+
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
22
|
+
}
|
|
23
|
+
function generateNonce() {
|
|
24
|
+
return randomBytes(16).toString("hex");
|
|
25
|
+
}
|
|
26
|
+
function generateTimestamp() {
|
|
27
|
+
return Math.floor(Date.now() / 1e3).toString();
|
|
28
|
+
}
|
|
29
|
+
function buildBaseString(method, url, params) {
|
|
30
|
+
const sorted = Object.keys(params).sort().map((k) => `${percentEncode(k)}=${percentEncode(params[k])}`).join("&");
|
|
31
|
+
return `${method.toUpperCase()}&${percentEncode(url)}&${percentEncode(sorted)}`;
|
|
32
|
+
}
|
|
33
|
+
function signRequest(method, url, oauthParams, consumerSecret, tokenSecret) {
|
|
34
|
+
const baseString = buildBaseString(method, url, oauthParams);
|
|
35
|
+
const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;
|
|
36
|
+
return createHmac("sha1", signingKey).update(baseString).digest("base64");
|
|
37
|
+
}
|
|
38
|
+
function buildAuthHeader(method, url, credentials, queryParams = {}) {
|
|
39
|
+
const oauthParams = {
|
|
40
|
+
oauth_consumer_key: credentials.consumerKey,
|
|
41
|
+
oauth_nonce: generateNonce(),
|
|
42
|
+
oauth_signature_method: "HMAC-SHA1",
|
|
43
|
+
oauth_timestamp: generateTimestamp(),
|
|
44
|
+
oauth_token: credentials.accessToken,
|
|
45
|
+
oauth_version: "1.0",
|
|
46
|
+
...queryParams
|
|
47
|
+
};
|
|
48
|
+
const signature = signRequest(
|
|
49
|
+
method,
|
|
50
|
+
url,
|
|
51
|
+
oauthParams,
|
|
52
|
+
credentials.consumerSecret,
|
|
53
|
+
credentials.accessTokenSecret
|
|
54
|
+
);
|
|
55
|
+
oauthParams.oauth_signature = signature;
|
|
56
|
+
const headerParts = Object.keys(oauthParams).filter((k) => k.startsWith("oauth_")).sort().map((k) => `${percentEncode(k)}="${percentEncode(oauthParams[k])}"`).join(", ");
|
|
57
|
+
return `OAuth ${headerParts}`;
|
|
58
|
+
}
|
|
59
|
+
async function xApiFetch(method, endpoint, credentials, { body = null, queryParams = {} } = {}) {
|
|
60
|
+
const url = `${X_API_BASE}${endpoint}`;
|
|
61
|
+
const queryString = Object.keys(queryParams).length > 0 ? "?" + Object.entries(queryParams).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&") : "";
|
|
62
|
+
const fullUrl = `${url}${queryString}`;
|
|
63
|
+
const authHeader = buildAuthHeader(method, url, credentials, method === "GET" ? queryParams : {});
|
|
64
|
+
const headers = {
|
|
65
|
+
Authorization: authHeader,
|
|
66
|
+
"User-Agent": "TraderClaw-Team/1.0"
|
|
67
|
+
};
|
|
68
|
+
const fetchOpts = { method, headers };
|
|
69
|
+
if (body && method !== "GET") {
|
|
70
|
+
headers["Content-Type"] = "application/json";
|
|
71
|
+
fetchOpts.body = JSON.stringify(body);
|
|
72
|
+
}
|
|
73
|
+
const response = await fetch(fullUrl, fetchOpts);
|
|
74
|
+
const responseText = await response.text();
|
|
75
|
+
let responseData;
|
|
76
|
+
try {
|
|
77
|
+
responseData = JSON.parse(responseText);
|
|
78
|
+
} catch {
|
|
79
|
+
responseData = { raw: responseText };
|
|
80
|
+
}
|
|
81
|
+
const rateLimitRemaining = response.headers.get("x-rate-limit-remaining");
|
|
82
|
+
const rateLimitReset = response.headers.get("x-rate-limit-reset");
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
if (response.status === 429) {
|
|
85
|
+
const resetTime = rateLimitReset ? new Date(parseInt(rateLimitReset) * 1e3).toISOString() : "unknown";
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: `Rate limited. Resets at ${resetTime}.`,
|
|
89
|
+
status: 429,
|
|
90
|
+
resetAt: resetTime,
|
|
91
|
+
data: responseData
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (response.status === 401) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: "Authentication failed. Check your X API credentials (consumer key/secret and access token/secret). Ensure the app has Read+Write permissions and tokens were regenerated after permission change.",
|
|
98
|
+
status: 401,
|
|
99
|
+
data: responseData
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (response.status === 403) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: "Forbidden. Your X API tier may not support this endpoint. Free tier is write-only (posting). Upgrade to Basic ($200/mo) or pay-as-you-go for read access (mentions, search).",
|
|
106
|
+
status: 403,
|
|
107
|
+
data: responseData
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: `X API error ${response.status}: ${responseData?.detail || responseData?.title || responseText.slice(0, 200)}`,
|
|
113
|
+
status: response.status,
|
|
114
|
+
data: responseData
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
status: response.status,
|
|
120
|
+
data: responseData,
|
|
121
|
+
rateLimitRemaining: rateLimitRemaining ? parseInt(rateLimitRemaining) : void 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function readMentions(credentials, { maxResults = 10, sinceId = null, paginationToken = null } = {}) {
|
|
125
|
+
if (!credentials.userId) {
|
|
126
|
+
return { ok: false, error: "userId is required to read mentions. Set it in the agent's X profile config." };
|
|
127
|
+
}
|
|
128
|
+
const queryParams = {
|
|
129
|
+
max_results: String(Math.min(Math.max(maxResults, 5), 100)),
|
|
130
|
+
"tweet.fields": "created_at,author_id,conversation_id,in_reply_to_user_id,text",
|
|
131
|
+
expansions: "author_id",
|
|
132
|
+
"user.fields": "username,name"
|
|
133
|
+
};
|
|
134
|
+
if (sinceId) queryParams.since_id = sinceId;
|
|
135
|
+
if (paginationToken) queryParams.pagination_token = paginationToken;
|
|
136
|
+
const result = await xApiFetch("GET", `/users/${credentials.userId}/mentions`, credentials, { queryParams });
|
|
137
|
+
if (result.ok) {
|
|
138
|
+
const tweets = result.data?.data || [];
|
|
139
|
+
const users = result.data?.includes?.users || [];
|
|
140
|
+
const userMap = Object.fromEntries(users.map((u) => [u.id, u]));
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
mentions: tweets.map((t) => ({
|
|
144
|
+
id: t.id,
|
|
145
|
+
text: t.text,
|
|
146
|
+
authorId: t.author_id,
|
|
147
|
+
authorUsername: userMap[t.author_id]?.username || "unknown",
|
|
148
|
+
authorName: userMap[t.author_id]?.name || "unknown",
|
|
149
|
+
createdAt: t.created_at,
|
|
150
|
+
conversationId: t.conversation_id
|
|
151
|
+
})),
|
|
152
|
+
nextToken: result.data?.meta?.next_token || null,
|
|
153
|
+
resultCount: result.data?.meta?.result_count || 0
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
async function searchTweets(credentials, query, { maxResults = 10, sinceId = null, paginationToken = null } = {}) {
|
|
159
|
+
if (!query || typeof query !== "string" || query.trim().length === 0) {
|
|
160
|
+
return { ok: false, error: "Search query is required." };
|
|
161
|
+
}
|
|
162
|
+
const queryParams = {
|
|
163
|
+
query: query.trim(),
|
|
164
|
+
max_results: String(Math.min(Math.max(maxResults, 10), 100)),
|
|
165
|
+
"tweet.fields": "created_at,author_id,public_metrics,conversation_id,text",
|
|
166
|
+
expansions: "author_id",
|
|
167
|
+
"user.fields": "username,name,public_metrics"
|
|
168
|
+
};
|
|
169
|
+
if (sinceId) queryParams.since_id = sinceId;
|
|
170
|
+
if (paginationToken) queryParams.next_token = paginationToken;
|
|
171
|
+
const result = await xApiFetch("GET", "/tweets/search/recent", credentials, { queryParams });
|
|
172
|
+
if (result.ok) {
|
|
173
|
+
const tweets = result.data?.data || [];
|
|
174
|
+
const users = result.data?.includes?.users || [];
|
|
175
|
+
const userMap = Object.fromEntries(users.map((u) => [u.id, u]));
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
tweets: tweets.map((t) => ({
|
|
179
|
+
id: t.id,
|
|
180
|
+
text: t.text,
|
|
181
|
+
authorId: t.author_id,
|
|
182
|
+
authorUsername: userMap[t.author_id]?.username || "unknown",
|
|
183
|
+
authorName: userMap[t.author_id]?.name || "unknown",
|
|
184
|
+
createdAt: t.created_at,
|
|
185
|
+
metrics: t.public_metrics || {},
|
|
186
|
+
conversationId: t.conversation_id
|
|
187
|
+
})),
|
|
188
|
+
nextToken: result.data?.meta?.next_token || null,
|
|
189
|
+
resultCount: result.data?.meta?.result_count || 0
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
async function getThread(credentials, tweetId, { maxResults = 20 } = {}) {
|
|
195
|
+
if (!tweetId || typeof tweetId !== "string") {
|
|
196
|
+
return { ok: false, error: "tweetId is required to get a thread." };
|
|
197
|
+
}
|
|
198
|
+
const queryParams = {
|
|
199
|
+
query: `conversation_id:${tweetId}`,
|
|
200
|
+
max_results: String(Math.min(Math.max(maxResults, 10), 100)),
|
|
201
|
+
"tweet.fields": "created_at,author_id,in_reply_to_user_id,conversation_id,text,public_metrics",
|
|
202
|
+
expansions: "author_id",
|
|
203
|
+
"user.fields": "username,name"
|
|
204
|
+
};
|
|
205
|
+
const result = await xApiFetch("GET", "/tweets/search/recent", credentials, { queryParams });
|
|
206
|
+
if (result.ok) {
|
|
207
|
+
const tweets = result.data?.data || [];
|
|
208
|
+
const users = result.data?.includes?.users || [];
|
|
209
|
+
const userMap = Object.fromEntries(users.map((u) => [u.id, u]));
|
|
210
|
+
return {
|
|
211
|
+
ok: true,
|
|
212
|
+
conversationId: tweetId,
|
|
213
|
+
replies: tweets.map((t) => ({
|
|
214
|
+
id: t.id,
|
|
215
|
+
text: t.text,
|
|
216
|
+
authorId: t.author_id,
|
|
217
|
+
authorUsername: userMap[t.author_id]?.username || "unknown",
|
|
218
|
+
createdAt: t.created_at,
|
|
219
|
+
metrics: t.public_metrics || {}
|
|
220
|
+
})),
|
|
221
|
+
resultCount: tweets.length
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// lib/x-tools.mjs
|
|
228
|
+
function parseXConfig(obj) {
|
|
229
|
+
const xRaw = obj?.x && typeof obj.x === "object" && !Array.isArray(obj.x) ? obj.x : {};
|
|
230
|
+
const consumerKey = typeof xRaw.consumerKey === "string" ? xRaw.consumerKey : process.env.X_CONSUMER_KEY || "";
|
|
231
|
+
const consumerSecret = typeof xRaw.consumerSecret === "string" ? xRaw.consumerSecret : process.env.X_CONSUMER_SECRET || "";
|
|
232
|
+
const profiles = {};
|
|
233
|
+
if (xRaw.profiles && typeof xRaw.profiles === "object" && !Array.isArray(xRaw.profiles)) {
|
|
234
|
+
for (const [key, val] of Object.entries(xRaw.profiles)) {
|
|
235
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
236
|
+
const envPrefix2 = `X_ACCESS_TOKEN_${key.toUpperCase().replace(/-/g, "_")}`;
|
|
237
|
+
const accessToken = typeof val.accessToken === "string" ? val.accessToken : process.env[envPrefix2] || "";
|
|
238
|
+
const accessTokenSecret = typeof val.accessTokenSecret === "string" ? val.accessTokenSecret : process.env[`${envPrefix2}_SECRET`] || "";
|
|
239
|
+
if (accessToken && accessTokenSecret) {
|
|
240
|
+
profiles[key] = {
|
|
241
|
+
accessToken,
|
|
242
|
+
accessTokenSecret,
|
|
243
|
+
userId: typeof val.userId === "string" ? val.userId : void 0,
|
|
244
|
+
username: typeof val.username === "string" ? val.username : void 0
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const defaultAgentId = typeof obj?.agentId === "string" ? obj.agentId : "main";
|
|
251
|
+
const envPrefix = `X_ACCESS_TOKEN_${defaultAgentId.toUpperCase().replace(/-/g, "_")}`;
|
|
252
|
+
const envAccessToken = process.env[envPrefix] || "";
|
|
253
|
+
const envAccessTokenSecret = process.env[`${envPrefix}_SECRET`] || "";
|
|
254
|
+
if (!profiles[defaultAgentId] && envAccessToken && envAccessTokenSecret) {
|
|
255
|
+
profiles[defaultAgentId] = { accessToken: envAccessToken, accessTokenSecret: envAccessTokenSecret };
|
|
256
|
+
}
|
|
257
|
+
if (consumerKey && consumerSecret) {
|
|
258
|
+
return { ok: true, consumerKey, consumerSecret, profiles };
|
|
259
|
+
}
|
|
260
|
+
return { ok: false, consumerKey: "", consumerSecret: "", profiles: {} };
|
|
261
|
+
}
|
|
262
|
+
function resolveAgentCredentials(xConfig, callerAgentId, requestedAgentId, fallbackAgentId) {
|
|
263
|
+
const agentId = callerAgentId || requestedAgentId || fallbackAgentId || "main";
|
|
264
|
+
if (!xConfig || !xConfig.ok) {
|
|
265
|
+
return { ok: false, error: "X/Twitter configuration missing. Set 'x.consumerKey', 'x.consumerSecret', and 'x.profiles.<agentId>' in plugin config, or use X_CONSUMER_KEY / X_CONSUMER_SECRET env vars." };
|
|
266
|
+
}
|
|
267
|
+
const profile = xConfig.profiles[agentId];
|
|
268
|
+
if (!profile) {
|
|
269
|
+
return { ok: false, error: `No X profile configured for agent '${agentId}'. Available profiles: ${Object.keys(xConfig.profiles).join(", ") || "none"}` };
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
ok: true,
|
|
273
|
+
agentId,
|
|
274
|
+
credentials: {
|
|
275
|
+
consumerKey: xConfig.consumerKey,
|
|
276
|
+
consumerSecret: xConfig.consumerSecret,
|
|
277
|
+
accessToken: profile.accessToken,
|
|
278
|
+
accessTokenSecret: profile.accessTokenSecret,
|
|
279
|
+
userId: profile.userId || null,
|
|
280
|
+
username: profile.username || agentId
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function registerXReadTools(api, Type2, xConfig, fallbackAgentId, logPrefix, options) {
|
|
285
|
+
const checkPermission = options?.checkPermission || null;
|
|
286
|
+
const json = (data) => ({
|
|
287
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
288
|
+
});
|
|
289
|
+
const wrapExecute = (toolName, fn) => async (toolCallId, params) => {
|
|
290
|
+
try {
|
|
291
|
+
if (checkPermission) {
|
|
292
|
+
const callingAgentId = params?._agentId || fallbackAgentId;
|
|
293
|
+
const permError = checkPermission(toolName, callingAgentId);
|
|
294
|
+
if (permError) {
|
|
295
|
+
return json({ error: permError, tool: toolName, agentId: callingAgentId });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const result = await fn(toolCallId, params ?? {});
|
|
299
|
+
return json(result);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
api.registerTool({
|
|
305
|
+
name: "x_search_tweets",
|
|
306
|
+
description: "Search recent tweets on X/Twitter by keyword, hashtag, cashtag, or advanced query. Use for social research: token sentiment, narrative detection, influencer monitoring, KOL tracking. Requires pay-as-you-go or Basic tier X API access.",
|
|
307
|
+
parameters: Type2.Object({
|
|
308
|
+
query: Type2.String({ description: "Search query. Examples: '$BONK sentiment', 'solana memecoin', 'from:elonmusk crypto', '#SOL', 'conversation_id:123'" }),
|
|
309
|
+
maxResults: Type2.Optional(Type2.Number({ description: "Number of results (10-100, default: 10)" })),
|
|
310
|
+
sinceId: Type2.Optional(Type2.String({ description: "Only return tweets newer than this tweet ID" })),
|
|
311
|
+
paginationToken: Type2.Optional(Type2.String({ description: "Pagination token from a previous response" })),
|
|
312
|
+
agentId: Type2.Optional(Type2.String({ description: "Override agent ID (default: caller's agent identity)" }))
|
|
313
|
+
}),
|
|
314
|
+
execute: wrapExecute("x_search_tweets", async (_id, params) => {
|
|
315
|
+
const callerAgentId = params._agentId;
|
|
316
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
317
|
+
if (!creds.ok) return { error: creds.error };
|
|
318
|
+
return searchTweets(creds.credentials, params.query, {
|
|
319
|
+
maxResults: params.maxResults,
|
|
320
|
+
sinceId: params.sinceId,
|
|
321
|
+
paginationToken: params.paginationToken
|
|
322
|
+
});
|
|
323
|
+
})
|
|
324
|
+
});
|
|
325
|
+
api.registerTool({
|
|
326
|
+
name: "x_read_mentions",
|
|
327
|
+
description: "Read recent @mentions of the configured X profile. Use for monitoring community reactions and engagement. Requires pay-as-you-go or Basic tier X API access.",
|
|
328
|
+
parameters: Type2.Object({
|
|
329
|
+
maxResults: Type2.Optional(Type2.Number({ description: "Number of mentions to return (5-100, default: 10)" })),
|
|
330
|
+
sinceId: Type2.Optional(Type2.String({ description: "Only return mentions newer than this tweet ID" })),
|
|
331
|
+
paginationToken: Type2.Optional(Type2.String({ description: "Pagination token from a previous response" })),
|
|
332
|
+
agentId: Type2.Optional(Type2.String({ description: "Override agent ID (default: caller's agent identity)" }))
|
|
333
|
+
}),
|
|
334
|
+
execute: wrapExecute("x_read_mentions", async (_id, params) => {
|
|
335
|
+
const callerAgentId = params._agentId;
|
|
336
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
337
|
+
if (!creds.ok) return { error: creds.error };
|
|
338
|
+
return readMentions(creds.credentials, {
|
|
339
|
+
maxResults: params.maxResults,
|
|
340
|
+
sinceId: params.sinceId,
|
|
341
|
+
paginationToken: params.paginationToken
|
|
342
|
+
});
|
|
343
|
+
})
|
|
344
|
+
});
|
|
345
|
+
api.registerTool({
|
|
346
|
+
name: "x_get_thread",
|
|
347
|
+
description: "Read a full conversation thread on X/Twitter by tweet ID. Use for understanding context around viral posts or influencer discussions. Requires pay-as-you-go or Basic tier X API access.",
|
|
348
|
+
parameters: Type2.Object({
|
|
349
|
+
tweetId: Type2.String({ description: "The tweet ID to get the conversation thread for" }),
|
|
350
|
+
maxResults: Type2.Optional(Type2.Number({ description: "Max replies to return (10-100, default: 20)" })),
|
|
351
|
+
agentId: Type2.Optional(Type2.String({ description: "Override agent ID (default: caller's agent identity)" }))
|
|
352
|
+
}),
|
|
353
|
+
execute: wrapExecute("x_get_thread", async (_id, params) => {
|
|
354
|
+
const callerAgentId = params._agentId;
|
|
355
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
356
|
+
if (!creds.ok) return { error: creds.error };
|
|
357
|
+
return getThread(creds.credentials, params.tweetId, {
|
|
358
|
+
maxResults: params.maxResults
|
|
359
|
+
});
|
|
360
|
+
})
|
|
361
|
+
});
|
|
362
|
+
api.logger.info(`${logPrefix} Registered 3 X/Twitter read tools (search, mentions, threads). Profiles: ${xConfig.ok ? Object.keys(xConfig.profiles).join(", ") || "none" : "unconfigured"}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// lib/web-fetch.mjs
|
|
366
|
+
import { lookup } from "node:dns/promises";
|
|
367
|
+
var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
|
|
368
|
+
"localhost",
|
|
369
|
+
"127.0.0.1",
|
|
370
|
+
"0.0.0.0",
|
|
371
|
+
"::1",
|
|
372
|
+
"metadata.google.internal",
|
|
373
|
+
"169.254.169.254"
|
|
374
|
+
]);
|
|
375
|
+
var MAX_BODY_BYTES = 512 * 1024;
|
|
376
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
377
|
+
var MAX_OUTPUT_CHARS = 8e3;
|
|
378
|
+
function isPrivateIp(ip) {
|
|
379
|
+
if (ip === "127.0.0.1" || ip === "::1" || ip === "0.0.0.0") return true;
|
|
380
|
+
if (ip.startsWith("10.")) return true;
|
|
381
|
+
if (ip.startsWith("192.168.")) return true;
|
|
382
|
+
if (ip.startsWith("169.254.")) return true;
|
|
383
|
+
if (ip.startsWith("fc") || ip.startsWith("fd")) return true;
|
|
384
|
+
if (ip.startsWith("fe80")) return true;
|
|
385
|
+
const parts = ip.split(".");
|
|
386
|
+
if (parts.length === 4 && parts[0] === "172") {
|
|
387
|
+
const second = parseInt(parts[1], 10);
|
|
388
|
+
if (second >= 16 && second <= 31) return true;
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
async function isBlockedUrl(urlStr) {
|
|
393
|
+
let parsed;
|
|
394
|
+
try {
|
|
395
|
+
parsed = new URL(urlStr);
|
|
396
|
+
} catch {
|
|
397
|
+
return { blocked: true, reason: "Invalid URL" };
|
|
398
|
+
}
|
|
399
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
400
|
+
return { blocked: true, reason: `Blocked scheme: ${parsed.protocol}` };
|
|
401
|
+
}
|
|
402
|
+
const host = parsed.hostname.toLowerCase();
|
|
403
|
+
if (BLOCKED_HOSTS.has(host)) {
|
|
404
|
+
return { blocked: true, reason: `Blocked host: ${host}` };
|
|
405
|
+
}
|
|
406
|
+
if (isPrivateIp(host)) {
|
|
407
|
+
return { blocked: true, reason: `Blocked private IP: ${host}` };
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const resolved = await lookup(host);
|
|
411
|
+
if (isPrivateIp(resolved.address)) {
|
|
412
|
+
return { blocked: true, reason: `Host ${host} resolves to private IP ${resolved.address}` };
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
return { blocked: true, reason: `DNS resolution failed for ${host}` };
|
|
416
|
+
}
|
|
417
|
+
return { blocked: false };
|
|
418
|
+
}
|
|
419
|
+
function stripHtml(html) {
|
|
420
|
+
let text = html;
|
|
421
|
+
text = text.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
422
|
+
text = text.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
423
|
+
text = text.replace(/<nav[\s\S]*?<\/nav>/gi, "");
|
|
424
|
+
text = text.replace(/<footer[\s\S]*?<\/footer>/gi, "");
|
|
425
|
+
text = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
426
|
+
text = text.replace(/<[^>]+>/g, " ");
|
|
427
|
+
text = text.replace(/ /gi, " ");
|
|
428
|
+
text = text.replace(/&/gi, "&");
|
|
429
|
+
text = text.replace(/</gi, "<");
|
|
430
|
+
text = text.replace(/>/gi, ">");
|
|
431
|
+
text = text.replace(/"/gi, '"');
|
|
432
|
+
text = text.replace(/'/gi, "'");
|
|
433
|
+
text = text.replace(/\s+/g, " ");
|
|
434
|
+
return text.trim();
|
|
435
|
+
}
|
|
436
|
+
function extractTitle(html) {
|
|
437
|
+
const m = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
438
|
+
return m ? m[1].replace(/<[^>]+>/g, "").trim() : null;
|
|
439
|
+
}
|
|
440
|
+
function extractMetaDescription(html) {
|
|
441
|
+
const m = html.match(/<meta[^>]+name\s*=\s*["']description["'][^>]+content\s*=\s*["']([\s\S]*?)["'][^>]*>/i) || html.match(/<meta[^>]+content\s*=\s*["']([\s\S]*?)["'][^>]+name\s*=\s*["']description["'][^>]*>/i);
|
|
442
|
+
return m ? m[1].trim() : null;
|
|
443
|
+
}
|
|
444
|
+
function extractHeadings(html) {
|
|
445
|
+
const headings = [];
|
|
446
|
+
const regex = /<h([1-3])[^>]*>([\s\S]*?)<\/h\1>/gi;
|
|
447
|
+
let match;
|
|
448
|
+
while ((match = regex.exec(html)) !== null && headings.length < 20) {
|
|
449
|
+
const text = match[2].replace(/<[^>]+>/g, "").trim();
|
|
450
|
+
if (text) headings.push({ level: parseInt(match[1], 10), text });
|
|
451
|
+
}
|
|
452
|
+
return headings;
|
|
453
|
+
}
|
|
454
|
+
function extractLinks(html, baseUrl) {
|
|
455
|
+
const links = [];
|
|
456
|
+
const seen = /* @__PURE__ */ new Set();
|
|
457
|
+
const regex = /<a[^>]+href\s*=\s*["']([^"'#]+?)["'][^>]*>/gi;
|
|
458
|
+
let match;
|
|
459
|
+
while ((match = regex.exec(html)) !== null && links.length < 30) {
|
|
460
|
+
let href = match[1].trim();
|
|
461
|
+
if (href.startsWith("mailto:") || href.startsWith("javascript:") || href.startsWith("tel:")) continue;
|
|
462
|
+
try {
|
|
463
|
+
const resolved = new URL(href, baseUrl).href;
|
|
464
|
+
if (!seen.has(resolved)) {
|
|
465
|
+
seen.add(resolved);
|
|
466
|
+
links.push(resolved);
|
|
467
|
+
}
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return links;
|
|
472
|
+
}
|
|
473
|
+
function extractSocialLinks(links) {
|
|
474
|
+
const social = {};
|
|
475
|
+
for (const link of links) {
|
|
476
|
+
const lower = link.toLowerCase();
|
|
477
|
+
if (lower.includes("twitter.com/") || lower.includes("x.com/")) {
|
|
478
|
+
if (!social.twitter) social.twitter = link;
|
|
479
|
+
} else if (lower.includes("t.me/") || lower.includes("telegram.")) {
|
|
480
|
+
if (!social.telegram) social.telegram = link;
|
|
481
|
+
} else if (lower.includes("discord.gg/") || lower.includes("discord.com/")) {
|
|
482
|
+
if (!social.discord) social.discord = link;
|
|
483
|
+
} else if (lower.includes("github.com/")) {
|
|
484
|
+
if (!social.github) social.github = link;
|
|
485
|
+
} else if (lower.includes("medium.com/") || lower.includes(".medium.com")) {
|
|
486
|
+
if (!social.medium) social.medium = link;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return social;
|
|
490
|
+
}
|
|
491
|
+
async function readBodyWithLimit(response, maxBytes) {
|
|
492
|
+
const reader = response.body?.getReader();
|
|
493
|
+
if (!reader) {
|
|
494
|
+
const buf = await response.arrayBuffer();
|
|
495
|
+
if (buf.byteLength > maxBytes) throw new Error(`Response too large: ${buf.byteLength} bytes (max ${maxBytes})`);
|
|
496
|
+
return new Uint8Array(buf);
|
|
497
|
+
}
|
|
498
|
+
const chunks = [];
|
|
499
|
+
let totalBytes = 0;
|
|
500
|
+
while (true) {
|
|
501
|
+
const { done, value } = await reader.read();
|
|
502
|
+
if (done) break;
|
|
503
|
+
totalBytes += value.byteLength;
|
|
504
|
+
if (totalBytes > maxBytes) {
|
|
505
|
+
reader.cancel();
|
|
506
|
+
throw new Error(`Response too large: exceeded ${maxBytes} bytes`);
|
|
507
|
+
}
|
|
508
|
+
chunks.push(value);
|
|
509
|
+
}
|
|
510
|
+
const result = new Uint8Array(totalBytes);
|
|
511
|
+
let offset = 0;
|
|
512
|
+
for (const chunk of chunks) {
|
|
513
|
+
result.set(chunk, offset);
|
|
514
|
+
offset += chunk.byteLength;
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
async function fetchUrl(url) {
|
|
519
|
+
const blockCheck = await isBlockedUrl(url);
|
|
520
|
+
if (blockCheck.blocked) {
|
|
521
|
+
return { ok: false, error: blockCheck.reason, url };
|
|
522
|
+
}
|
|
523
|
+
const controller = new AbortController();
|
|
524
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
525
|
+
try {
|
|
526
|
+
const response = await fetch(url, {
|
|
527
|
+
signal: controller.signal,
|
|
528
|
+
headers: {
|
|
529
|
+
"User-Agent": "Mozilla/5.0 (compatible; TraderClaw/1.0; +https://traderclaw.com)",
|
|
530
|
+
"Accept": "text/html,application/xhtml+xml,application/json,text/plain,*/*",
|
|
531
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
532
|
+
},
|
|
533
|
+
redirect: "follow"
|
|
534
|
+
});
|
|
535
|
+
clearTimeout(timeout);
|
|
536
|
+
if (!response.ok) {
|
|
537
|
+
return { ok: false, error: `HTTP ${response.status} ${response.statusText}`, url };
|
|
538
|
+
}
|
|
539
|
+
const finalUrl = response.url;
|
|
540
|
+
const finalCheck = await isBlockedUrl(finalUrl);
|
|
541
|
+
if (finalCheck.blocked) {
|
|
542
|
+
return { ok: false, error: `Redirect to blocked URL: ${finalCheck.reason}`, url };
|
|
543
|
+
}
|
|
544
|
+
const contentType = response.headers.get("content-type") || "";
|
|
545
|
+
const isJson = contentType.includes("application/json");
|
|
546
|
+
const isHtml = contentType.includes("text/html") || contentType.includes("text/xhtml");
|
|
547
|
+
const isText = contentType.includes("text/plain");
|
|
548
|
+
let bodyBytes;
|
|
549
|
+
try {
|
|
550
|
+
bodyBytes = await readBodyWithLimit(response, MAX_BODY_BYTES);
|
|
551
|
+
} catch (sizeErr) {
|
|
552
|
+
return { ok: false, error: sizeErr.message, url };
|
|
553
|
+
}
|
|
554
|
+
const raw = new TextDecoder("utf-8", { fatal: false }).decode(bodyBytes);
|
|
555
|
+
if (isJson) {
|
|
556
|
+
let parsed;
|
|
557
|
+
try {
|
|
558
|
+
parsed = JSON.parse(raw);
|
|
559
|
+
} catch {
|
|
560
|
+
parsed = null;
|
|
561
|
+
}
|
|
562
|
+
const jsonStr = parsed ? JSON.stringify(parsed, null, 2) : raw;
|
|
563
|
+
return {
|
|
564
|
+
ok: true,
|
|
565
|
+
url,
|
|
566
|
+
finalUrl,
|
|
567
|
+
contentType: "json",
|
|
568
|
+
title: null,
|
|
569
|
+
metaDescription: null,
|
|
570
|
+
headings: [],
|
|
571
|
+
socialLinks: {},
|
|
572
|
+
outboundLinks: [],
|
|
573
|
+
bodyText: jsonStr.slice(0, MAX_OUTPUT_CHARS),
|
|
574
|
+
bodyTruncated: jsonStr.length > MAX_OUTPUT_CHARS
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
if (isHtml || !isText && !isJson) {
|
|
578
|
+
const title = extractTitle(raw);
|
|
579
|
+
const metaDescription = extractMetaDescription(raw);
|
|
580
|
+
const headings = extractHeadings(raw);
|
|
581
|
+
const allLinks = extractLinks(raw, finalUrl);
|
|
582
|
+
const socialLinks = extractSocialLinks(allLinks);
|
|
583
|
+
const bodyText = stripHtml(raw).slice(0, MAX_OUTPUT_CHARS);
|
|
584
|
+
return {
|
|
585
|
+
ok: true,
|
|
586
|
+
url,
|
|
587
|
+
finalUrl,
|
|
588
|
+
contentType: "html",
|
|
589
|
+
title,
|
|
590
|
+
metaDescription,
|
|
591
|
+
headings,
|
|
592
|
+
socialLinks,
|
|
593
|
+
outboundLinks: allLinks.slice(0, 20),
|
|
594
|
+
bodyText,
|
|
595
|
+
bodyTruncated: stripHtml(raw).length > MAX_OUTPUT_CHARS
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
ok: true,
|
|
600
|
+
url,
|
|
601
|
+
finalUrl,
|
|
602
|
+
contentType: "text",
|
|
603
|
+
title: null,
|
|
604
|
+
metaDescription: null,
|
|
605
|
+
headings: [],
|
|
606
|
+
socialLinks: {},
|
|
607
|
+
outboundLinks: [],
|
|
608
|
+
bodyText: raw.slice(0, MAX_OUTPUT_CHARS),
|
|
609
|
+
bodyTruncated: raw.length > MAX_OUTPUT_CHARS
|
|
610
|
+
};
|
|
611
|
+
} catch (err) {
|
|
612
|
+
clearTimeout(timeout);
|
|
613
|
+
if (err.name === "AbortError") {
|
|
614
|
+
return { ok: false, error: `Request timed out after ${FETCH_TIMEOUT_MS}ms`, url };
|
|
615
|
+
}
|
|
616
|
+
return { ok: false, error: err.message || String(err), url };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function registerWebFetchTool(api, Type2, logPrefix, options) {
|
|
620
|
+
const checkPermission = options?.checkPermission || null;
|
|
621
|
+
const json = (data) => ({
|
|
622
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
623
|
+
});
|
|
624
|
+
api.registerTool({
|
|
625
|
+
name: "web_fetch_url",
|
|
626
|
+
description: "Fetch a URL and return its content as structured text. Extracts title, meta description, headings, social links, outbound links, and body text from HTML pages. Returns raw JSON for JSON responses. Use for analyzing token project websites, metadata URIs, and verifying social link legitimacy. Results should be cached in memory \u2014 do not re-fetch the same URL within 48 hours.",
|
|
627
|
+
parameters: Type2.Object({
|
|
628
|
+
url: Type2.String({ description: "The URL to fetch (must be http:// or https://)" })
|
|
629
|
+
}),
|
|
630
|
+
execute: async (toolCallId, params) => {
|
|
631
|
+
try {
|
|
632
|
+
if (checkPermission) {
|
|
633
|
+
const callingAgentId = params?._agentId || "main";
|
|
634
|
+
const permError = checkPermission("web_fetch_url", callingAgentId);
|
|
635
|
+
if (permError) {
|
|
636
|
+
return json({ error: permError, tool: "web_fetch_url", agentId: callingAgentId });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const { url } = params;
|
|
640
|
+
api.logger.info(`${logPrefix} web_fetch_url: fetching ${url}`);
|
|
641
|
+
const result = await fetchUrl(url);
|
|
642
|
+
if (!result.ok) {
|
|
643
|
+
api.logger.warn(`${logPrefix} web_fetch_url failed for ${url}: ${result.error}`);
|
|
644
|
+
return json({ ok: false, error: result.error, url });
|
|
645
|
+
}
|
|
646
|
+
api.logger.info(`${logPrefix} web_fetch_url: success for ${url} (${result.contentType}, title: ${result.title || "none"})`);
|
|
647
|
+
return json(result);
|
|
648
|
+
} catch (err) {
|
|
649
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
api.logger.info(`${logPrefix} Registered web_fetch_url tool (website analysis, metadata URI inspection)`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// index.ts
|
|
657
|
+
import * as fs from "fs";
|
|
658
|
+
import * as path from "path";
|
|
659
|
+
import { homedir } from "os";
|
|
660
|
+
function parseConfig(raw) {
|
|
661
|
+
const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
662
|
+
const orchestratorUrl = typeof obj.orchestratorUrl === "string" ? obj.orchestratorUrl : "";
|
|
663
|
+
const walletId = typeof obj.walletId === "string" ? obj.walletId : typeof obj.walletId === "number" ? String(obj.walletId) : "";
|
|
664
|
+
const apiKey = typeof obj.apiKey === "string" ? obj.apiKey : "";
|
|
665
|
+
const externalUserId = typeof obj.externalUserId === "string" ? obj.externalUserId : void 0;
|
|
666
|
+
const refreshToken = typeof obj.refreshToken === "string" ? obj.refreshToken : void 0;
|
|
667
|
+
const walletPublicKey = typeof obj.walletPublicKey === "string" ? obj.walletPublicKey : void 0;
|
|
668
|
+
const apiTimeout = typeof obj.apiTimeout === "number" ? obj.apiTimeout : 12e4;
|
|
669
|
+
const agentId = typeof obj.agentId === "string" ? obj.agentId : void 0;
|
|
670
|
+
const gatewayBaseUrl = typeof obj.gatewayBaseUrl === "string" ? obj.gatewayBaseUrl : void 0;
|
|
671
|
+
const gatewayToken = typeof obj.gatewayToken === "string" ? obj.gatewayToken : void 0;
|
|
672
|
+
const dataDir = typeof obj.dataDir === "string" ? obj.dataDir : void 0;
|
|
673
|
+
const xConfig = parseXConfig(obj);
|
|
674
|
+
return {
|
|
675
|
+
orchestratorUrl,
|
|
676
|
+
walletId,
|
|
677
|
+
apiKey,
|
|
678
|
+
externalUserId,
|
|
679
|
+
refreshToken,
|
|
680
|
+
walletPublicKey,
|
|
681
|
+
apiTimeout,
|
|
682
|
+
agentId,
|
|
683
|
+
gatewayBaseUrl,
|
|
684
|
+
gatewayToken,
|
|
685
|
+
dataDir,
|
|
686
|
+
xConfig
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function buildTraderClawWelcomeMessage(apiKeyForDisplay) {
|
|
690
|
+
const keyBlock = apiKeyForDisplay ? `Your TraderClaw API Key:
|
|
691
|
+
|
|
692
|
+
${apiKeyForDisplay}
|
|
693
|
+
|
|
694
|
+
Use this to connect your dashboard.` : `Your API key is not stored in plaintext in this OpenClaw config (session-only or refresh-token flow). On the machine where you ran setup, run \`traderclaw config show\` to view it, or use the TraderClaw dashboard account settings.`;
|
|
695
|
+
return `\u{1F680} TraderClaw is live.
|
|
696
|
+
|
|
697
|
+
Connection established. The desk is up.
|
|
698
|
+
|
|
699
|
+
I'm now watching the Solana memecoin market, tracking launches, ingesting alpha signals, and analyzing liquidity, wallets, and sentiment in real time.
|
|
700
|
+
|
|
701
|
+
Nothing moves without context.
|
|
702
|
+
Nothing executes without passing risk.
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
\u{1F9E0} How I operate
|
|
706
|
+
|
|
707
|
+
I don't chase noise.
|
|
708
|
+
|
|
709
|
+
Scan \u2192 analyze \u2192 score \u2192 validate \u2192 execute.
|
|
710
|
+
Every trade is structured. Every decision is logged.
|
|
711
|
+
|
|
712
|
+
And I evolve.
|
|
713
|
+
|
|
714
|
+
Every outcome feeds back into the system.
|
|
715
|
+
Patterns improve. Filters sharpen. Decisions get better over time.
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
\u{1F511} Access
|
|
719
|
+
|
|
720
|
+
${keyBlock}
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
\u2699\uFE0F Get started
|
|
724
|
+
|
|
725
|
+
1) Fund your wallet
|
|
726
|
+
Send SOL to your trading wallet
|
|
727
|
+
Ask: what is my wallet address?
|
|
728
|
+
|
|
729
|
+
2) Choose operating mode
|
|
730
|
+
HARDENED \u2192 defensive, selective
|
|
731
|
+
DEGEN \u2192 aggressive, faster
|
|
732
|
+
|
|
733
|
+
3) Give me a name (optional)
|
|
734
|
+
Example: Your name is Atlas
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
\u{1F91D} How we work together
|
|
738
|
+
|
|
739
|
+
I operate autonomously, scanning, filtering, and acting when conditions make sense.
|
|
740
|
+
|
|
741
|
+
You can guide or question decisions anytime.
|
|
742
|
+
I will not execute trades that do not meet criteria.
|
|
743
|
+
|
|
744
|
+
Think of this as a trading desk you work with, not a bot you micromanage.
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
\u26A1 Command Examples (not limited)
|
|
748
|
+
|
|
749
|
+
\u2022 scan for alpha \u2192 start hunting
|
|
750
|
+
\u2022 status \u2192 full system check
|
|
751
|
+
\u2022 pause trading \u2192 halt execution
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
Start simple. Fund \u2192 set mode \u2192 observe.
|
|
755
|
+
|
|
756
|
+
Let's see what the market gives us.`;
|
|
757
|
+
}
|
|
758
|
+
var solanaTraderPlugin = {
|
|
759
|
+
id: "solana-trader",
|
|
760
|
+
name: "Solana Trader",
|
|
761
|
+
description: "Autonomous Solana memecoin trading agent \u2014 orchestrator integration",
|
|
762
|
+
register(api) {
|
|
763
|
+
const config = parseConfig(api.pluginConfig);
|
|
764
|
+
const { orchestratorUrl, walletId, apiKey, apiTimeout } = config;
|
|
765
|
+
if (!orchestratorUrl) {
|
|
766
|
+
api.logger.error("[solana-trader] orchestratorUrl is required in plugin config. Run: traderclaw setup");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (!apiKey && !config.refreshToken) {
|
|
770
|
+
api.logger.error(
|
|
771
|
+
"[solana-trader] apiKey or refreshToken is required. Tell the user to run on their machine: traderclaw setup --signup (or traderclaw signup) for a new account, or traderclaw setup / traderclaw login if they already have an API key. The agent cannot sign up or edit credentials."
|
|
772
|
+
);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
api.logger.info(
|
|
776
|
+
`[solana-trader] Session config: refreshToken=${config.refreshToken ? "present (" + config.refreshToken.slice(0, 8) + "...)" : "MISSING"}, apiKey=${apiKey ? "present" : "MISSING"}, walletPublicKey=${config.walletPublicKey ? "present" : "MISSING"}`
|
|
777
|
+
);
|
|
778
|
+
const sessionManager = new SessionManager({
|
|
779
|
+
baseUrl: orchestratorUrl,
|
|
780
|
+
apiKey: apiKey || "",
|
|
781
|
+
refreshToken: config.refreshToken,
|
|
782
|
+
walletPublicKey: config.walletPublicKey,
|
|
783
|
+
walletPrivateKeyProvider: () => {
|
|
784
|
+
const runtimeKey = process.env.TRADERCLAW_WALLET_PRIVATE_KEY || "";
|
|
785
|
+
return runtimeKey.trim() || void 0;
|
|
786
|
+
},
|
|
787
|
+
clientLabel: "openclaw-plugin-runtime",
|
|
788
|
+
timeout: apiTimeout,
|
|
789
|
+
onTokensRotated: (tokens) => {
|
|
790
|
+
const configPath = path.join(homedir(), ".openclaw", "openclaw.json");
|
|
791
|
+
try {
|
|
792
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
793
|
+
const entry = raw?.plugins?.entries?.["solana-trader"];
|
|
794
|
+
if (entry?.config) {
|
|
795
|
+
entry.config.refreshToken = tokens.refreshToken;
|
|
796
|
+
if (tokens.walletPublicKey) entry.config.walletPublicKey = tokens.walletPublicKey;
|
|
797
|
+
fs.writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
798
|
+
api.logger.info(`[solana-trader] Persisted rotated refreshToken to ${configPath}`);
|
|
799
|
+
} else {
|
|
800
|
+
api.logger.warn("[solana-trader] Could not persist refreshToken \u2014 plugin config entry not found in openclaw.json");
|
|
801
|
+
}
|
|
802
|
+
} catch (err) {
|
|
803
|
+
api.logger.warn(
|
|
804
|
+
`[solana-trader] Failed to persist rotated refreshToken: ${err instanceof Error ? err.message : String(err)}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
logger: {
|
|
809
|
+
info: (msg) => api.logger.info(`[solana-trader] ${msg}`),
|
|
810
|
+
warn: (msg) => api.logger.warn(`[solana-trader] ${msg}`),
|
|
811
|
+
error: (msg) => api.logger.error(`[solana-trader] ${msg}`)
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
const onUnauthorized = async () => {
|
|
815
|
+
api.logger.warn("[solana-trader] Received 401 \u2014 refreshing session...");
|
|
816
|
+
return sessionManager.handleUnauthorized();
|
|
817
|
+
};
|
|
818
|
+
const post = async (path2, body, extraHeaders) => {
|
|
819
|
+
const token = await sessionManager.getAccessToken();
|
|
820
|
+
return orchestratorRequest({
|
|
821
|
+
baseUrl: orchestratorUrl,
|
|
822
|
+
method: "POST",
|
|
823
|
+
path: path2,
|
|
824
|
+
body: { walletId, ...body },
|
|
825
|
+
timeout: apiTimeout,
|
|
826
|
+
accessToken: token,
|
|
827
|
+
extraHeaders,
|
|
828
|
+
onUnauthorized
|
|
829
|
+
});
|
|
830
|
+
};
|
|
831
|
+
const get = async (path2) => {
|
|
832
|
+
const token = await sessionManager.getAccessToken();
|
|
833
|
+
return orchestratorRequest({
|
|
834
|
+
baseUrl: orchestratorUrl,
|
|
835
|
+
method: "GET",
|
|
836
|
+
path: path2,
|
|
837
|
+
timeout: apiTimeout,
|
|
838
|
+
accessToken: token,
|
|
839
|
+
onUnauthorized
|
|
840
|
+
});
|
|
841
|
+
};
|
|
842
|
+
const put = async (path2, body) => {
|
|
843
|
+
const token = await sessionManager.getAccessToken();
|
|
844
|
+
return orchestratorRequest({
|
|
845
|
+
baseUrl: orchestratorUrl,
|
|
846
|
+
method: "PUT",
|
|
847
|
+
path: path2,
|
|
848
|
+
body,
|
|
849
|
+
timeout: apiTimeout,
|
|
850
|
+
accessToken: token,
|
|
851
|
+
onUnauthorized
|
|
852
|
+
});
|
|
853
|
+
};
|
|
854
|
+
const del = async (path2) => {
|
|
855
|
+
const token = await sessionManager.getAccessToken();
|
|
856
|
+
return orchestratorRequest({
|
|
857
|
+
baseUrl: orchestratorUrl,
|
|
858
|
+
method: "DELETE",
|
|
859
|
+
path: path2,
|
|
860
|
+
timeout: apiTimeout,
|
|
861
|
+
accessToken: token,
|
|
862
|
+
onUnauthorized
|
|
863
|
+
});
|
|
864
|
+
};
|
|
865
|
+
const json = (data) => ({
|
|
866
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
867
|
+
});
|
|
868
|
+
const wrapExecute = (fn) => async (toolCallId, params) => {
|
|
869
|
+
try {
|
|
870
|
+
const result = await fn(toolCallId, params ?? {});
|
|
871
|
+
return json(result);
|
|
872
|
+
} catch (err) {
|
|
873
|
+
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
const dataDir = config.dataDir || path.join(process.cwd(), ".traderclaw-v1-data");
|
|
877
|
+
const stateDir = path.join(dataDir, "state");
|
|
878
|
+
const logsDir = path.join(dataDir, "logs");
|
|
879
|
+
const sharedLogsDir = path.join(logsDir, "shared");
|
|
880
|
+
const memoryDir = path.join(process.cwd(), "memory");
|
|
881
|
+
const memoryMdPath = path.join(process.cwd(), "MEMORY.md");
|
|
882
|
+
const ensureDir = (dirPath) => {
|
|
883
|
+
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
|
884
|
+
};
|
|
885
|
+
ensureDir(stateDir);
|
|
886
|
+
ensureDir(sharedLogsDir);
|
|
887
|
+
const readJsonFile = (filePath) => {
|
|
888
|
+
try {
|
|
889
|
+
if (!fs.existsSync(filePath)) return null;
|
|
890
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
891
|
+
} catch {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
const deepMerge = (target, source) => {
|
|
896
|
+
const result = { ...target };
|
|
897
|
+
for (const key of Object.keys(source)) {
|
|
898
|
+
const sv = source[key];
|
|
899
|
+
const tv = result[key];
|
|
900
|
+
if (sv && typeof sv === "object" && !Array.isArray(sv) && tv && typeof tv === "object" && !Array.isArray(tv)) {
|
|
901
|
+
result[key] = deepMerge(tv, sv);
|
|
902
|
+
} else {
|
|
903
|
+
result[key] = sv;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return result;
|
|
907
|
+
};
|
|
908
|
+
const writeJsonFile = (filePath, data) => {
|
|
909
|
+
ensureDir(path.dirname(filePath));
|
|
910
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
911
|
+
};
|
|
912
|
+
const readJsonlFile = (filePath, maxEntries) => {
|
|
913
|
+
try {
|
|
914
|
+
if (!fs.existsSync(filePath)) return [];
|
|
915
|
+
const lines = fs.readFileSync(filePath, "utf-8").trim().split("\n").filter(Boolean);
|
|
916
|
+
const entries = lines.map((l) => {
|
|
917
|
+
try {
|
|
918
|
+
return JSON.parse(l);
|
|
919
|
+
} catch {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}).filter(Boolean);
|
|
923
|
+
return maxEntries ? entries.slice(-maxEntries) : entries;
|
|
924
|
+
} catch {
|
|
925
|
+
return [];
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
const appendJsonlFile = (filePath, entry, maxEntries) => {
|
|
929
|
+
ensureDir(path.dirname(filePath));
|
|
930
|
+
let entries = readJsonlFile(filePath);
|
|
931
|
+
entries.push(entry);
|
|
932
|
+
if (entries.length > maxEntries) entries = entries.slice(-maxEntries);
|
|
933
|
+
fs.writeFileSync(filePath, entries.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf-8");
|
|
934
|
+
return entries.length;
|
|
935
|
+
};
|
|
936
|
+
const generateMemoryMd = (aid, stateObj) => {
|
|
937
|
+
const lines = [
|
|
938
|
+
`# ${aid} \u2014 Durable Memory`,
|
|
939
|
+
``,
|
|
940
|
+
`> Auto-generated by solana_state_save. OpenClaw loads this file into context at every session start.`,
|
|
941
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
942
|
+
``
|
|
943
|
+
];
|
|
944
|
+
if (!stateObj || typeof stateObj !== "object") {
|
|
945
|
+
lines.push("_No state saved yet._");
|
|
946
|
+
return lines.join("\n");
|
|
947
|
+
}
|
|
948
|
+
const state = stateObj;
|
|
949
|
+
const identity = [];
|
|
950
|
+
if (state.tier) identity.push(`- **Tier:** ${state.tier}`);
|
|
951
|
+
if (state.walletId) identity.push(`- **Wallet:** ${state.walletId}`);
|
|
952
|
+
if (state.mode) identity.push(`- **Mode:** ${state.mode}`);
|
|
953
|
+
if (state.strategyVersion) identity.push(`- **Strategy Version:** ${state.strategyVersion}`);
|
|
954
|
+
if (state.regime) identity.push(`- **Regime:** ${state.regime}`);
|
|
955
|
+
if (state.maxPositions) identity.push(`- **Max Positions:** ${state.maxPositions}`);
|
|
956
|
+
if (state.maxPositionSizeSol) identity.push(`- **Max Position Size:** ${state.maxPositionSizeSol} SOL`);
|
|
957
|
+
if (identity.length > 0) {
|
|
958
|
+
lines.push("## Identity & Config", "", ...identity, "");
|
|
959
|
+
}
|
|
960
|
+
if (state.defenseMode !== void 0) lines.push(`## Defense Mode
|
|
961
|
+
|
|
962
|
+
- **Active:** ${state.defenseMode}
|
|
963
|
+
`);
|
|
964
|
+
if (state.killSwitchActive !== void 0) lines.push(`## Kill Switch
|
|
965
|
+
|
|
966
|
+
- **Active:** ${state.killSwitchActive}
|
|
967
|
+
`);
|
|
968
|
+
if (state.watchlist && Array.isArray(state.watchlist) && state.watchlist.length > 0) {
|
|
969
|
+
lines.push("## Watchlist", "");
|
|
970
|
+
for (const item of state.watchlist.slice(0, 20)) {
|
|
971
|
+
lines.push(`- ${typeof item === "string" ? item : JSON.stringify(item)}`);
|
|
972
|
+
}
|
|
973
|
+
lines.push("");
|
|
974
|
+
}
|
|
975
|
+
if (state.permanentLearnings && Array.isArray(state.permanentLearnings)) {
|
|
976
|
+
lines.push("## Permanent Learnings", "");
|
|
977
|
+
for (const learning of state.permanentLearnings.slice(0, 30)) {
|
|
978
|
+
lines.push(`- ${typeof learning === "string" ? learning : JSON.stringify(learning)}`);
|
|
979
|
+
}
|
|
980
|
+
lines.push("");
|
|
981
|
+
}
|
|
982
|
+
if (state.regimeCanary && typeof state.regimeCanary === "object") {
|
|
983
|
+
const rc = state.regimeCanary;
|
|
984
|
+
lines.push("## Regime Canary", "", `- **Regime:** ${rc.regime || "unknown"}`, `- **Detected At:** ${rc.detectedAt || "unknown"}`, "");
|
|
985
|
+
}
|
|
986
|
+
const excludeKeys = /* @__PURE__ */ new Set(["tier", "walletId", "mode", "strategyVersion", "regime", "maxPositions", "maxPositionSizeSol", "defenseMode", "killSwitchActive", "watchlist", "permanentLearnings", "regimeCanary"]);
|
|
987
|
+
const otherKeys = Object.keys(state).filter((k) => !excludeKeys.has(k));
|
|
988
|
+
if (otherKeys.length > 0) {
|
|
989
|
+
lines.push("## Other State Keys", "");
|
|
990
|
+
for (const key of otherKeys.slice(0, 30)) {
|
|
991
|
+
const val = state[key];
|
|
992
|
+
const display = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
993
|
+
lines.push(`- **${key}:** ${display.length > 200 ? display.slice(0, 200) + "\u2026" : display}`);
|
|
994
|
+
}
|
|
995
|
+
lines.push("");
|
|
996
|
+
}
|
|
997
|
+
return lines.join("\n");
|
|
998
|
+
};
|
|
999
|
+
const writeMemoryMd = (aid, stateObj) => {
|
|
1000
|
+
try {
|
|
1001
|
+
const content = generateMemoryMd(aid, stateObj);
|
|
1002
|
+
fs.writeFileSync(memoryMdPath, content, "utf-8");
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const getDailyLogPath = (date) => {
|
|
1007
|
+
const d = date || /* @__PURE__ */ new Date();
|
|
1008
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
1009
|
+
return path.join(memoryDir, `${dateStr}.md`);
|
|
1010
|
+
};
|
|
1011
|
+
const pruneDailyLogs = (retentionDays = 7) => {
|
|
1012
|
+
try {
|
|
1013
|
+
if (!fs.existsSync(memoryDir)) return;
|
|
1014
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
1015
|
+
cutoff.setDate(cutoff.getDate() - retentionDays);
|
|
1016
|
+
const files = fs.readdirSync(memoryDir).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f));
|
|
1017
|
+
for (const file of files) {
|
|
1018
|
+
const dateStr = file.replace(".md", "");
|
|
1019
|
+
if (new Date(dateStr) < cutoff) {
|
|
1020
|
+
try {
|
|
1021
|
+
fs.unlinkSync(path.join(memoryDir, file));
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
} catch {
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
const agentId = config.agentId || "main";
|
|
1030
|
+
const sanitizeAgentId = (id) => {
|
|
1031
|
+
const clean = id.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 64);
|
|
1032
|
+
if (!clean) return agentId;
|
|
1033
|
+
return clean;
|
|
1034
|
+
};
|
|
1035
|
+
api.registerTool({
|
|
1036
|
+
name: "solana_scan_launches",
|
|
1037
|
+
description: "Scan for new Solana token launches (Pump.fun, Raydium, PumpSwap). Returns recent launches with initial metrics. Watch for deployer patterns \u2014 same deployer launching multiple tokens is a serial rugger red flag.",
|
|
1038
|
+
parameters: Type.Object({}),
|
|
1039
|
+
execute: wrapExecute(async () => post("/api/scan/new-launches", {}))
|
|
1040
|
+
});
|
|
1041
|
+
api.registerTool({
|
|
1042
|
+
name: "solana_scan_hot_pairs",
|
|
1043
|
+
description: "Find Solana trading pairs with high volume and price acceleration. Returns hot pairs ranked by activity.",
|
|
1044
|
+
parameters: Type.Object({}),
|
|
1045
|
+
execute: wrapExecute(async () => post("/api/scan/hot-pairs", {}))
|
|
1046
|
+
});
|
|
1047
|
+
api.registerTool({
|
|
1048
|
+
name: "solana_market_regime",
|
|
1049
|
+
description: "Get the current Solana market regime (bullish/bearish/neutral) with aggregate metrics like total DEX volume and trending sectors.",
|
|
1050
|
+
parameters: Type.Object({}),
|
|
1051
|
+
execute: wrapExecute(async () => post("/api/market/regime", {}))
|
|
1052
|
+
});
|
|
1053
|
+
api.registerTool({
|
|
1054
|
+
name: "solana_token_snapshot",
|
|
1055
|
+
description: "Get a price/volume snapshot for a Solana token including current price, 24h OHLC, volume, and trade count.",
|
|
1056
|
+
parameters: Type.Object({
|
|
1057
|
+
tokenAddress: Type.String({ description: "Solana token mint address" })
|
|
1058
|
+
}),
|
|
1059
|
+
execute: wrapExecute(
|
|
1060
|
+
async (_id, params) => post("/api/token/snapshot", { tokenAddress: params.tokenAddress })
|
|
1061
|
+
)
|
|
1062
|
+
});
|
|
1063
|
+
api.registerTool({
|
|
1064
|
+
name: "solana_token_holders",
|
|
1065
|
+
description: "Get holder distribution for a Solana token \u2014 top 10 concentration, dev holdings percentage, total holder count.",
|
|
1066
|
+
parameters: Type.Object({
|
|
1067
|
+
tokenAddress: Type.String({ description: "Solana token mint address" })
|
|
1068
|
+
}),
|
|
1069
|
+
execute: wrapExecute(
|
|
1070
|
+
async (_id, params) => post("/api/token/holders", { tokenAddress: params.tokenAddress })
|
|
1071
|
+
)
|
|
1072
|
+
});
|
|
1073
|
+
api.registerTool({
|
|
1074
|
+
name: "solana_token_flows",
|
|
1075
|
+
description: "Get buy/sell flow data for a Solana token \u2014 pressure ratio, net flow, unique trader count.",
|
|
1076
|
+
parameters: Type.Object({
|
|
1077
|
+
tokenAddress: Type.String({ description: "Solana token mint address" })
|
|
1078
|
+
}),
|
|
1079
|
+
execute: wrapExecute(
|
|
1080
|
+
async (_id, params) => post("/api/token/flows", { tokenAddress: params.tokenAddress })
|
|
1081
|
+
)
|
|
1082
|
+
});
|
|
1083
|
+
api.registerTool({
|
|
1084
|
+
name: "solana_token_liquidity",
|
|
1085
|
+
description: "Get liquidity profile for a Solana token \u2014 pool depth in USD, locked liquidity percentage, DEX breakdown.",
|
|
1086
|
+
parameters: Type.Object({
|
|
1087
|
+
tokenAddress: Type.String({ description: "Solana token mint address" })
|
|
1088
|
+
}),
|
|
1089
|
+
execute: wrapExecute(
|
|
1090
|
+
async (_id, params) => post("/api/token/liquidity", { tokenAddress: params.tokenAddress })
|
|
1091
|
+
)
|
|
1092
|
+
});
|
|
1093
|
+
api.registerTool({
|
|
1094
|
+
name: "solana_token_risk",
|
|
1095
|
+
description: "Get composite risk assessment for a Solana token \u2014 checks mint authority, freeze authority, LP lock/burn status, deployer history, concentration, dev holdings, and honeypot indicators. Hard-skip tokens with active mint or freeze authority.",
|
|
1096
|
+
parameters: Type.Object({
|
|
1097
|
+
tokenAddress: Type.String({ description: "Solana token mint address" })
|
|
1098
|
+
}),
|
|
1099
|
+
execute: wrapExecute(
|
|
1100
|
+
async (_id, params) => post("/api/token/risk", { tokenAddress: params.tokenAddress })
|
|
1101
|
+
)
|
|
1102
|
+
});
|
|
1103
|
+
api.registerTool({
|
|
1104
|
+
name: "solana_build_thesis",
|
|
1105
|
+
description: "Build a complete thesis package for a token \u2014 assembles market data, your strategy weights, your prior trades on this token, journal stats, wallet context, and an advisory risk pre-screen. This is your full intelligence briefing before making a trade decision.",
|
|
1106
|
+
parameters: Type.Object({
|
|
1107
|
+
tokenAddress: Type.String({ description: "Solana token mint address" }),
|
|
1108
|
+
maxSizeSol: Type.Optional(Type.Number({ description: "Advisory \u2014 max position size in SOL for risk pre-screen. Not in server schema; accepted but currently ignored." }))
|
|
1109
|
+
}),
|
|
1110
|
+
execute: wrapExecute(
|
|
1111
|
+
async (_id, params) => post("/api/thesis/build", {
|
|
1112
|
+
tokenAddress: params.tokenAddress,
|
|
1113
|
+
maxSizeSol: params.maxSizeSol
|
|
1114
|
+
})
|
|
1115
|
+
)
|
|
1116
|
+
});
|
|
1117
|
+
api.registerTool({
|
|
1118
|
+
name: "solana_trade_precheck",
|
|
1119
|
+
description: "Pre-trade risk check \u2014 validates a proposed trade against risk rules, kill switch, entitlement limits, and on-chain conditions. Returns approved/denied with reasons and capped size. Always call this before executing a trade.",
|
|
1120
|
+
parameters: Type.Object({
|
|
1121
|
+
tokenAddress: Type.String({ description: "Solana token mint address" }),
|
|
1122
|
+
side: Type.Union([Type.Literal("buy"), Type.Literal("sell")], { description: "Trade direction" }),
|
|
1123
|
+
sizeSol: Type.Number({ description: "Intended position size in SOL" }),
|
|
1124
|
+
slippageBps: Type.Optional(Type.Number({ description: "Slippage tolerance in basis points (e.g., 300 = 3%)" }))
|
|
1125
|
+
}),
|
|
1126
|
+
execute: wrapExecute(
|
|
1127
|
+
async (_id, params) => post("/api/trade/precheck", {
|
|
1128
|
+
tokenAddress: params.tokenAddress,
|
|
1129
|
+
side: params.side,
|
|
1130
|
+
sizeSol: params.sizeSol,
|
|
1131
|
+
slippageBps: params.slippageBps
|
|
1132
|
+
})
|
|
1133
|
+
)
|
|
1134
|
+
});
|
|
1135
|
+
api.registerTool({
|
|
1136
|
+
name: "solana_trade_execute",
|
|
1137
|
+
description: "Execute a trade on Solana via the SpyFly bot. Enforces risk rules before proxying to on-chain execution. Returns trade ID, position ID, and transaction signature. IMPORTANT: tpLevels alone (e.g. [10, 15]) means EACH level sells 100% of the position at that gain \u2014 use tpExits for partials (e.g. +10% sell 50%, +15% sell 100%).",
|
|
1138
|
+
parameters: Type.Object({
|
|
1139
|
+
tokenAddress: Type.String({ description: "Solana token mint address" }),
|
|
1140
|
+
side: Type.Union([Type.Literal("buy"), Type.Literal("sell")], { description: "Trade direction" }),
|
|
1141
|
+
sizeSol: Type.Number({ description: "Position size in SOL" }),
|
|
1142
|
+
symbol: Type.String({ description: "Token symbol (e.g., BONK, WIF)" }),
|
|
1143
|
+
slippageBps: Type.Optional(Type.Number({ description: "Slippage in basis points (default: 300)" })),
|
|
1144
|
+
slPct: Type.Optional(Type.Number({ description: "Stop-loss percentage (e.g., 15 = 15% below entry)" })),
|
|
1145
|
+
tpLevels: Type.Optional(
|
|
1146
|
+
Type.Array(Type.Number(), {
|
|
1147
|
+
description: "TP gain % from entry only \u2014 each level defaults to selling 100% of position. Prefer tpExits when you want partial sells."
|
|
1148
|
+
})
|
|
1149
|
+
),
|
|
1150
|
+
tpExits: Type.Optional(
|
|
1151
|
+
Type.Array(
|
|
1152
|
+
Type.Object({
|
|
1153
|
+
percent: Type.Number({ description: "Take-profit trigger: % gain from entry (e.g. 10 = +10%)" }),
|
|
1154
|
+
amountPct: Type.Number({
|
|
1155
|
+
description: "% of position to sell at this TP (1\u2013100). Example: [{percent:10,amountPct:50},{percent:15,amountPct:100}]"
|
|
1156
|
+
})
|
|
1157
|
+
}),
|
|
1158
|
+
{ description: "Per-level take-profit sizes. Sent to API as tpExits; overrides plain tpLevels for sizing." }
|
|
1159
|
+
)
|
|
1160
|
+
),
|
|
1161
|
+
slExits: Type.Optional(
|
|
1162
|
+
Type.Array(
|
|
1163
|
+
Type.Object({
|
|
1164
|
+
percent: Type.Number({ description: "Stop-loss trigger: % drawdown from entry" }),
|
|
1165
|
+
amountPct: Type.Number({ description: "% of position to close at this SL level (1\u2013100)" })
|
|
1166
|
+
}),
|
|
1167
|
+
{ description: "Multi-level stop-loss with partial exits (optional). Otherwise use slPct for a single full exit." }
|
|
1168
|
+
)
|
|
1169
|
+
),
|
|
1170
|
+
trailingStopPct: Type.Optional(Type.Number({ description: "Trailing stop percentage" })),
|
|
1171
|
+
managementMode: Type.Optional(
|
|
1172
|
+
Type.Union([Type.Literal("LOCAL_MANAGED"), Type.Literal("SERVER_MANAGED")], {
|
|
1173
|
+
description: "Advisory only \u2014 server decides position mode internally. Sent for future compatibility."
|
|
1174
|
+
})
|
|
1175
|
+
),
|
|
1176
|
+
idempotencyKey: Type.Optional(Type.String({ description: "Unique key to prevent duplicate executions (e.g., UUID). Server uses walletId + key for replay cache." }))
|
|
1177
|
+
}),
|
|
1178
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1179
|
+
const headers = {};
|
|
1180
|
+
if (params.idempotencyKey) {
|
|
1181
|
+
headers["x-idempotency-key"] = String(params.idempotencyKey);
|
|
1182
|
+
}
|
|
1183
|
+
const body = {
|
|
1184
|
+
tokenAddress: params.tokenAddress,
|
|
1185
|
+
side: params.side,
|
|
1186
|
+
sizeSol: params.sizeSol,
|
|
1187
|
+
symbol: params.symbol,
|
|
1188
|
+
slippageBps: params.slippageBps,
|
|
1189
|
+
slPct: params.slPct,
|
|
1190
|
+
trailingStopPct: params.trailingStopPct,
|
|
1191
|
+
managementMode: params.managementMode
|
|
1192
|
+
};
|
|
1193
|
+
const tpExits = params.tpExits;
|
|
1194
|
+
const slExits = params.slExits;
|
|
1195
|
+
if (Array.isArray(tpExits) && tpExits.length > 0) {
|
|
1196
|
+
body.tpExits = tpExits;
|
|
1197
|
+
}
|
|
1198
|
+
if (Array.isArray(params.tpLevels) && params.tpLevels.length > 0) {
|
|
1199
|
+
body.tpLevels = params.tpLevels;
|
|
1200
|
+
}
|
|
1201
|
+
if (Array.isArray(slExits) && slExits.length > 0) {
|
|
1202
|
+
body.slExits = slExits;
|
|
1203
|
+
}
|
|
1204
|
+
return post("/api/trade/execute", body, Object.keys(headers).length > 0 ? headers : void 0);
|
|
1205
|
+
})
|
|
1206
|
+
});
|
|
1207
|
+
api.registerTool({
|
|
1208
|
+
name: "solana_trade_review",
|
|
1209
|
+
description: "Submit a post-trade review with outcome and notes. Creates a memory entry linked to the trade for future learning. Be honest \u2014 your future strategy evolution depends on accurate reviews.",
|
|
1210
|
+
parameters: Type.Object({
|
|
1211
|
+
tradeId: Type.Optional(Type.String({ description: "Trade ID (UUID) to review" })),
|
|
1212
|
+
tokenAddress: Type.Optional(Type.String({ description: "Token mint address for the reviewed trade" })),
|
|
1213
|
+
outcome: Type.Union([Type.Literal("win"), Type.Literal("loss"), Type.Literal("neutral")], {
|
|
1214
|
+
description: "Trade outcome"
|
|
1215
|
+
}),
|
|
1216
|
+
notes: Type.String({ description: "Detailed analysis: what worked, what didn't, key signals, lessons learned" }),
|
|
1217
|
+
pnlSol: Type.Optional(Type.Number({ description: "Actual profit/loss in SOL" })),
|
|
1218
|
+
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for categorization (e.g., ['momentum_win', 'late_entry'])" })),
|
|
1219
|
+
strategyVersion: Type.Optional(Type.String({ description: "Strategy version at time of trade (e.g., 'v1.3.0')" }))
|
|
1220
|
+
}),
|
|
1221
|
+
execute: wrapExecute(
|
|
1222
|
+
async (_id, params) => post("/api/trade/review", {
|
|
1223
|
+
tradeId: params.tradeId,
|
|
1224
|
+
tokenAddress: params.tokenAddress,
|
|
1225
|
+
outcome: params.outcome,
|
|
1226
|
+
notes: params.notes,
|
|
1227
|
+
pnlSol: params.pnlSol,
|
|
1228
|
+
tags: params.tags,
|
|
1229
|
+
strategyVersion: params.strategyVersion
|
|
1230
|
+
})
|
|
1231
|
+
)
|
|
1232
|
+
});
|
|
1233
|
+
api.registerTool({
|
|
1234
|
+
name: "solana_memory_write",
|
|
1235
|
+
description: "Write a memory entry \u2014 journal observations, market insights, or trading lessons. These memories are searchable and appear in future thesis packages.",
|
|
1236
|
+
parameters: Type.Object({
|
|
1237
|
+
notes: Type.String({ description: "Observation or lesson to remember" }),
|
|
1238
|
+
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for categorization (e.g., ['momentum', 'risk', 'regime'])" })),
|
|
1239
|
+
tokenAddress: Type.Optional(Type.String({ description: "Associate with a specific token" })),
|
|
1240
|
+
outcome: Type.Optional(Type.Union([Type.Literal("win"), Type.Literal("loss"), Type.Literal("neutral")], {
|
|
1241
|
+
description: "Outcome if trade-related"
|
|
1242
|
+
})),
|
|
1243
|
+
strategyVersion: Type.Optional(Type.String({ description: "Strategy version at time of writing (e.g., 'v1.3.0')" }))
|
|
1244
|
+
}),
|
|
1245
|
+
execute: wrapExecute(
|
|
1246
|
+
async (_id, params) => post("/api/memory/write", {
|
|
1247
|
+
notes: params.notes,
|
|
1248
|
+
tags: params.tags,
|
|
1249
|
+
tokenAddress: params.tokenAddress,
|
|
1250
|
+
outcome: params.outcome,
|
|
1251
|
+
strategyVersion: params.strategyVersion
|
|
1252
|
+
})
|
|
1253
|
+
)
|
|
1254
|
+
});
|
|
1255
|
+
api.registerTool({
|
|
1256
|
+
name: "solana_memory_search",
|
|
1257
|
+
description: "Search your trading memory by text query. Returns matching journal entries, trade reviews, and observations.",
|
|
1258
|
+
parameters: Type.Object({
|
|
1259
|
+
query: Type.String({ description: "Search text (e.g., 'high concentration tokens' or 'momentum plays')" }),
|
|
1260
|
+
limit: Type.Optional(Type.Number({ description: "Advisory \u2014 max results to return. Not honored by server; storage applies internal cap (~50)." }))
|
|
1261
|
+
}),
|
|
1262
|
+
execute: wrapExecute(
|
|
1263
|
+
async (_id, params) => post("/api/memory/search", {
|
|
1264
|
+
query: params.query,
|
|
1265
|
+
limit: params.limit
|
|
1266
|
+
})
|
|
1267
|
+
)
|
|
1268
|
+
});
|
|
1269
|
+
api.registerTool({
|
|
1270
|
+
name: "solana_memory_by_token",
|
|
1271
|
+
description: "Get all your prior memory entries for a specific token \u2014 past trades, reviews, and observations. MANDATORY: always call this before re-entering any token you've previously traded. Required by risk rules.",
|
|
1272
|
+
parameters: Type.Object({
|
|
1273
|
+
tokenAddress: Type.String({ description: "Solana token mint address" })
|
|
1274
|
+
}),
|
|
1275
|
+
execute: wrapExecute(
|
|
1276
|
+
async (_id, params) => post("/api/memory/by-token", {
|
|
1277
|
+
tokenAddress: params.tokenAddress
|
|
1278
|
+
})
|
|
1279
|
+
)
|
|
1280
|
+
});
|
|
1281
|
+
api.registerTool({
|
|
1282
|
+
name: "solana_journal_summary",
|
|
1283
|
+
description: "Get a summary of your trading journal \u2014 win rate, total entries, recent notes, and performance over a time period.",
|
|
1284
|
+
parameters: Type.Object({
|
|
1285
|
+
days: Type.Optional(Type.Number({ description: "Look back period in days (default: 7)" }))
|
|
1286
|
+
}),
|
|
1287
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1288
|
+
let path2 = `/api/memory/journal-summary?walletId=${walletId}`;
|
|
1289
|
+
if (params.days) path2 += `&lookbackDays=${params.days}`;
|
|
1290
|
+
return get(path2);
|
|
1291
|
+
})
|
|
1292
|
+
});
|
|
1293
|
+
api.registerTool({
|
|
1294
|
+
name: "solana_strategy_state",
|
|
1295
|
+
description: "Read your current strategy state \u2014 feature weights and strategy version. These are YOUR learned preferences that evolve over time.",
|
|
1296
|
+
parameters: Type.Object({}),
|
|
1297
|
+
execute: wrapExecute(
|
|
1298
|
+
async () => get(`/api/strategy/state?walletId=${walletId}`)
|
|
1299
|
+
)
|
|
1300
|
+
});
|
|
1301
|
+
api.registerTool({
|
|
1302
|
+
name: "solana_strategy_update",
|
|
1303
|
+
description: "Update your strategy weights and/or operating mode. Weights reflect which market signals best predict winners. Server enforces guardrails: min 3 features, each weight 0.01\u20130.50, sum 0.95\u20131.05, max \xB10.20 delta per feature, semver format required, version must increment. Always increment strategyVersion.",
|
|
1304
|
+
parameters: Type.Object({
|
|
1305
|
+
featureWeights: Type.Record(Type.String(), Type.Number(), {
|
|
1306
|
+
description: "Feature weight map (e.g., { volume_momentum: 0.25, buy_pressure: 0.20, ... }). Values should sum to ~1.0"
|
|
1307
|
+
}),
|
|
1308
|
+
strategyVersion: Type.String({ description: "New version string (e.g., 'v1.3.0'). Always increment from current." }),
|
|
1309
|
+
mode: Type.Optional(
|
|
1310
|
+
Type.Union([Type.Literal("HARDENED"), Type.Literal("DEGEN")], {
|
|
1311
|
+
description: "Operating mode. HARDENED = survival-first, DEGEN = high-velocity. Default: HARDENED"
|
|
1312
|
+
})
|
|
1313
|
+
)
|
|
1314
|
+
}),
|
|
1315
|
+
execute: wrapExecute(
|
|
1316
|
+
async (_id, params) => post("/api/strategy/update", {
|
|
1317
|
+
featureWeights: params.featureWeights,
|
|
1318
|
+
strategyVersion: params.strategyVersion,
|
|
1319
|
+
mode: params.mode
|
|
1320
|
+
})
|
|
1321
|
+
)
|
|
1322
|
+
});
|
|
1323
|
+
api.registerTool({
|
|
1324
|
+
name: "solana_killswitch",
|
|
1325
|
+
description: "Toggle the emergency kill switch. When enabled, ALL trade execution is blocked. Use in emergencies: repeated losses, unusual market behavior, or security concerns.",
|
|
1326
|
+
parameters: Type.Object({
|
|
1327
|
+
enabled: Type.Boolean({ description: "true to activate (block all trades), false to deactivate" }),
|
|
1328
|
+
mode: Type.Optional(
|
|
1329
|
+
Type.Union([Type.Literal("TRADES_ONLY"), Type.Literal("TRADES_AND_STREAMS")], {
|
|
1330
|
+
description: "TRADES_ONLY blocks execution; TRADES_AND_STREAMS blocks everything"
|
|
1331
|
+
})
|
|
1332
|
+
)
|
|
1333
|
+
}),
|
|
1334
|
+
execute: wrapExecute(
|
|
1335
|
+
async (_id, params) => post("/api/killswitch", {
|
|
1336
|
+
enabled: params.enabled,
|
|
1337
|
+
mode: params.mode
|
|
1338
|
+
})
|
|
1339
|
+
)
|
|
1340
|
+
});
|
|
1341
|
+
api.registerTool({
|
|
1342
|
+
name: "solana_killswitch_status",
|
|
1343
|
+
description: "Check the current kill switch state \u2014 whether it's enabled and in what mode.",
|
|
1344
|
+
parameters: Type.Object({}),
|
|
1345
|
+
execute: wrapExecute(
|
|
1346
|
+
async () => get(`/api/killswitch/status?walletId=${walletId}`)
|
|
1347
|
+
)
|
|
1348
|
+
});
|
|
1349
|
+
api.registerTool({
|
|
1350
|
+
name: "solana_capital_status",
|
|
1351
|
+
description: "Get your current capital status \u2014 SOL balance, open position count, unrealized PnL, daily notional used, daily loss, and effective limits (adjusted by entitlements).",
|
|
1352
|
+
parameters: Type.Object({}),
|
|
1353
|
+
execute: wrapExecute(
|
|
1354
|
+
async () => get(`/api/capital/status?walletId=${walletId}`)
|
|
1355
|
+
)
|
|
1356
|
+
});
|
|
1357
|
+
api.registerTool({
|
|
1358
|
+
name: "solana_positions",
|
|
1359
|
+
description: "List your current trading positions with unrealized PnL, entry price, current price, stop-loss/take-profit settings, and management mode. Call at the START of every trading cycle for interrupt check. Also use to detect dead money (flat positions).",
|
|
1360
|
+
parameters: Type.Object({
|
|
1361
|
+
status: Type.Optional(Type.String({ description: "Filter by status: 'open', 'closed', or omit for all" }))
|
|
1362
|
+
}),
|
|
1363
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1364
|
+
let path2 = `/api/wallet/positions?walletId=${walletId}`;
|
|
1365
|
+
if (params.status) path2 += `&status=${params.status}`;
|
|
1366
|
+
return get(path2);
|
|
1367
|
+
})
|
|
1368
|
+
});
|
|
1369
|
+
api.registerTool({
|
|
1370
|
+
name: "solana_funding_instructions",
|
|
1371
|
+
description: "Get deposit instructions for funding your trading wallet with SOL.",
|
|
1372
|
+
parameters: Type.Object({}),
|
|
1373
|
+
execute: wrapExecute(
|
|
1374
|
+
async () => get(`/api/funding/instructions?walletId=${walletId}`)
|
|
1375
|
+
)
|
|
1376
|
+
});
|
|
1377
|
+
api.registerTool({
|
|
1378
|
+
name: "solana_wallets",
|
|
1379
|
+
description: "List all wallets associated with your account. Optionally refresh balances from on-chain.",
|
|
1380
|
+
parameters: Type.Object({
|
|
1381
|
+
refresh: Type.Optional(Type.Boolean({ description: "If true, refresh balances from on-chain before returning" }))
|
|
1382
|
+
}),
|
|
1383
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1384
|
+
let path2 = "/api/wallets";
|
|
1385
|
+
if (params.refresh) path2 += "?refresh=true";
|
|
1386
|
+
return get(path2);
|
|
1387
|
+
})
|
|
1388
|
+
});
|
|
1389
|
+
api.registerTool({
|
|
1390
|
+
name: "solana_wallet_create",
|
|
1391
|
+
description: "Create a new trading wallet. Returns the wallet ID and public key. Use this to provision additional wallets for strategy isolation or multi-wallet trading.",
|
|
1392
|
+
parameters: Type.Object({
|
|
1393
|
+
label: Type.Optional(Type.String({ description: "Human-readable label for the wallet (e.g., 'Degen Wallet')" })),
|
|
1394
|
+
publicKey: Type.Optional(Type.String({ description: "Existing Solana public key to import (omit to generate new)" })),
|
|
1395
|
+
chain: Type.Optional(Type.Union([Type.Literal("solana"), Type.Literal("bsc")], { description: "Blockchain (default: solana)" })),
|
|
1396
|
+
ownerRef: Type.Optional(Type.String({ description: "Owner reference string" })),
|
|
1397
|
+
includePrivateKey: Type.Optional(Type.Boolean({ description: "If true, return the private key in the response (only for newly generated wallets)" }))
|
|
1398
|
+
}),
|
|
1399
|
+
execute: wrapExecute(
|
|
1400
|
+
async (_id, params) => post("/api/wallet/create", {
|
|
1401
|
+
label: params.label,
|
|
1402
|
+
publicKey: params.publicKey,
|
|
1403
|
+
chain: params.chain,
|
|
1404
|
+
ownerRef: params.ownerRef,
|
|
1405
|
+
includePrivateKey: params.includePrivateKey
|
|
1406
|
+
})
|
|
1407
|
+
)
|
|
1408
|
+
});
|
|
1409
|
+
api.registerTool({
|
|
1410
|
+
name: "solana_trades",
|
|
1411
|
+
description: "List your trade history with pagination. Returns executed trades with details like token, side, size, PnL, and timestamp.",
|
|
1412
|
+
parameters: Type.Object({
|
|
1413
|
+
limit: Type.Optional(Type.Number({ description: "Max trades to return (1-200, default: 50)" })),
|
|
1414
|
+
offset: Type.Optional(Type.Number({ description: "Offset for pagination (default: 0)" }))
|
|
1415
|
+
}),
|
|
1416
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1417
|
+
let path2 = `/api/trades?walletId=${walletId}`;
|
|
1418
|
+
if (params.limit) path2 += `&limit=${params.limit}`;
|
|
1419
|
+
if (params.offset) path2 += `&offset=${params.offset}`;
|
|
1420
|
+
return get(path2);
|
|
1421
|
+
})
|
|
1422
|
+
});
|
|
1423
|
+
api.registerTool({
|
|
1424
|
+
name: "solana_risk_denials",
|
|
1425
|
+
description: "List recent risk denials \u2014 trades that were blocked by the policy engine. Review these to understand what setups trigger denials and avoid repeating wasted analysis.",
|
|
1426
|
+
parameters: Type.Object({
|
|
1427
|
+
limit: Type.Optional(Type.Number({ description: "Max denials to return (1-200, default: 50)" }))
|
|
1428
|
+
}),
|
|
1429
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1430
|
+
let path2 = `/api/risk-denials?walletId=${walletId}`;
|
|
1431
|
+
if (params.limit) path2 += `&limit=${params.limit}`;
|
|
1432
|
+
return get(path2);
|
|
1433
|
+
})
|
|
1434
|
+
});
|
|
1435
|
+
api.registerTool({
|
|
1436
|
+
name: "solana_entitlement_costs",
|
|
1437
|
+
description: "Get tier costs \u2014 what each tier (starter, pro, enterprise) costs and what capabilities it unlocks.",
|
|
1438
|
+
parameters: Type.Object({}),
|
|
1439
|
+
execute: wrapExecute(async () => get("/api/entitlements/costs"))
|
|
1440
|
+
});
|
|
1441
|
+
api.registerTool({
|
|
1442
|
+
name: "solana_entitlement_plans",
|
|
1443
|
+
description: "List available monthly entitlement plans that upgrade your trading limits (position size, daily notional, bandwidth). Shows price, duration, and limit boosts.",
|
|
1444
|
+
parameters: Type.Object({}),
|
|
1445
|
+
execute: wrapExecute(async () => get("/api/entitlements/plans"))
|
|
1446
|
+
});
|
|
1447
|
+
api.registerTool({
|
|
1448
|
+
name: "solana_entitlement_current",
|
|
1449
|
+
description: "Get your current entitlements \u2014 active tier, scope access, effective limits, and expiration details.",
|
|
1450
|
+
parameters: Type.Object({}),
|
|
1451
|
+
execute: wrapExecute(async () => {
|
|
1452
|
+
const result = await get(`/api/entitlements/current?walletId=${walletId}`);
|
|
1453
|
+
if (result && typeof result === "object") {
|
|
1454
|
+
try {
|
|
1455
|
+
const cacheFile = path.join(stateDir, "entitlement-cache.json");
|
|
1456
|
+
writeJsonFile(cacheFile, { ...result, cachedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1457
|
+
} catch (_) {
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return result;
|
|
1461
|
+
})
|
|
1462
|
+
});
|
|
1463
|
+
api.registerTool({
|
|
1464
|
+
name: "solana_entitlement_purchase",
|
|
1465
|
+
description: "Purchase an entitlement plan to upgrade your trading limits. Deducts SOL from your wallet balance. Subject to spend guardrails (daily max, per-upgrade max, cooldown).",
|
|
1466
|
+
parameters: Type.Object({
|
|
1467
|
+
planCode: Type.String({ description: "Plan code to purchase (e.g., 'pro_trader', 'bandwidth_boost')" })
|
|
1468
|
+
}),
|
|
1469
|
+
execute: wrapExecute(
|
|
1470
|
+
async (_id, params) => post("/api/entitlements/purchase", {
|
|
1471
|
+
planCode: params.planCode
|
|
1472
|
+
})
|
|
1473
|
+
)
|
|
1474
|
+
});
|
|
1475
|
+
api.registerTool({
|
|
1476
|
+
name: "solana_entitlement_upgrade",
|
|
1477
|
+
description: "Upgrade your account tier (starter \u2192 pro \u2192 enterprise). Unlocks additional endpoints and capabilities. Pro tier is required for scanning, token analysis, and Bitquery tools.",
|
|
1478
|
+
parameters: Type.Object({
|
|
1479
|
+
targetTier: Type.Union([Type.Literal("starter"), Type.Literal("pro"), Type.Literal("enterprise")], {
|
|
1480
|
+
description: "Target tier to upgrade to"
|
|
1481
|
+
})
|
|
1482
|
+
}),
|
|
1483
|
+
execute: wrapExecute(
|
|
1484
|
+
async (_id, params) => post("/api/entitlements/upgrade", {
|
|
1485
|
+
targetTier: params.targetTier
|
|
1486
|
+
})
|
|
1487
|
+
)
|
|
1488
|
+
});
|
|
1489
|
+
api.registerTool({
|
|
1490
|
+
name: "solana_bitquery_templates",
|
|
1491
|
+
description: "List all available pre-built Bitquery query templates with descriptions and required variables. Call this first to discover what templates are available before using solana_bitquery_catalog. Returns 50+ templates organized by category covering Pump.fun, PumpSwap, Raydium, Jupiter, BonkSwap, and generic DEX analytics.",
|
|
1492
|
+
parameters: Type.Object({}),
|
|
1493
|
+
execute: wrapExecute(async () => ({
|
|
1494
|
+
categories: {
|
|
1495
|
+
pumpFunCreation: [
|
|
1496
|
+
{ path: "pumpFunCreation.trackNewTokens", description: "Track newly created Pump.fun tokens", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1497
|
+
{ path: "pumpFunCreation.getCreationTimeAndDev", description: "Get creation time and dev address for token", variables: { token: "String!" } },
|
|
1498
|
+
{ path: "pumpFunCreation.trackLaunchesRealtime", description: "Track new token launches in real-time via query polling", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1499
|
+
{ path: "pumpFunCreation.getTokensByCreatorAddress", description: "Get all Pump.fun tokens created by creator wallet", variables: { creator: "String!", limit: "Int!" } },
|
|
1500
|
+
{ path: "pumpFunCreation.getTokensByCreatorHistorical", description: "Historical token creations by wallet", variables: { creator: "String!", since: "DateTime!", till: "DateTime!" } }
|
|
1501
|
+
],
|
|
1502
|
+
pumpFunMetadata: [
|
|
1503
|
+
{ path: "pumpFunMetadata.tokenMetadataByAddress", description: "Get token metadata plus dev and creation time", variables: { token: "String!" } },
|
|
1504
|
+
{ path: "pumpFunMetadata.trackMayhemModeRealtime", description: "Track Mayhem Mode enabled tokens in real-time", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1505
|
+
{ path: "pumpFunMetadata.currentMayhemModeStatus", description: "Check current Mayhem mode status for token", variables: { token: "String!" } },
|
|
1506
|
+
{ path: "pumpFunMetadata.historicalMayhemModeStatus", description: "Historical mayhem mode changes for token", variables: { token: "String!", since: "DateTime!", till: "DateTime!" } },
|
|
1507
|
+
{ path: "pumpFunMetadata.latestPrice", description: "Latest price for Pump.fun token", variables: { token: "String!" } }
|
|
1508
|
+
],
|
|
1509
|
+
pumpFunPriceMomentum: [
|
|
1510
|
+
{ path: "pumpFunPriceMomentum.streamTokenPrice", description: "Price stream query for polling mode", variables: { token: "String!", since: "DateTime!" } },
|
|
1511
|
+
{ path: "pumpFunPriceMomentum.top10PriceChange5m", description: "Top 10 by short-term price change", variables: { since: "DateTime!" } },
|
|
1512
|
+
{ path: "pumpFunPriceMomentum.tokenOHLC", description: "OHLC data for Pump.fun token", variables: { token: "String!", since: "DateTime!" } },
|
|
1513
|
+
{ path: "pumpFunPriceMomentum.athMarketCapWindow", description: "ATH market cap in window", variables: { token: "String!", since: "DateTime!", till: "DateTime!" } },
|
|
1514
|
+
{ path: "pumpFunPriceMomentum.priceChangeDeltaFromMinutesAgo", description: "Price-change delta from X minutes back", variables: { token: "String!", since: "DateTime!" } }
|
|
1515
|
+
],
|
|
1516
|
+
pumpFunTradesLiquidity: [
|
|
1517
|
+
{ path: "pumpFunTradesLiquidity.realtimeTrades", description: "Get real-time trades on Pump.fun", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1518
|
+
{ path: "pumpFunTradesLiquidity.latestTradesByToken", description: "Latest trades by token", variables: { token: "String!", limit: "Int!" } },
|
|
1519
|
+
{ path: "pumpFunTradesLiquidity.tradingVolume", description: "Get trading volume for token", variables: { token: "String!", since: "DateTime!" } },
|
|
1520
|
+
{ path: "pumpFunTradesLiquidity.detailedTradeStats", description: "Detailed trade stats (volume/buys/sells/makers/buyers/sellers)", variables: { token: "String!", since: "DateTime!" } },
|
|
1521
|
+
{ path: "pumpFunTradesLiquidity.lastTradeBeforeMigration", description: "Last Pump.fun trade before migration to PumpSwap", variables: { token: "String!" } }
|
|
1522
|
+
],
|
|
1523
|
+
pumpFunHoldersRisk: [
|
|
1524
|
+
{ path: "pumpFunHoldersRisk.first100Buyers", description: "Get first 100 buyers", variables: { token: "String!" } },
|
|
1525
|
+
{ path: "pumpFunHoldersRisk.first100StillHolding", description: "Check whether first 100 buyers still hold", variables: { holders: "[String!]", token: "String!" } },
|
|
1526
|
+
{ path: "pumpFunHoldersRisk.devHoldings", description: "Get developer holdings for token", variables: { devWallet: "String!", token: "String!" } },
|
|
1527
|
+
{ path: "pumpFunHoldersRisk.topHoldersTopTradersTopCreators", description: "Get top holders/top traders/top creators", variables: { token: "String!", since: "DateTime!" } },
|
|
1528
|
+
{ path: "pumpFunHoldersRisk.phishyAndMarketCapFilters", description: "Phishy check + market cap filter scaffolding", variables: { since: "DateTime!", minCap: "String!", maxCap: "String!" } }
|
|
1529
|
+
],
|
|
1530
|
+
pumpSwapPostMigration: [
|
|
1531
|
+
{ path: "pumpSwapPostMigration.newPoolsRealtime", description: "Get newly created PumpSwap pools", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1532
|
+
{ path: "pumpSwapPostMigration.trackMigratedPools", description: "Track pools migrated to PumpSwap", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1533
|
+
{ path: "pumpSwapPostMigration.latestTrades", description: "Get latest trades on PumpSwap", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1534
|
+
{ path: "pumpSwapPostMigration.latestTradesByToken", description: "Latest PumpSwap trades for token", variables: { token: "String!", limit: "Int!" } },
|
|
1535
|
+
{ path: "pumpSwapPostMigration.pumpSwapSubscriptionScaffold", description: "Query mirror for PumpSwap realtime subscription", variables: { since: "DateTime!" } }
|
|
1536
|
+
],
|
|
1537
|
+
pumpSwapPriceTrader: [
|
|
1538
|
+
{ path: "pumpSwapPriceTrader.trackTokenPriceRealtime", description: "Track PumpSwap token price realtime", variables: { token: "String!", since: "DateTime!" } },
|
|
1539
|
+
{ path: "pumpSwapPriceTrader.latestPrice", description: "Get latest price for PumpSwap token", variables: { token: "String!" } },
|
|
1540
|
+
{ path: "pumpSwapPriceTrader.ohlc", description: "OHLC for PumpSwap token", variables: { token: "String!", since: "DateTime!" } },
|
|
1541
|
+
{ path: "pumpSwapPriceTrader.latestTradesByTrader", description: "Get latest trades by trader", variables: { wallet: "String!", since: "DateTime!" } },
|
|
1542
|
+
{ path: "pumpSwapPriceTrader.topTradersAndStats", description: "Top traders and token trade stats", variables: { token: "String!", since: "DateTime!" } }
|
|
1543
|
+
],
|
|
1544
|
+
launchpadsRaydiumLetsBonk: [
|
|
1545
|
+
{ path: "launchpadsRaydiumLetsBonk.latestRaydiumLaunchpadPools", description: "Track latest pools created on Raydium Launchpad", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1546
|
+
{ path: "launchpadsRaydiumLetsBonk.trackMigrationsToRaydium", description: "Track migrations to Raydium DEX/CPMM across launchpads", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1547
|
+
{ path: "launchpadsRaydiumLetsBonk.bondingCurveProgress", description: "Compute bonding curve progress from latest pool/liquidity snapshot", variables: { token: "String!", since: "DateTime!" } },
|
|
1548
|
+
{ path: "launchpadsRaydiumLetsBonk.tokensAbove95Progress", description: "Track launchpad tokens above 95% bonding curve progress", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1549
|
+
{ path: "launchpadsRaydiumLetsBonk.top100AboutToGraduate", description: "Top 100 launchpad tokens near migration", variables: { since: "DateTime!" } }
|
|
1550
|
+
],
|
|
1551
|
+
launchpadsTokenLevel: [
|
|
1552
|
+
{ path: "launchpadsTokenLevel.latestLaunchpadTrades", description: "Get latest launchpad trades", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1553
|
+
{ path: "launchpadsTokenLevel.latestPriceForToken", description: "Get latest price for launchpad token", variables: { token: "String!" } },
|
|
1554
|
+
{ path: "launchpadsTokenLevel.latestTradesByUser", description: "Get latest trades by user", variables: { wallet: "String!", since: "DateTime!" } },
|
|
1555
|
+
{ path: "launchpadsTokenLevel.topBuyersAndSellers", description: "Get top buyers and top sellers for token", variables: { token: "String!", since: "DateTime!" } },
|
|
1556
|
+
{ path: "launchpadsTokenLevel.ohlcPairAndLiquidity", description: "Get OHLC, pair address and latest liquidity", variables: { token: "String!", since: "DateTime!" } }
|
|
1557
|
+
],
|
|
1558
|
+
exchangeSpecific: [
|
|
1559
|
+
{ path: "exchangeSpecific.raydiumSuite", description: "Raydium: pools, pair create time, latest price, trades, LP changes, OHLC", variables: { token: "String!", since: "DateTime!" } },
|
|
1560
|
+
{ path: "exchangeSpecific.bonkSwapSuite", description: "BonkSwap: latest trades, top traders, trader feed, OHLC", variables: { token: "String!", wallet: "String!", since: "DateTime!" } },
|
|
1561
|
+
{ path: "exchangeSpecific.jupiterSuite", description: "Jupiter swaps and order lifecycle query suite", variables: { since: "DateTime!" } },
|
|
1562
|
+
{ path: "exchangeSpecific.jupiterStudioSuite", description: "Jupiter Studio token trades, prices, OHLC, launches, migrations", variables: { since: "DateTime!", token: "String" } }
|
|
1563
|
+
],
|
|
1564
|
+
genericDexAnalytics: [
|
|
1565
|
+
{ path: "genericDexAnalytics.latestSolanaTrades", description: "Subscribe/query latest Solana trades", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1566
|
+
{ path: "genericDexAnalytics.priceVsWsolUsdMultiMarket", description: "Token price vs WSOL/USD and multi-market", variables: { token: "String!", since: "DateTime!" } },
|
|
1567
|
+
{ path: "genericDexAnalytics.pressureTopsAndDexs", description: "Buy/sell pressure and top-bought/top-sold/pairs/dexs", variables: { since: "DateTime!", limit: "Int!" } },
|
|
1568
|
+
{ path: "genericDexAnalytics.dexMarketsPairsTokenDetails", description: "DEX markets/pairs/token details", variables: { token: "String!", since: "DateTime!" } },
|
|
1569
|
+
{ path: "genericDexAnalytics.ohlcHistoryAthTrendSearch", description: "OHLC history, ATH, first-24h, trend, search", variables: { token: "String!", since: "DateTime!" } }
|
|
1570
|
+
]
|
|
1571
|
+
},
|
|
1572
|
+
subscriptions: [
|
|
1573
|
+
{ key: "realtimeTokenPricesSolana", description: "Real-time token prices on Solana", variables: { token: "String!" } },
|
|
1574
|
+
{ key: "ohlc1s", description: "1-second OHLC stream", variables: { token: "String!" } },
|
|
1575
|
+
{ key: "dexPoolLiquidityChanges", description: "DEXPool liquidity changes stream", variables: { token: "String!" } },
|
|
1576
|
+
{ key: "pumpFunTokenCreation", description: "Pump.fun token creation stream", variables: {} },
|
|
1577
|
+
{ key: "pumpFunTrades", description: "Pump.fun trades stream", variables: { token: "String" } },
|
|
1578
|
+
{ key: "pumpSwapTrades", description: "PumpSwap trades stream", variables: { token: "String" } },
|
|
1579
|
+
{ key: "raydiumNewPools", description: "Raydium v4/Launchpad/CLMM new pools stream", variables: {} }
|
|
1580
|
+
],
|
|
1581
|
+
totalTemplates: 54,
|
|
1582
|
+
totalSubscriptions: 7,
|
|
1583
|
+
usage: "Use solana_bitquery_catalog with templatePath and variables to run any template. For custom queries, use solana_bitquery_query with raw GraphQL."
|
|
1584
|
+
}))
|
|
1585
|
+
});
|
|
1586
|
+
api.registerTool({
|
|
1587
|
+
name: "solana_bitquery_catalog",
|
|
1588
|
+
description: "Run a pre-built Bitquery query template from the catalog. Use solana_bitquery_templates first to discover available templates. Templates cover Pump.fun creation/metadata/price/trades/holders, PumpSwap post-migration, launchpad analytics, exchange-specific suites (Raydium/Jupiter/BonkSwap), and generic DEX analytics. See query-catalog.md in the solana-trader skill for the full reference.",
|
|
1589
|
+
parameters: Type.Object({
|
|
1590
|
+
templatePath: Type.String({ description: "Template path in 'category.key' format (e.g., 'pumpFunHoldersRisk.first100Buyers')" }),
|
|
1591
|
+
variables: Type.Object({}, { additionalProperties: true, description: "Variables required by the template (e.g., { token: 'MINT_ADDRESS', since: '2025-01-01T00:00:00Z' })" })
|
|
1592
|
+
}),
|
|
1593
|
+
execute: wrapExecute(
|
|
1594
|
+
async (_id, params) => post("/api/bitquery/catalog", {
|
|
1595
|
+
templatePath: params.templatePath,
|
|
1596
|
+
variables: params.variables || {}
|
|
1597
|
+
})
|
|
1598
|
+
)
|
|
1599
|
+
});
|
|
1600
|
+
api.registerTool({
|
|
1601
|
+
name: "solana_bitquery_query",
|
|
1602
|
+
description: "Run a custom raw GraphQL query against the Bitquery v2 EAP endpoint for Solana on-chain data. Use this when no pre-built template fits your needs. IMPORTANT: Consult bitquery-schema.md in the solana-trader skill before writing queries \u2014 DEXTrades and DEXTradeByTokens have different field shapes and mixing them causes errors. The schema reference includes a decision guide, correct field paths, aggregate keys, and a common error fix map.",
|
|
1603
|
+
parameters: Type.Object({
|
|
1604
|
+
query: Type.String({ description: "Raw GraphQL query string (query or subscription operation)" }),
|
|
1605
|
+
variables: Type.Optional(Type.Object({}, { additionalProperties: true, description: "GraphQL variables (e.g., { token: 'MINT_ADDRESS', since: '2025-01-01T00:00:00Z' })" }))
|
|
1606
|
+
}),
|
|
1607
|
+
execute: wrapExecute(
|
|
1608
|
+
async (_id, params) => post("/api/bitquery/query", {
|
|
1609
|
+
query: params.query,
|
|
1610
|
+
variables: params.variables || {}
|
|
1611
|
+
})
|
|
1612
|
+
)
|
|
1613
|
+
});
|
|
1614
|
+
api.registerTool({
|
|
1615
|
+
name: "solana_bitquery_subscribe",
|
|
1616
|
+
description: "Subscribe to a managed real-time Bitquery data stream. The orchestrator manages the WebSocket connection and broadcasts events. Available templates: realtimeTokenPricesSolana, ohlc1s, dexPoolLiquidityChanges, pumpFunTokenCreation, pumpFunTrades, pumpSwapTrades, raydiumNewPools. Returns a subscriptionId for tracking. Pass agentId to enable event-to-agent forwarding \u2014 orchestrator delivers each event to your Gateway via /v1/responses in addition to normal WS delivery. Subscriptions expire after 24h and emit subscription_expiring/subscription_expired events. See websocket-streaming.md in the solana-trader skill for the full message contract and usage patterns.",
|
|
1617
|
+
parameters: Type.Object({
|
|
1618
|
+
templateKey: Type.String({ description: "Subscription template key (e.g., 'pumpFunTrades', 'ohlc1s', 'realtimeTokenPricesSolana')" }),
|
|
1619
|
+
variables: Type.Optional(Type.Object({}, { additionalProperties: true, description: "Template variables (e.g., { token: 'MINT_ADDRESS' })" })),
|
|
1620
|
+
agentId: Type.Optional(Type.String({ description: "Agent ID for event-to-agent forwarding (e.g., 'main'). When set, orchestrator forwards each stream event to your registered Gateway via /v1/responses." })),
|
|
1621
|
+
subscriberType: Type.Optional(Type.Union([Type.Literal("agent"), Type.Literal("client")], { description: "Subscriber type. Inferred as 'agent' when agentId is present. Defaults to 'client'." }))
|
|
1622
|
+
}),
|
|
1623
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1624
|
+
const body = {
|
|
1625
|
+
templateKey: params.templateKey,
|
|
1626
|
+
variables: params.variables || {}
|
|
1627
|
+
};
|
|
1628
|
+
const effectiveAgentId = params.agentId || config.agentId;
|
|
1629
|
+
if (effectiveAgentId) {
|
|
1630
|
+
body.agentId = effectiveAgentId;
|
|
1631
|
+
body.subscriberType = params.subscriberType || "agent";
|
|
1632
|
+
} else if (params.subscriberType) {
|
|
1633
|
+
body.subscriberType = params.subscriberType;
|
|
1634
|
+
}
|
|
1635
|
+
return post("/api/bitquery/subscribe", body);
|
|
1636
|
+
})
|
|
1637
|
+
});
|
|
1638
|
+
api.registerTool({
|
|
1639
|
+
name: "solana_bitquery_unsubscribe",
|
|
1640
|
+
description: "Unsubscribe from a managed Bitquery data stream. Pass the subscriptionId returned by solana_bitquery_subscribe. Important: always use the server-returned subscriptionId, never generate your own.",
|
|
1641
|
+
parameters: Type.Object({
|
|
1642
|
+
subscriptionId: Type.String({ description: "Subscription ID returned by solana_bitquery_subscribe (e.g., 'bqs_abc123...')" })
|
|
1643
|
+
}),
|
|
1644
|
+
execute: wrapExecute(
|
|
1645
|
+
async (_id, params) => post("/api/bitquery/unsubscribe", {
|
|
1646
|
+
subscriptionId: params.subscriptionId
|
|
1647
|
+
})
|
|
1648
|
+
)
|
|
1649
|
+
});
|
|
1650
|
+
api.registerTool({
|
|
1651
|
+
name: "solana_bitquery_subscriptions",
|
|
1652
|
+
description: "List all active Bitquery subscriptions and bridge diagnostics. Returns connected clients, active streams, upstream connection status, and per-stream subscriber counts. Use for monitoring real-time data feed health.",
|
|
1653
|
+
parameters: Type.Object({}),
|
|
1654
|
+
execute: wrapExecute(
|
|
1655
|
+
async () => get("/api/bitquery/subscriptions/active")
|
|
1656
|
+
)
|
|
1657
|
+
});
|
|
1658
|
+
api.registerTool({
|
|
1659
|
+
name: "solana_bitquery_subscription_reopen",
|
|
1660
|
+
description: "Reopen an expired or expiring Bitquery subscription. Subscriptions have a 24h TTL and emit bitquery_subscription_expiring (30 min warning), bitquery_subscription_expired, and reconnect_required events. Call this to renew before or after expiry. The subscription_cleanup cron job handles this automatically, but manual reopen is available for critical subscriptions. Returns the new subscriptionId.",
|
|
1661
|
+
parameters: Type.Object({
|
|
1662
|
+
subscriptionId: Type.String({ description: "The expired or expiring subscription ID to reopen (e.g., 'bqs_abc123...')" }),
|
|
1663
|
+
walletId: Type.Optional(Type.String({ description: "Wallet ID to reopen the subscription for. Defaults to the plugin's configured walletId." }))
|
|
1664
|
+
}),
|
|
1665
|
+
execute: wrapExecute(
|
|
1666
|
+
async (_id, params) => post("/api/bitquery/subscriptions/reopen", {
|
|
1667
|
+
subscriptionId: params.subscriptionId,
|
|
1668
|
+
...params.walletId ? { walletId: params.walletId } : {}
|
|
1669
|
+
})
|
|
1670
|
+
)
|
|
1671
|
+
});
|
|
1672
|
+
api.registerTool({
|
|
1673
|
+
name: "solana_gateway_credentials_set",
|
|
1674
|
+
description: "Register or update your OpenClaw Gateway credentials with the orchestrator. This enables event-to-agent forwarding \u2014 when subscriptions include agentId, the orchestrator delivers each stream event to your Gateway via /v1/responses. Call this once during initial setup (Step 0). The gatewayBaseUrl is your self-hosted OpenClaw Gateway's public URL. The gatewayToken is the Bearer token for authenticating forwarded events.",
|
|
1675
|
+
parameters: Type.Object({
|
|
1676
|
+
gatewayBaseUrl: Type.String({ description: "Your OpenClaw Gateway's public HTTPS URL (e.g., 'https://gateway.example.com')" }),
|
|
1677
|
+
gatewayToken: Type.String({ description: "Bearer token for authenticating forwarded events to your Gateway" }),
|
|
1678
|
+
agentId: Type.Optional(Type.String({ description: "Agent ID to associate credentials with (default: 'main'). Omit to store as the default fallback." })),
|
|
1679
|
+
active: Type.Optional(Type.Boolean({ description: "Whether forwarding is active (default: true)" }))
|
|
1680
|
+
}),
|
|
1681
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1682
|
+
const body = {
|
|
1683
|
+
gatewayBaseUrl: params.gatewayBaseUrl,
|
|
1684
|
+
gatewayToken: params.gatewayToken
|
|
1685
|
+
};
|
|
1686
|
+
if (params.agentId) body.agentId = params.agentId;
|
|
1687
|
+
if (params.active !== void 0) body.active = params.active;
|
|
1688
|
+
return put("/api/agents/gateway-credentials", body);
|
|
1689
|
+
})
|
|
1690
|
+
});
|
|
1691
|
+
api.registerTool({
|
|
1692
|
+
name: "solana_gateway_credentials_get",
|
|
1693
|
+
description: "Get the currently registered Gateway credentials for event-to-agent forwarding. Returns the gatewayBaseUrl, agentId, active status, and masked token. Use to verify Gateway setup is correct.",
|
|
1694
|
+
parameters: Type.Object({}),
|
|
1695
|
+
execute: wrapExecute(
|
|
1696
|
+
async () => get("/api/agents/gateway-credentials")
|
|
1697
|
+
)
|
|
1698
|
+
});
|
|
1699
|
+
api.registerTool({
|
|
1700
|
+
name: "solana_gateway_credentials_delete",
|
|
1701
|
+
description: "Delete your registered Gateway credentials. This disables event-to-agent forwarding \u2014 subscriptions with agentId will no longer forward events to your Gateway. Only use if decommissioning the Gateway.",
|
|
1702
|
+
parameters: Type.Object({}),
|
|
1703
|
+
execute: wrapExecute(
|
|
1704
|
+
async () => del("/api/agents/gateway-credentials")
|
|
1705
|
+
)
|
|
1706
|
+
});
|
|
1707
|
+
api.registerTool({
|
|
1708
|
+
name: "solana_agent_sessions",
|
|
1709
|
+
description: "List active agent sessions registered with the orchestrator. Returns session IDs, agent IDs, connection status, and subscription counts. Use for diagnostics \u2014 verify your agent is properly registered and its subscriptions are forwarding events.",
|
|
1710
|
+
parameters: Type.Object({}),
|
|
1711
|
+
execute: wrapExecute(
|
|
1712
|
+
async () => get("/api/agents/active")
|
|
1713
|
+
)
|
|
1714
|
+
});
|
|
1715
|
+
const alphaBuffer = new AlphaBuffer();
|
|
1716
|
+
const alphaStreamManager = new AlphaStreamManager({
|
|
1717
|
+
wsUrl: orchestratorUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/ws",
|
|
1718
|
+
getAccessToken: () => sessionManager.getAccessToken(),
|
|
1719
|
+
buffer: alphaBuffer,
|
|
1720
|
+
agentId: config.agentId,
|
|
1721
|
+
logger: {
|
|
1722
|
+
info: (msg) => api.logger.info(`[solana-trader] ${msg}`),
|
|
1723
|
+
warn: (msg) => api.logger.warn(`[solana-trader] ${msg}`),
|
|
1724
|
+
error: (msg) => api.logger.error(`[solana-trader] ${msg}`)
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
let startupGateRunning = null;
|
|
1728
|
+
let startupGateState = {
|
|
1729
|
+
ok: false,
|
|
1730
|
+
ts: 0,
|
|
1731
|
+
steps: []
|
|
1732
|
+
};
|
|
1733
|
+
let lastForwardProbeState = null;
|
|
1734
|
+
const getActiveCredential = (payload) => {
|
|
1735
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1736
|
+
const credentials = payload.credentials;
|
|
1737
|
+
if (!Array.isArray(credentials)) return null;
|
|
1738
|
+
const preferredAgentId = config.agentId || "main";
|
|
1739
|
+
const active = credentials.find(
|
|
1740
|
+
(entry) => entry && typeof entry === "object" && Boolean(entry.active) && (entry.agentId || "main") === preferredAgentId
|
|
1741
|
+
) || credentials.find(
|
|
1742
|
+
(entry) => entry && typeof entry === "object" && Boolean(entry.active)
|
|
1743
|
+
);
|
|
1744
|
+
return active && typeof active === "object" ? active : null;
|
|
1745
|
+
};
|
|
1746
|
+
const runForwardProbe = async ({
|
|
1747
|
+
agentId: agentId2,
|
|
1748
|
+
source = "plugin_probe"
|
|
1749
|
+
} = {}) => {
|
|
1750
|
+
const payload = await post("/api/agents/gateway-forward-probe", {
|
|
1751
|
+
agentId: agentId2 || config.agentId || "main",
|
|
1752
|
+
source
|
|
1753
|
+
});
|
|
1754
|
+
const result = payload && typeof payload === "object" ? payload : {};
|
|
1755
|
+
const ok = Boolean(result.ok);
|
|
1756
|
+
lastForwardProbeState = {
|
|
1757
|
+
ok,
|
|
1758
|
+
ts: Date.now(),
|
|
1759
|
+
result
|
|
1760
|
+
};
|
|
1761
|
+
return result;
|
|
1762
|
+
};
|
|
1763
|
+
const runStartupGate = async ({
|
|
1764
|
+
autoFixGateway = true,
|
|
1765
|
+
force = false
|
|
1766
|
+
} = {}) => {
|
|
1767
|
+
if (startupGateRunning && !force) return startupGateRunning;
|
|
1768
|
+
startupGateRunning = (async () => {
|
|
1769
|
+
const steps = [];
|
|
1770
|
+
const pushStep = (entry) => steps.push(entry);
|
|
1771
|
+
try {
|
|
1772
|
+
await get("/api/system/status");
|
|
1773
|
+
pushStep({
|
|
1774
|
+
step: "solana_system_status",
|
|
1775
|
+
ok: true,
|
|
1776
|
+
ts: Date.now()
|
|
1777
|
+
});
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
pushStep({
|
|
1780
|
+
step: "solana_system_status",
|
|
1781
|
+
ok: false,
|
|
1782
|
+
ts: Date.now(),
|
|
1783
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
let gatewayStepOk = false;
|
|
1787
|
+
try {
|
|
1788
|
+
const creds = await get("/api/agents/gateway-credentials");
|
|
1789
|
+
let activeCredential = getActiveCredential(creds);
|
|
1790
|
+
if (!activeCredential && autoFixGateway) {
|
|
1791
|
+
const gatewayBaseUrl = String(config.gatewayBaseUrl || "").trim();
|
|
1792
|
+
const gatewayToken = String(config.gatewayToken || "").trim();
|
|
1793
|
+
if (gatewayBaseUrl && gatewayToken) {
|
|
1794
|
+
const body = {
|
|
1795
|
+
gatewayBaseUrl,
|
|
1796
|
+
gatewayToken,
|
|
1797
|
+
active: true
|
|
1798
|
+
};
|
|
1799
|
+
if (config.agentId) body.agentId = config.agentId;
|
|
1800
|
+
await put("/api/agents/gateway-credentials", body);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const refreshed = await get("/api/agents/gateway-credentials");
|
|
1804
|
+
activeCredential = getActiveCredential(refreshed);
|
|
1805
|
+
gatewayStepOk = Boolean(activeCredential);
|
|
1806
|
+
if (!gatewayStepOk) {
|
|
1807
|
+
throw new Error("Gateway credentials are missing or inactive");
|
|
1808
|
+
}
|
|
1809
|
+
pushStep({
|
|
1810
|
+
step: "solana_gateway_credentials_get",
|
|
1811
|
+
ok: true,
|
|
1812
|
+
ts: Date.now(),
|
|
1813
|
+
details: {
|
|
1814
|
+
active: true,
|
|
1815
|
+
agentId: String(activeCredential?.agentId || config.agentId || "main"),
|
|
1816
|
+
gatewayBaseUrl: String(activeCredential?.gatewayBaseUrl || "")
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
} catch (err) {
|
|
1820
|
+
pushStep({
|
|
1821
|
+
step: "solana_gateway_credentials_get",
|
|
1822
|
+
ok: false,
|
|
1823
|
+
ts: Date.now(),
|
|
1824
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1825
|
+
details: {
|
|
1826
|
+
hasConfiguredGatewayBaseUrl: Boolean(config.gatewayBaseUrl),
|
|
1827
|
+
hasConfiguredGatewayToken: Boolean(config.gatewayToken)
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
const effectiveAgentId = config.agentId || "main";
|
|
1833
|
+
if (effectiveAgentId && alphaStreamManager.getAgentId() !== effectiveAgentId) {
|
|
1834
|
+
alphaStreamManager.setAgentId(effectiveAgentId);
|
|
1835
|
+
}
|
|
1836
|
+
alphaStreamManager.setSubscriberType("agent");
|
|
1837
|
+
const subscribed = await alphaStreamManager.subscribe();
|
|
1838
|
+
pushStep({
|
|
1839
|
+
step: "solana_alpha_subscribe",
|
|
1840
|
+
ok: Boolean(subscribed?.subscribed),
|
|
1841
|
+
ts: Date.now(),
|
|
1842
|
+
details: {
|
|
1843
|
+
agentId: effectiveAgentId,
|
|
1844
|
+
premiumAccess: subscribed?.premiumAccess || false,
|
|
1845
|
+
tier: subscribed?.tier || ""
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
} catch (err) {
|
|
1849
|
+
pushStep({
|
|
1850
|
+
step: "solana_alpha_subscribe",
|
|
1851
|
+
ok: false,
|
|
1852
|
+
ts: Date.now(),
|
|
1853
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1854
|
+
details: {
|
|
1855
|
+
skippedBecauseGatewayFailed: !gatewayStepOk
|
|
1856
|
+
}
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
try {
|
|
1860
|
+
await get(`/api/capital/status?walletId=${walletId}`);
|
|
1861
|
+
pushStep({
|
|
1862
|
+
step: "solana_capital_status",
|
|
1863
|
+
ok: true,
|
|
1864
|
+
ts: Date.now()
|
|
1865
|
+
});
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
pushStep({
|
|
1868
|
+
step: "solana_capital_status",
|
|
1869
|
+
ok: false,
|
|
1870
|
+
ts: Date.now(),
|
|
1871
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
try {
|
|
1875
|
+
await get(`/api/wallet/positions?walletId=${walletId}`);
|
|
1876
|
+
pushStep({
|
|
1877
|
+
step: "solana_positions",
|
|
1878
|
+
ok: true,
|
|
1879
|
+
ts: Date.now()
|
|
1880
|
+
});
|
|
1881
|
+
} catch (err) {
|
|
1882
|
+
pushStep({
|
|
1883
|
+
step: "solana_positions",
|
|
1884
|
+
ok: false,
|
|
1885
|
+
ts: Date.now(),
|
|
1886
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
try {
|
|
1890
|
+
await get(`/api/killswitch/status?walletId=${walletId}`);
|
|
1891
|
+
pushStep({
|
|
1892
|
+
step: "solana_killswitch_status",
|
|
1893
|
+
ok: true,
|
|
1894
|
+
ts: Date.now()
|
|
1895
|
+
});
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
pushStep({
|
|
1898
|
+
step: "solana_killswitch_status",
|
|
1899
|
+
ok: false,
|
|
1900
|
+
ts: Date.now(),
|
|
1901
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
const passed = steps.filter((step) => step.ok).length;
|
|
1905
|
+
const failed = steps.length - passed;
|
|
1906
|
+
const failedSteps = steps.filter((step) => !step.ok);
|
|
1907
|
+
const onlyCapitalFailed = failedSteps.length === 1 && failedSteps[0]?.step === "solana_capital_status";
|
|
1908
|
+
startupGateState = {
|
|
1909
|
+
ok: failed === 0,
|
|
1910
|
+
ts: Date.now(),
|
|
1911
|
+
steps
|
|
1912
|
+
};
|
|
1913
|
+
const base = {
|
|
1914
|
+
ok: startupGateState.ok,
|
|
1915
|
+
ts: startupGateState.ts,
|
|
1916
|
+
steps,
|
|
1917
|
+
summary: { passed, failed }
|
|
1918
|
+
};
|
|
1919
|
+
const includeWelcome = startupGateState.ok || onlyCapitalFailed;
|
|
1920
|
+
if (includeWelcome) {
|
|
1921
|
+
const k = config.apiKey && String(config.apiKey).trim() || null;
|
|
1922
|
+
const out = { ...base, welcomeMessage: buildTraderClawWelcomeMessage(k) };
|
|
1923
|
+
if (onlyCapitalFailed && !startupGateState.ok) {
|
|
1924
|
+
return {
|
|
1925
|
+
...out,
|
|
1926
|
+
welcomeNote: "Startup gate reported solana_capital_status failed (e.g. capital API error). Welcome message still included so the user gets onboarding text and API key; fix capital/wallet if tools keep failing."
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
return out;
|
|
1930
|
+
}
|
|
1931
|
+
return base;
|
|
1932
|
+
})().finally(() => {
|
|
1933
|
+
startupGateRunning = null;
|
|
1934
|
+
});
|
|
1935
|
+
return startupGateRunning;
|
|
1936
|
+
};
|
|
1937
|
+
api.registerTool({
|
|
1938
|
+
name: "solana_alpha_subscribe",
|
|
1939
|
+
description: "Subscribe to the SpyFly alpha signal stream via WebSocket. Starts receiving real-time alpha signals (TG/Discord channel calls) into the buffer. Call once on first heartbeat \u2014 stays connected with auto-reconnect. Pass agentId to enable event-to-agent forwarding \u2014 orchestrator delivers each alpha signal to your Gateway via /v1/responses in addition to buffering. Returns subscription status, tier, and premium access level.",
|
|
1940
|
+
parameters: Type.Object({
|
|
1941
|
+
agentId: Type.Optional(Type.String({ description: "Agent ID for event-to-agent forwarding (e.g., 'main'). Overrides plugin config agentId if provided." })),
|
|
1942
|
+
subscriberType: Type.Optional(Type.String({ description: "Subscriber type: 'agent' (default when agentId is set) or 'user'. Controls how the orchestrator routes events." }))
|
|
1943
|
+
}),
|
|
1944
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1945
|
+
const effectiveAgentId = params.agentId || config.agentId;
|
|
1946
|
+
if (effectiveAgentId && alphaStreamManager.getAgentId() !== effectiveAgentId) {
|
|
1947
|
+
alphaStreamManager.setAgentId(effectiveAgentId);
|
|
1948
|
+
}
|
|
1949
|
+
const effectiveSubscriberType = params.subscriberType || (effectiveAgentId ? "agent" : void 0);
|
|
1950
|
+
if (effectiveSubscriberType) {
|
|
1951
|
+
alphaStreamManager.setSubscriberType(effectiveSubscriberType);
|
|
1952
|
+
}
|
|
1953
|
+
return alphaStreamManager.subscribe();
|
|
1954
|
+
})
|
|
1955
|
+
});
|
|
1956
|
+
api.registerTool({
|
|
1957
|
+
name: "solana_alpha_unsubscribe",
|
|
1958
|
+
description: "Unsubscribe from the SpyFly alpha signal stream and disconnect WebSocket. Use when shutting down or if kill switch is activated with TRADES_AND_STREAMS mode.",
|
|
1959
|
+
parameters: Type.Object({}),
|
|
1960
|
+
execute: wrapExecute(async () => alphaStreamManager.unsubscribe())
|
|
1961
|
+
});
|
|
1962
|
+
api.registerTool({
|
|
1963
|
+
name: "solana_alpha_signals",
|
|
1964
|
+
description: "Get buffered alpha signals from the SpyFly stream. By default returns only unseen signals and marks them as seen. Use minScore to filter low-quality signals. Poll this every heartbeat cycle in Step 1.5b. Returns signals sorted by ingestion time (newest last).",
|
|
1965
|
+
parameters: Type.Object({
|
|
1966
|
+
minScore: Type.Optional(Type.Number({ description: "Minimum systemScore threshold (0-100). Signals below this are excluded." })),
|
|
1967
|
+
chain: Type.Optional(Type.String({ description: "Filter by chain (e.g., 'solana'). BSC is already filtered at ingestion." })),
|
|
1968
|
+
kinds: Type.Optional(Type.Array(Type.String(), { description: "Filter by signal kind: 'ca_drop', 'milestone', 'update', 'risk', 'exit'" })),
|
|
1969
|
+
unseen: Type.Optional(Type.Boolean({ description: "If true (default), return only unseen signals and mark them as seen. Set false to get all buffered signals." }))
|
|
1970
|
+
}),
|
|
1971
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1972
|
+
const signals = alphaBuffer.getSignals({
|
|
1973
|
+
minScore: params.minScore,
|
|
1974
|
+
chain: params.chain,
|
|
1975
|
+
kinds: params.kinds,
|
|
1976
|
+
unseen: params.unseen !== void 0 ? params.unseen : true
|
|
1977
|
+
});
|
|
1978
|
+
return {
|
|
1979
|
+
signals,
|
|
1980
|
+
count: signals.length,
|
|
1981
|
+
bufferSize: alphaBuffer.getBufferSize(),
|
|
1982
|
+
subscribed: alphaStreamManager.isSubscribed(),
|
|
1983
|
+
stats: alphaStreamManager.getStats()
|
|
1984
|
+
};
|
|
1985
|
+
})
|
|
1986
|
+
});
|
|
1987
|
+
api.registerTool({
|
|
1988
|
+
name: "solana_alpha_history",
|
|
1989
|
+
description: "Query historical alpha signal data via the SpyFly REST API (GET /api/pings). Returns up to 1 year of stored signals for source reputation analysis, post-downtime catch-up, and strategy learning. Tier-gated: starter=10, pro=50, enterprise=200 results. 99.99% of tokens are dead but source patterns are invaluable.",
|
|
1990
|
+
parameters: Type.Object({
|
|
1991
|
+
tokenAddress: Type.Optional(Type.String({ description: "Filter by token mint address" })),
|
|
1992
|
+
channelId: Type.Optional(Type.String({ description: "Filter by source channel ID" })),
|
|
1993
|
+
limit: Type.Optional(Type.Number({ description: "Max results (tier-capped: starter=10, pro=50, enterprise=200)" })),
|
|
1994
|
+
days: Type.Optional(Type.Number({ description: "Look back period in days. Converted to then/now timestamp range." }))
|
|
1995
|
+
}),
|
|
1996
|
+
execute: wrapExecute(async (_id, params) => {
|
|
1997
|
+
const queryParts = [];
|
|
1998
|
+
if (params.limit) queryParts.push(`limit=${params.limit}`);
|
|
1999
|
+
if (params.channelId) queryParts.push(`channelId=${params.channelId}`);
|
|
2000
|
+
if (params.days) {
|
|
2001
|
+
const now = Date.now();
|
|
2002
|
+
const then = now - params.days * 24 * 60 * 60 * 1e3;
|
|
2003
|
+
queryParts.push(`then=${then}`);
|
|
2004
|
+
queryParts.push(`now=${now}`);
|
|
2005
|
+
}
|
|
2006
|
+
if (params.tokenAddress) queryParts.push(`tokenAddress=${params.tokenAddress}`);
|
|
2007
|
+
const qs = queryParts.length > 0 ? `?${queryParts.join("&")}` : "";
|
|
2008
|
+
return get(`/api/pings${qs}`);
|
|
2009
|
+
})
|
|
2010
|
+
});
|
|
2011
|
+
api.registerTool({
|
|
2012
|
+
name: "solana_alpha_sources",
|
|
2013
|
+
description: "Get per-source statistics from the alpha signal buffer \u2014 signal count, average systemScore, and source type for each channel. Use for quick reputation checks during signal processing and to identify high-quality vs low-quality sources.",
|
|
2014
|
+
parameters: Type.Object({}),
|
|
2015
|
+
execute: wrapExecute(async () => ({
|
|
2016
|
+
sources: alphaBuffer.getSourceStatsAll(),
|
|
2017
|
+
bufferSize: alphaBuffer.getBufferSize(),
|
|
2018
|
+
subscribed: alphaStreamManager.isSubscribed()
|
|
2019
|
+
}))
|
|
2020
|
+
});
|
|
2021
|
+
api.registerTool({
|
|
2022
|
+
name: "solana_system_status",
|
|
2023
|
+
description: "Check orchestrator system health \u2014 uptime, connected services, database status, execution mode, and upstream API connectivity.",
|
|
2024
|
+
parameters: Type.Object({}),
|
|
2025
|
+
execute: wrapExecute(async () => get("/api/system/status"))
|
|
2026
|
+
});
|
|
2027
|
+
api.registerTool({
|
|
2028
|
+
name: "solana_startup_gate",
|
|
2029
|
+
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.",
|
|
2030
|
+
parameters: Type.Object({
|
|
2031
|
+
autoFixGateway: Type.Optional(Type.Boolean({ description: "If true (default), auto-register gateway credentials when missing and config includes gatewayBaseUrl + gatewayToken." })),
|
|
2032
|
+
force: Type.Optional(Type.Boolean({ description: "If true, always run the startup checks now even if a recent run exists." }))
|
|
2033
|
+
}),
|
|
2034
|
+
execute: wrapExecute(
|
|
2035
|
+
async (_id, params) => runStartupGate({
|
|
2036
|
+
autoFixGateway: params.autoFixGateway !== void 0 ? Boolean(params.autoFixGateway) : true,
|
|
2037
|
+
force: Boolean(params.force)
|
|
2038
|
+
})
|
|
2039
|
+
)
|
|
2040
|
+
});
|
|
2041
|
+
api.registerTool({
|
|
2042
|
+
name: "solana_traderclaw_welcome",
|
|
2043
|
+
description: "Returns the canonical TraderClaw welcome message for the user after startup checks succeed (including when the only issue is zero balance \u2014 funding is separate). Includes API key when stored in plugin config. Use when the user ran the manual startup checklist instead of solana_startup_gate, or whenever welcomeMessage was not already appended from solana_startup_gate.",
|
|
2044
|
+
parameters: Type.Object({}),
|
|
2045
|
+
execute: wrapExecute(async () => {
|
|
2046
|
+
const k = config.apiKey && String(config.apiKey).trim() || null;
|
|
2047
|
+
return { welcomeMessage: buildTraderClawWelcomeMessage(k) };
|
|
2048
|
+
})
|
|
2049
|
+
});
|
|
2050
|
+
api.registerTool({
|
|
2051
|
+
name: "solana_gateway_forward_probe",
|
|
2052
|
+
description: "Run a synthetic orchestrator-to-gateway forwarding probe for /v1/responses and return latency plus failure diagnostics.",
|
|
2053
|
+
parameters: Type.Object({
|
|
2054
|
+
agentId: Type.Optional(Type.String({ description: "Agent ID to probe (default: plugin config agentId or 'main')." })),
|
|
2055
|
+
source: Type.Optional(Type.String({ description: "Probe source label for diagnostics." }))
|
|
2056
|
+
}),
|
|
2057
|
+
execute: wrapExecute(
|
|
2058
|
+
async (_id, params) => runForwardProbe({
|
|
2059
|
+
agentId: params.agentId ? String(params.agentId) : void 0,
|
|
2060
|
+
source: params.source ? String(params.source) : "plugin_probe_tool"
|
|
2061
|
+
})
|
|
2062
|
+
)
|
|
2063
|
+
});
|
|
2064
|
+
api.registerTool({
|
|
2065
|
+
name: "solana_runtime_status",
|
|
2066
|
+
description: "Return plugin runtime diagnostics including startup-gate cache, alpha stream status, and latest forwarding probe result.",
|
|
2067
|
+
parameters: Type.Object({}),
|
|
2068
|
+
execute: wrapExecute(async () => ({
|
|
2069
|
+
startupGate: startupGateState,
|
|
2070
|
+
alphaStream: {
|
|
2071
|
+
subscribed: alphaStreamManager.isSubscribed(),
|
|
2072
|
+
stats: alphaStreamManager.getStats(),
|
|
2073
|
+
bufferSize: alphaBuffer.getBufferSize()
|
|
2074
|
+
},
|
|
2075
|
+
lastForwardProbe: lastForwardProbeState
|
|
2076
|
+
}))
|
|
2077
|
+
});
|
|
2078
|
+
api.registerTool({
|
|
2079
|
+
name: "solana_state_save",
|
|
2080
|
+
description: "Persist durable agent state to local storage via deep merge. New keys are added, existing keys are updated, omitted keys are preserved. State survives across sessions and is auto-injected at bootstrap. Use for: strategy weights cache, watchlists, running counters, regime observations, any data that must survive session boundaries.",
|
|
2081
|
+
parameters: Type.Object({
|
|
2082
|
+
agentId: Type.String({ description: "Agent ID whose state to save (must match calling agent)." }),
|
|
2083
|
+
state: Type.Unknown({ description: "JSON object to deep-merge into existing state. New keys are added, existing keys are updated, omitted keys are preserved." }),
|
|
2084
|
+
overwrite: Type.Optional(Type.Boolean({ description: "If true, replace entire state instead of merging. Default false." }))
|
|
2085
|
+
}),
|
|
2086
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2087
|
+
const targetAgentId = sanitizeAgentId(String(params.agentId));
|
|
2088
|
+
const filePath = path.join(stateDir, `${targetAgentId}.json`);
|
|
2089
|
+
const shouldOverwrite = Boolean(params.overwrite);
|
|
2090
|
+
let mergedState;
|
|
2091
|
+
if (shouldOverwrite) {
|
|
2092
|
+
mergedState = params.state;
|
|
2093
|
+
} else {
|
|
2094
|
+
const existing = readJsonFile(filePath);
|
|
2095
|
+
const existingState = existing?.state && typeof existing.state === "object" ? existing.state : {};
|
|
2096
|
+
const newState = params.state && typeof params.state === "object" ? params.state : params.state;
|
|
2097
|
+
if (typeof existingState === "object" && typeof newState === "object" && newState !== null) {
|
|
2098
|
+
mergedState = deepMerge(existingState, newState);
|
|
2099
|
+
} else {
|
|
2100
|
+
mergedState = newState;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
const payload = { agentId: targetAgentId, state: mergedState, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2104
|
+
writeJsonFile(filePath, payload);
|
|
2105
|
+
writeMemoryMd(targetAgentId, mergedState);
|
|
2106
|
+
return { ok: true, agentId: targetAgentId, updatedAt: payload.updatedAt, merged: !shouldOverwrite, memoryMdWritten: true };
|
|
2107
|
+
})
|
|
2108
|
+
});
|
|
2109
|
+
api.registerTool({
|
|
2110
|
+
name: "solana_state_read",
|
|
2111
|
+
description: "Read durable agent state from local storage. Returns the last saved state object or null if no state exists. Also auto-injected at bootstrap \u2014 this tool is for mid-session reads.",
|
|
2112
|
+
parameters: Type.Object({
|
|
2113
|
+
agentId: Type.String({ description: "Agent ID whose state to read." })
|
|
2114
|
+
}),
|
|
2115
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2116
|
+
const targetAgentId = sanitizeAgentId(String(params.agentId));
|
|
2117
|
+
const filePath = path.join(stateDir, `${targetAgentId}.json`);
|
|
2118
|
+
const data = readJsonFile(filePath);
|
|
2119
|
+
return data || { agentId: targetAgentId, state: null, updatedAt: null };
|
|
2120
|
+
})
|
|
2121
|
+
});
|
|
2122
|
+
api.registerTool({
|
|
2123
|
+
name: "solana_decision_log",
|
|
2124
|
+
description: "Append a structured decision entry to the agent's episodic decision log. Maintains the last 50 entries per agent (FIFO). Entries are auto-injected at bootstrap for session continuity. Use for: trade decisions, analysis conclusions, relay actions, skip reasons.",
|
|
2125
|
+
parameters: Type.Object({
|
|
2126
|
+
agentId: Type.String({ description: "Agent ID writing the decision." }),
|
|
2127
|
+
type: Type.String({ description: "Decision type: 'trade_entry', 'trade_exit', 'skip', 'watch', 'relay', 'analysis', 'alert', 'cron_result'." }),
|
|
2128
|
+
token: Type.Optional(Type.String({ description: "Token mint address if decision relates to a specific token." })),
|
|
2129
|
+
rationale: Type.String({ description: "Brief reasoning for the decision (< 500 chars)." }),
|
|
2130
|
+
scores: Type.Optional(Type.Unknown({ description: "Relevant scores object (confidence, analyst scores, etc.)." })),
|
|
2131
|
+
outcome: Type.Optional(Type.String({ description: "Outcome if known: 'pending', 'win', 'loss', 'neutral'." }))
|
|
2132
|
+
}),
|
|
2133
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2134
|
+
const targetAgentId = sanitizeAgentId(String(params.agentId));
|
|
2135
|
+
const logPath = path.join(logsDir, targetAgentId, "decisions.jsonl");
|
|
2136
|
+
const entry = {
|
|
2137
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2138
|
+
agentId: targetAgentId,
|
|
2139
|
+
type: String(params.type),
|
|
2140
|
+
token: params.token ? String(params.token) : void 0,
|
|
2141
|
+
rationale: String(params.rationale),
|
|
2142
|
+
scores: params.scores || void 0,
|
|
2143
|
+
outcome: params.outcome ? String(params.outcome) : "pending"
|
|
2144
|
+
};
|
|
2145
|
+
const count = appendJsonlFile(logPath, entry, 50);
|
|
2146
|
+
return { ok: true, agentId: targetAgentId, entryCount: count };
|
|
2147
|
+
})
|
|
2148
|
+
});
|
|
2149
|
+
api.registerTool({
|
|
2150
|
+
name: "solana_team_bulletin_post",
|
|
2151
|
+
description: "Post a finding or alert to the shared team bulletin board. All agents can read the bulletin. Maintains last 200 entries with 3-day retention. Use for: broadcasting discoveries, risk alerts, regime observations, cross-agent coordination signals.",
|
|
2152
|
+
parameters: Type.Object({
|
|
2153
|
+
fromAgent: Type.String({ description: "Posting agent's ID." }),
|
|
2154
|
+
type: Type.String({ description: "Bulletin type: 'discovery', 'risk_alert', 'regime_shift', 'position_update', 'convergence', 'exhaustion', 'whale_move', 'source_rep_update', 'pattern_match'." }),
|
|
2155
|
+
priority: Type.String({ description: "Priority: 'low', 'medium', 'high', 'critical'." }),
|
|
2156
|
+
payload: Type.Unknown({ description: "Structured payload relevant to the bulletin type." })
|
|
2157
|
+
}),
|
|
2158
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2159
|
+
const fromAgent = sanitizeAgentId(String(params.fromAgent));
|
|
2160
|
+
const bulletinPath = path.join(sharedLogsDir, "team-bulletin.jsonl");
|
|
2161
|
+
const now = /* @__PURE__ */ new Date();
|
|
2162
|
+
const entry = {
|
|
2163
|
+
ts: now.toISOString(),
|
|
2164
|
+
fromAgent,
|
|
2165
|
+
type: String(params.type),
|
|
2166
|
+
priority: String(params.priority),
|
|
2167
|
+
payload: params.payload
|
|
2168
|
+
};
|
|
2169
|
+
let entries = readJsonlFile(bulletinPath);
|
|
2170
|
+
const threeDaysAgo = now.getTime() - 3 * 24 * 60 * 60 * 1e3;
|
|
2171
|
+
entries = entries.filter((e) => new Date(e.ts).getTime() > threeDaysAgo);
|
|
2172
|
+
entries.push(entry);
|
|
2173
|
+
if (entries.length > 200) entries = entries.slice(-200);
|
|
2174
|
+
fs.writeFileSync(bulletinPath, entries.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf-8");
|
|
2175
|
+
return { ok: true, entryCount: entries.length };
|
|
2176
|
+
})
|
|
2177
|
+
});
|
|
2178
|
+
api.registerTool({
|
|
2179
|
+
name: "solana_team_bulletin_read",
|
|
2180
|
+
description: "Read entries from the shared team bulletin board with optional filters. Returns entries in chronological order.",
|
|
2181
|
+
parameters: Type.Object({
|
|
2182
|
+
since: Type.Optional(Type.String({ description: "ISO timestamp \u2014 only return entries after this time." })),
|
|
2183
|
+
fromAgent: Type.Optional(Type.String({ description: "Filter by posting agent ID." })),
|
|
2184
|
+
type: Type.Optional(Type.String({ description: "Filter by bulletin type." })),
|
|
2185
|
+
limit: Type.Optional(Type.Number({ description: "Max entries to return (default 50)." }))
|
|
2186
|
+
}),
|
|
2187
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2188
|
+
const bulletinPath = path.join(sharedLogsDir, "team-bulletin.jsonl");
|
|
2189
|
+
let entries = readJsonlFile(bulletinPath);
|
|
2190
|
+
if (params.since) {
|
|
2191
|
+
const sinceTs = new Date(String(params.since)).getTime();
|
|
2192
|
+
entries = entries.filter((e) => new Date(String(e.ts)).getTime() > sinceTs);
|
|
2193
|
+
}
|
|
2194
|
+
if (params.fromAgent) entries = entries.filter((e) => e.fromAgent === String(params.fromAgent));
|
|
2195
|
+
if (params.type) entries = entries.filter((e) => e.type === String(params.type));
|
|
2196
|
+
const limit = typeof params.limit === "number" ? params.limit : 50;
|
|
2197
|
+
return { entries: entries.slice(-limit), total: entries.length };
|
|
2198
|
+
})
|
|
2199
|
+
});
|
|
2200
|
+
api.registerTool({
|
|
2201
|
+
name: "solana_context_snapshot_write",
|
|
2202
|
+
description: "Write the portfolio context snapshot. CTO writes this at the end of each session to give all agents a consistent world-view at next bootstrap. Contains: open positions, capital state, active regime, recent decisions summary, strategy version.",
|
|
2203
|
+
parameters: Type.Object({
|
|
2204
|
+
snapshot: Type.Unknown({ description: "Context snapshot object with positions, capital, regime, strategyVersion, activeSubscriptions, recentDecisions summary." })
|
|
2205
|
+
}),
|
|
2206
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2207
|
+
const filePath = path.join(stateDir, "context-snapshot.json");
|
|
2208
|
+
const payload = { snapshot: params.snapshot, writtenBy: agentId, ts: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2209
|
+
writeJsonFile(filePath, payload);
|
|
2210
|
+
return { ok: true, ts: payload.ts };
|
|
2211
|
+
})
|
|
2212
|
+
});
|
|
2213
|
+
api.registerTool({
|
|
2214
|
+
name: "solana_context_snapshot_read",
|
|
2215
|
+
description: "Read the latest portfolio context snapshot written by the CTO. Provides a consistent world-view: open positions, capital, regime, strategy version. Also auto-injected at bootstrap.",
|
|
2216
|
+
parameters: Type.Object({}),
|
|
2217
|
+
execute: wrapExecute(async () => {
|
|
2218
|
+
const filePath = path.join(stateDir, "context-snapshot.json");
|
|
2219
|
+
const data = readJsonFile(filePath);
|
|
2220
|
+
return data || { snapshot: null, ts: null };
|
|
2221
|
+
})
|
|
2222
|
+
});
|
|
2223
|
+
api.registerTool({
|
|
2224
|
+
name: "solana_compute_confidence",
|
|
2225
|
+
description: "Deterministic confidence score computation. Applies the V2 weighted formula with convergence bonus and risk penalty. Returns the computed score with full breakdown \u2014 no hallucination possible.",
|
|
2226
|
+
parameters: Type.Object({
|
|
2227
|
+
onchainScore: Type.Number({ description: "On-Chain Analyst score (0.0-1.0)." }),
|
|
2228
|
+
signalScore: Type.Number({ description: "Alpha Signal Analyst score (0.0-1.0)." }),
|
|
2229
|
+
socialScore: Type.Optional(Type.Number({ description: "Social Intelligence Analyst score (0.0-1.0). Default 0." })),
|
|
2230
|
+
smartMoneyScore: Type.Optional(Type.Number({ description: "Smart Money Tracker score (0.0-1.0). Default 0." })),
|
|
2231
|
+
riskPenalty: Type.Number({ description: "Risk penalty from Risk Officer flags, hardDeny, manipulation, liquidity, front-running, late freshness." }),
|
|
2232
|
+
weights: Type.Optional(Type.Object({
|
|
2233
|
+
onchain: Type.Optional(Type.Number()),
|
|
2234
|
+
signal: Type.Optional(Type.Number()),
|
|
2235
|
+
social: Type.Optional(Type.Number()),
|
|
2236
|
+
smart: Type.Optional(Type.Number())
|
|
2237
|
+
}, { description: "Custom weights. Default: onchain=0.45, signal=0.35, social=0.05, smart=0.15." })),
|
|
2238
|
+
convergenceSources: Type.Optional(Type.Number({ description: "Number of independent discovery sources that flagged same token. 2=+0.15, 3=+0.20, 4+=+0.25." }))
|
|
2239
|
+
}),
|
|
2240
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2241
|
+
const onchain = Number(params.onchainScore) || 0;
|
|
2242
|
+
const signal = Number(params.signalScore) || 0;
|
|
2243
|
+
const social = Number(params.socialScore) || 0;
|
|
2244
|
+
const smart = Number(params.smartMoneyScore) || 0;
|
|
2245
|
+
const penalty = Number(params.riskPenalty) || 0;
|
|
2246
|
+
const w = params.weights;
|
|
2247
|
+
const wOnchain = w?.onchain ?? 0.45;
|
|
2248
|
+
const wSignal = w?.signal ?? 0.35;
|
|
2249
|
+
const wSocial = w?.social ?? 0.05;
|
|
2250
|
+
const wSmart = w?.smart ?? 0.15;
|
|
2251
|
+
const sources = Number(params.convergenceSources) || 0;
|
|
2252
|
+
let convergenceBonus = 0;
|
|
2253
|
+
if (sources >= 4) convergenceBonus = 0.25;
|
|
2254
|
+
else if (sources >= 3) convergenceBonus = 0.2;
|
|
2255
|
+
else if (sources >= 2) convergenceBonus = 0.15;
|
|
2256
|
+
const raw = wOnchain * onchain + wSignal * signal + wSocial * social + wSmart * smart;
|
|
2257
|
+
const confidence = Math.max(0, Math.min(1, raw - penalty + convergenceBonus));
|
|
2258
|
+
return {
|
|
2259
|
+
confidence: Math.round(confidence * 1e4) / 1e4,
|
|
2260
|
+
raw: Math.round(raw * 1e4) / 1e4,
|
|
2261
|
+
convergenceBonus,
|
|
2262
|
+
riskPenalty: penalty,
|
|
2263
|
+
weights: { onchain: wOnchain, signal: wSignal, social: wSocial, smart: wSmart },
|
|
2264
|
+
components: {
|
|
2265
|
+
onchain: Math.round(wOnchain * onchain * 1e4) / 1e4,
|
|
2266
|
+
signal: Math.round(wSignal * signal * 1e4) / 1e4,
|
|
2267
|
+
social: Math.round(wSocial * social * 1e4) / 1e4,
|
|
2268
|
+
smart: Math.round(wSmart * smart * 1e4) / 1e4
|
|
2269
|
+
},
|
|
2270
|
+
formula: `(${wOnchain}\xD7${onchain}) + (${wSignal}\xD7${signal}) + (${wSocial}\xD7${social}) + (${wSmart}\xD7${smart}) - ${penalty} + ${convergenceBonus} = ${confidence.toFixed(4)}`
|
|
2271
|
+
};
|
|
2272
|
+
})
|
|
2273
|
+
});
|
|
2274
|
+
api.registerTool({
|
|
2275
|
+
name: "solana_compute_freshness_decay",
|
|
2276
|
+
description: "Compute signal freshness decay factor based on signal age. Returns a 0.0-1.0 multiplier and age category. Deterministic \u2014 no API calls.",
|
|
2277
|
+
parameters: Type.Object({
|
|
2278
|
+
signalAgeMinutes: Type.Number({ description: "Age of the signal in minutes since original call." }),
|
|
2279
|
+
signalType: Type.Optional(Type.String({ description: "Signal type: 'ca_drop' (default), 'exit', 'sentiment', 'confirmation'." }))
|
|
2280
|
+
}),
|
|
2281
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2282
|
+
const age = Number(params.signalAgeMinutes) || 0;
|
|
2283
|
+
const signalType = String(params.signalType || "ca_drop");
|
|
2284
|
+
let decay = 1;
|
|
2285
|
+
let category = "EARLY";
|
|
2286
|
+
let recommendation = "PROCEED";
|
|
2287
|
+
if (signalType === "exit" || signalType === "sentiment") {
|
|
2288
|
+
if (age <= 5) {
|
|
2289
|
+
decay = 1;
|
|
2290
|
+
category = "IMMEDIATE";
|
|
2291
|
+
} else if (age <= 15) {
|
|
2292
|
+
decay = 0.8;
|
|
2293
|
+
category = "RECENT";
|
|
2294
|
+
} else if (age <= 30) {
|
|
2295
|
+
decay = 0.5;
|
|
2296
|
+
category = "AGING";
|
|
2297
|
+
recommendation = "REDUCE_WEIGHT";
|
|
2298
|
+
} else {
|
|
2299
|
+
decay = 0.2;
|
|
2300
|
+
category = "STALE";
|
|
2301
|
+
recommendation = "SKIP";
|
|
2302
|
+
}
|
|
2303
|
+
} else {
|
|
2304
|
+
if (age <= 3) {
|
|
2305
|
+
decay = 1;
|
|
2306
|
+
category = "EARLY";
|
|
2307
|
+
} else if (age <= 10) {
|
|
2308
|
+
decay = 0.9;
|
|
2309
|
+
category = "ONTIME";
|
|
2310
|
+
} else if (age <= 30) {
|
|
2311
|
+
decay = 0.7;
|
|
2312
|
+
category = "LATE";
|
|
2313
|
+
recommendation = "REDUCE_SIZE";
|
|
2314
|
+
} else if (age <= 60) {
|
|
2315
|
+
decay = 0.4;
|
|
2316
|
+
category = "VERY_LATE";
|
|
2317
|
+
recommendation = "WATCH_ONLY";
|
|
2318
|
+
} else {
|
|
2319
|
+
decay = 0.1;
|
|
2320
|
+
category = "STALE";
|
|
2321
|
+
recommendation = "SKIP";
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
return { decayFactor: decay, ageMinutes: age, ageCategory: category, recommendation, signalType };
|
|
2325
|
+
})
|
|
2326
|
+
});
|
|
2327
|
+
api.registerTool({
|
|
2328
|
+
name: "solana_compute_position_limits",
|
|
2329
|
+
description: "Compute final position size after all stacked reductions. Applies mode-based range \u2192 Risk Officer cap \u2192 precheck cap \u2192 liquidity hard cap \u2192 reduction triggers \u2192 floor. Returns sizeSol with full reduction breakdown. Deterministic.",
|
|
2330
|
+
parameters: Type.Object({
|
|
2331
|
+
mode: Type.String({ description: "'HARDENED' or 'DEGEN'." }),
|
|
2332
|
+
confidence: Type.Number({ description: "Confidence score (0.0-1.0)." }),
|
|
2333
|
+
capitalSol: Type.Number({ description: "Total available capital in SOL." }),
|
|
2334
|
+
poolDepthUsd: Type.Number({ description: "Pool liquidity depth in USD." }),
|
|
2335
|
+
solPriceUsd: Type.Number({ description: "Current SOL price in USD for pool-depth conversion." }),
|
|
2336
|
+
lifecycle: Type.String({ description: "'FRESH', 'EMERGING', or 'ESTABLISHED'." }),
|
|
2337
|
+
winRateLast10: Type.Optional(Type.Number({ description: "Win rate over last 10 trades (0.0-1.0)." })),
|
|
2338
|
+
dailyNotionalUsedPct: Type.Optional(Type.Number({ description: "Daily notional used as percentage (0-100)." })),
|
|
2339
|
+
consecutiveLosses: Type.Optional(Type.Number({ description: "Current consecutive loss count." })),
|
|
2340
|
+
openPositionCount: Type.Optional(Type.Number({ description: "Number of open positions." })),
|
|
2341
|
+
tokenConcentrationPct: Type.Optional(Type.Number({ description: "Token concentration percentage (0-100)." })),
|
|
2342
|
+
priceMovePct: Type.Optional(Type.Number({ description: "Token price move percentage from recent low." })),
|
|
2343
|
+
riskOfficerMaxSizeSol: Type.Optional(Type.Number({ description: "Risk Officer's maxSizeSol cap." })),
|
|
2344
|
+
precheckCappedSizeSol: Type.Optional(Type.Number({ description: "Precheck cappedSizeSol." }))
|
|
2345
|
+
}),
|
|
2346
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2347
|
+
const mode = String(params.mode).toUpperCase();
|
|
2348
|
+
const isHardened = mode === "HARDENED";
|
|
2349
|
+
const confidence = Number(params.confidence) || 0;
|
|
2350
|
+
const capital = Number(params.capitalSol) || 0;
|
|
2351
|
+
const poolUsd = Number(params.poolDepthUsd) || 0;
|
|
2352
|
+
const solPrice = Number(params.solPriceUsd) || 1;
|
|
2353
|
+
const lifecycle = String(params.lifecycle).toUpperCase();
|
|
2354
|
+
const reductions = [];
|
|
2355
|
+
const highMin = isHardened ? 0.1 : 0.12;
|
|
2356
|
+
const highMax = isHardened ? 0.2 : 0.25;
|
|
2357
|
+
const exploMin = isHardened ? 0.03 : 0.05;
|
|
2358
|
+
const exploMax = isHardened ? 0.08 : 0.1;
|
|
2359
|
+
const isHighConf = confidence > 0.75;
|
|
2360
|
+
let baseMin = isHighConf ? highMin : exploMin;
|
|
2361
|
+
let baseMax = isHighConf ? highMax : exploMax;
|
|
2362
|
+
if (lifecycle === "FRESH") {
|
|
2363
|
+
baseMin = exploMin;
|
|
2364
|
+
baseMax = isHardened ? 0.05 : exploMax;
|
|
2365
|
+
}
|
|
2366
|
+
let sizeSol = capital * ((baseMin + baseMax) / 2);
|
|
2367
|
+
const riskMax = params.riskOfficerMaxSizeSol != null ? Number(params.riskOfficerMaxSizeSol) : Infinity;
|
|
2368
|
+
if (riskMax < sizeSol) {
|
|
2369
|
+
reductions.push({ factor: riskMax / sizeSol, reason: "Risk Officer maxSizeSol cap" });
|
|
2370
|
+
sizeSol = riskMax;
|
|
2371
|
+
}
|
|
2372
|
+
const precheckCap = params.precheckCappedSizeSol != null ? Number(params.precheckCappedSizeSol) : Infinity;
|
|
2373
|
+
if (precheckCap < sizeSol) {
|
|
2374
|
+
reductions.push({ factor: precheckCap / sizeSol, reason: "Precheck cappedSizeSol" });
|
|
2375
|
+
sizeSol = precheckCap;
|
|
2376
|
+
}
|
|
2377
|
+
const poolCapSol = poolUsd * 0.02 / solPrice;
|
|
2378
|
+
const poolHardCapSol = poolUsd < 5e4 ? 1e3 / solPrice : Infinity;
|
|
2379
|
+
const effectivePoolCap = Math.min(poolCapSol, poolHardCapSol);
|
|
2380
|
+
if (effectivePoolCap < sizeSol) {
|
|
2381
|
+
reductions.push({ factor: effectivePoolCap / sizeSol, reason: poolUsd < 5e4 ? "Pool < $50K hard cap ($1K max)" : "2% pool depth cap" });
|
|
2382
|
+
sizeSol = effectivePoolCap;
|
|
2383
|
+
}
|
|
2384
|
+
const wr = params.winRateLast10 != null ? Number(params.winRateLast10) : 1;
|
|
2385
|
+
if (wr < 0.4) {
|
|
2386
|
+
sizeSol *= 0.6;
|
|
2387
|
+
reductions.push({ factor: 0.6, reason: "Win rate < 40%" });
|
|
2388
|
+
}
|
|
2389
|
+
const dnPct = params.dailyNotionalUsedPct != null ? Number(params.dailyNotionalUsedPct) : 0;
|
|
2390
|
+
if (dnPct > 70) {
|
|
2391
|
+
sizeSol *= 0.5;
|
|
2392
|
+
reductions.push({ factor: 0.5, reason: "Daily notional > 70%" });
|
|
2393
|
+
}
|
|
2394
|
+
const consLoss = params.consecutiveLosses != null ? Number(params.consecutiveLosses) : 0;
|
|
2395
|
+
if (consLoss >= 2) {
|
|
2396
|
+
sizeSol *= 0.7;
|
|
2397
|
+
reductions.push({ factor: 0.7, reason: `${consLoss} consecutive losses` });
|
|
2398
|
+
}
|
|
2399
|
+
const openPos = params.openPositionCount != null ? Number(params.openPositionCount) : 0;
|
|
2400
|
+
if (openPos >= 3) {
|
|
2401
|
+
sizeSol *= 0.8;
|
|
2402
|
+
reductions.push({ factor: 0.8, reason: `${openPos} open positions` });
|
|
2403
|
+
}
|
|
2404
|
+
const concPct = params.tokenConcentrationPct != null ? Number(params.tokenConcentrationPct) : 0;
|
|
2405
|
+
if (concPct > 30) {
|
|
2406
|
+
sizeSol *= 0.5;
|
|
2407
|
+
reductions.push({ factor: 0.5, reason: "Token concentration > 30%" });
|
|
2408
|
+
}
|
|
2409
|
+
const pricePct = params.priceMovePct != null ? Number(params.priceMovePct) : 0;
|
|
2410
|
+
if (pricePct > 200) {
|
|
2411
|
+
sizeSol *= 0.5;
|
|
2412
|
+
reductions.push({ factor: 0.5, reason: "Token moved +200%" });
|
|
2413
|
+
}
|
|
2414
|
+
const floorPct = isHardened ? 75e-4 : 0.0125;
|
|
2415
|
+
const floor = capital * floorPct;
|
|
2416
|
+
if (sizeSol < floor) {
|
|
2417
|
+
sizeSol = floor;
|
|
2418
|
+
reductions.push({ factor: 1, reason: `Floor applied: ${(floorPct * 100).toFixed(2)}% of capital` });
|
|
2419
|
+
}
|
|
2420
|
+
return {
|
|
2421
|
+
sizeSol: Math.round(sizeSol * 1e4) / 1e4,
|
|
2422
|
+
mode,
|
|
2423
|
+
baseRange: { min: baseMin, max: baseMax },
|
|
2424
|
+
poolCap: Math.round(effectivePoolCap * 1e4) / 1e4,
|
|
2425
|
+
floor: Math.round(floor * 1e4) / 1e4,
|
|
2426
|
+
reductions
|
|
2427
|
+
};
|
|
2428
|
+
})
|
|
2429
|
+
});
|
|
2430
|
+
api.registerTool({
|
|
2431
|
+
name: "solana_classify_deployer_risk",
|
|
2432
|
+
description: "Classify deployer wallet risk level based on history. Returns risk class, score, and flags. Deterministic computation \u2014 no API calls.",
|
|
2433
|
+
parameters: Type.Object({
|
|
2434
|
+
previousTokens: Type.Number({ description: "Number of tokens previously deployed by this wallet." }),
|
|
2435
|
+
rugHistory: Type.Boolean({ description: "Whether any previous token was a confirmed rug." }),
|
|
2436
|
+
avgTokenLifespanHours: Type.Optional(Type.Number({ description: "Average lifespan of previous tokens in hours." })),
|
|
2437
|
+
freshWalletSurge: Type.Optional(Type.Number({ description: "Fresh wallet surge ratio (0.0-1.0) for this deployer's tokens." })),
|
|
2438
|
+
devSoldEarlyCount: Type.Optional(Type.Number({ description: "Number of previous tokens where dev sold within first hour." }))
|
|
2439
|
+
}),
|
|
2440
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2441
|
+
const prev = Number(params.previousTokens) || 0;
|
|
2442
|
+
const rugged = Boolean(params.rugHistory);
|
|
2443
|
+
const avgLife = params.avgTokenLifespanHours != null ? Number(params.avgTokenLifespanHours) : null;
|
|
2444
|
+
const freshSurge = params.freshWalletSurge != null ? Number(params.freshWalletSurge) : 0;
|
|
2445
|
+
const devSold = params.devSoldEarlyCount != null ? Number(params.devSoldEarlyCount) : 0;
|
|
2446
|
+
const flags = [];
|
|
2447
|
+
let score = 0;
|
|
2448
|
+
if (rugged) {
|
|
2449
|
+
score += 40;
|
|
2450
|
+
flags.push("CONFIRMED_RUG_HISTORY");
|
|
2451
|
+
}
|
|
2452
|
+
if (prev >= 10) {
|
|
2453
|
+
score += 20;
|
|
2454
|
+
flags.push("SERIAL_DEPLOYER");
|
|
2455
|
+
} else if (prev >= 5) {
|
|
2456
|
+
score += 10;
|
|
2457
|
+
flags.push("FREQUENT_DEPLOYER");
|
|
2458
|
+
}
|
|
2459
|
+
if (avgLife !== null && avgLife < 2) {
|
|
2460
|
+
score += 15;
|
|
2461
|
+
flags.push("SHORT_LIVED_TOKENS");
|
|
2462
|
+
}
|
|
2463
|
+
if (freshSurge > 0.5) {
|
|
2464
|
+
score += 15;
|
|
2465
|
+
flags.push("HIGH_FRESH_WALLET_SURGE");
|
|
2466
|
+
}
|
|
2467
|
+
if (devSold > 0 && prev > 0 && devSold / prev > 0.5) {
|
|
2468
|
+
score += 10;
|
|
2469
|
+
flags.push("FREQUENT_EARLY_DEV_SELLS");
|
|
2470
|
+
}
|
|
2471
|
+
let riskClass;
|
|
2472
|
+
if (score >= 50) riskClass = "CRITICAL";
|
|
2473
|
+
else if (score >= 30) riskClass = "HIGH";
|
|
2474
|
+
else if (score >= 15) riskClass = "MODERATE";
|
|
2475
|
+
else riskClass = "LOW";
|
|
2476
|
+
return { riskClass, score, flags, inputs: { previousTokens: prev, rugHistory: rugged, avgTokenLifespanHours: avgLife, freshWalletSurge: freshSurge, devSoldEarlyCount: devSold } };
|
|
2477
|
+
})
|
|
2478
|
+
});
|
|
2479
|
+
api.registerTool({
|
|
2480
|
+
name: "solana_history_export",
|
|
2481
|
+
description: "Export comprehensive historical data for analysis: local decision logs + server-side closed trades + memory entries + strategy evolution history. Supports filtering by agent, time range, decision type, and token. Designed for deep analysis with full lookback depth.",
|
|
2482
|
+
parameters: Type.Object({
|
|
2483
|
+
agentId: Type.Optional(Type.String({ description: "Agent ID to export local logs for. Defaults to configured agent." })),
|
|
2484
|
+
since: Type.Optional(Type.String({ description: "ISO timestamp \u2014 only export entries after this time." })),
|
|
2485
|
+
before: Type.Optional(Type.String({ description: "ISO timestamp \u2014 only export entries before this time." })),
|
|
2486
|
+
decisionType: Type.Optional(Type.String({ description: "Filter local decisions by type (e.g., 'trade_entry', 'trade_exit', 'analysis')." })),
|
|
2487
|
+
token: Type.Optional(Type.String({ description: "Filter decisions and memory by token mint address." })),
|
|
2488
|
+
includeState: Type.Optional(Type.Boolean({ description: "Include agent durable state. Default true." })),
|
|
2489
|
+
includeBulletin: Type.Optional(Type.Boolean({ description: "Include team bulletin entries. Default false." })),
|
|
2490
|
+
includePatterns: Type.Optional(Type.Boolean({ description: "Include named patterns. Default false." })),
|
|
2491
|
+
includeTrades: Type.Optional(Type.Boolean({ description: "Include server-side closed trade history (via /api/trades). Default true." })),
|
|
2492
|
+
includeMemory: Type.Optional(Type.Boolean({ description: "Include server-side memory entries matching filters (via /api/memory/search). Default false." })),
|
|
2493
|
+
includeStrategy: Type.Optional(Type.Boolean({ description: "Include server-side strategy state and weight history (via /api/strategy/state). Default false." })),
|
|
2494
|
+
memoryTags: Type.Optional(Type.String({ description: "Comma-separated memory tags to search (used with includeMemory). Default: 'learning_entry,strategy_evolution,pattern_detection'." })),
|
|
2495
|
+
limit: Type.Optional(Type.Number({ description: "Max decision entries (local logs). Default 50." })),
|
|
2496
|
+
offset: Type.Optional(Type.Number({ description: "Skip first N decision entries. Default 0." })),
|
|
2497
|
+
tradesLimit: Type.Optional(Type.Number({ description: "Max closed trades to fetch. Default 100." })),
|
|
2498
|
+
tradesPage: Type.Optional(Type.Number({ description: "Page number for trade pagination (1-based). Default 1." }))
|
|
2499
|
+
}),
|
|
2500
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2501
|
+
const targetAgentId = sanitizeAgentId(params.agentId ? String(params.agentId) : agentId);
|
|
2502
|
+
const sinceTs = params.since ? new Date(String(params.since)).getTime() : 0;
|
|
2503
|
+
const beforeTs = params.before ? new Date(String(params.before)).getTime() : Infinity;
|
|
2504
|
+
const filterType = params.decisionType ? String(params.decisionType) : null;
|
|
2505
|
+
const filterToken = params.token ? String(params.token) : null;
|
|
2506
|
+
const maxEntries = typeof params.limit === "number" ? params.limit : 50;
|
|
2507
|
+
const skipEntries = typeof params.offset === "number" ? params.offset : 0;
|
|
2508
|
+
const includeState = params.includeState !== false;
|
|
2509
|
+
const shouldFetchTrades = params.includeTrades !== false;
|
|
2510
|
+
const shouldFetchMemory = Boolean(params.includeMemory);
|
|
2511
|
+
const shouldFetchStrategy = Boolean(params.includeStrategy);
|
|
2512
|
+
const logPath = path.join(logsDir, targetAgentId, "decisions.jsonl");
|
|
2513
|
+
let decisions = readJsonlFile(logPath);
|
|
2514
|
+
if (sinceTs > 0) decisions = decisions.filter((d) => new Date(d.ts).getTime() > sinceTs);
|
|
2515
|
+
if (beforeTs < Infinity) decisions = decisions.filter((d) => new Date(d.ts).getTime() < beforeTs);
|
|
2516
|
+
if (filterType) decisions = decisions.filter((d) => d.type === filterType);
|
|
2517
|
+
if (filterToken) decisions = decisions.filter((d) => d.token === filterToken);
|
|
2518
|
+
const totalFiltered = decisions.length;
|
|
2519
|
+
decisions = decisions.slice(skipEntries, skipEntries + maxEntries);
|
|
2520
|
+
const agentResult = {
|
|
2521
|
+
decisions,
|
|
2522
|
+
decisionCount: decisions.length,
|
|
2523
|
+
totalFiltered
|
|
2524
|
+
};
|
|
2525
|
+
if (includeState) {
|
|
2526
|
+
const statePath = path.join(stateDir, `${targetAgentId}.json`);
|
|
2527
|
+
agentResult.state = readJsonFile(statePath);
|
|
2528
|
+
}
|
|
2529
|
+
const exportResult = {
|
|
2530
|
+
agents: { [targetAgentId]: agentResult },
|
|
2531
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2532
|
+
};
|
|
2533
|
+
exportResult.contextSnapshot = readJsonFile(path.join(stateDir, "context-snapshot.json"));
|
|
2534
|
+
if (params.includeBulletin) {
|
|
2535
|
+
let bulletin = readJsonlFile(path.join(sharedLogsDir, "team-bulletin.jsonl"));
|
|
2536
|
+
if (sinceTs > 0) bulletin = bulletin.filter((b) => new Date(b.ts).getTime() > sinceTs);
|
|
2537
|
+
if (beforeTs < Infinity) bulletin = bulletin.filter((b) => new Date(b.ts).getTime() < beforeTs);
|
|
2538
|
+
exportResult.bulletin = bulletin.slice(-maxEntries);
|
|
2539
|
+
}
|
|
2540
|
+
if (params.includePatterns) {
|
|
2541
|
+
exportResult.patterns = readJsonFile(path.join(stateDir, "patterns.json")) || {};
|
|
2542
|
+
}
|
|
2543
|
+
if (shouldFetchTrades) {
|
|
2544
|
+
try {
|
|
2545
|
+
const trLimit = typeof params.tradesLimit === "number" ? params.tradesLimit : 100;
|
|
2546
|
+
const trPage = typeof params.tradesPage === "number" ? params.tradesPage : 1;
|
|
2547
|
+
let tradePath = `/api/trades?walletId=${walletId}&limit=${trLimit}&page=${trPage}`;
|
|
2548
|
+
if (filterToken) tradePath += `&tokenAddress=${filterToken}`;
|
|
2549
|
+
const trades = await get(tradePath);
|
|
2550
|
+
exportResult.closedTrades = trades;
|
|
2551
|
+
} catch (err) {
|
|
2552
|
+
exportResult.closedTrades = { error: err instanceof Error ? err.message : String(err) };
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
if (shouldFetchMemory) {
|
|
2556
|
+
try {
|
|
2557
|
+
const tags = params.memoryTags ? String(params.memoryTags).split(",").map((t) => t.trim()) : ["learning_entry", "strategy_evolution", "pattern_detection"];
|
|
2558
|
+
const memoryResults = [];
|
|
2559
|
+
for (const tag of tags) {
|
|
2560
|
+
try {
|
|
2561
|
+
const entries = await post("/api/memory/search", { query: tag, walletId });
|
|
2562
|
+
memoryResults.push({ tag, entries });
|
|
2563
|
+
} catch {
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
exportResult.memoryEntries = memoryResults;
|
|
2567
|
+
} catch (err) {
|
|
2568
|
+
exportResult.memoryEntries = { error: err instanceof Error ? err.message : String(err) };
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
if (shouldFetchStrategy) {
|
|
2572
|
+
try {
|
|
2573
|
+
const strategyState = await get("/api/strategy/state");
|
|
2574
|
+
exportResult.strategyState = strategyState;
|
|
2575
|
+
} catch (err) {
|
|
2576
|
+
exportResult.strategyState = { error: err instanceof Error ? err.message : String(err) };
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
return exportResult;
|
|
2580
|
+
})
|
|
2581
|
+
});
|
|
2582
|
+
api.registerTool({
|
|
2583
|
+
name: "solana_pattern_store",
|
|
2584
|
+
description: "Read, write, or list named trading patterns. Patterns are shared state used for pattern matching and strategy evolution.",
|
|
2585
|
+
parameters: Type.Object({
|
|
2586
|
+
action: Type.String({ description: "'read', 'write', or 'list'." }),
|
|
2587
|
+
patternId: Type.Optional(Type.String({ description: "Pattern identifier (required for read/write)." })),
|
|
2588
|
+
pattern: Type.Optional(Type.Unknown({ description: "Pattern object to store (required for write). Should include: name, description, conditions, expectedOutcome, confidence, sampleSize, discoveredAt." }))
|
|
2589
|
+
}),
|
|
2590
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2591
|
+
const action = String(params.action);
|
|
2592
|
+
const patternsPath = path.join(stateDir, "patterns.json");
|
|
2593
|
+
const patterns = readJsonFile(patternsPath) || {};
|
|
2594
|
+
if (action === "list") {
|
|
2595
|
+
const ids = Object.keys(patterns);
|
|
2596
|
+
return { patterns: ids.map((id) => ({ id, ...patterns[id] })), count: ids.length };
|
|
2597
|
+
}
|
|
2598
|
+
const patternId = params.patternId ? String(params.patternId) : null;
|
|
2599
|
+
if (!patternId) return { error: "patternId is required for read/write." };
|
|
2600
|
+
if (action === "read") {
|
|
2601
|
+
return patterns[patternId] ? { patternId, ...patterns[patternId] } : { patternId, found: false };
|
|
2602
|
+
}
|
|
2603
|
+
if (action === "write") {
|
|
2604
|
+
if (!params.pattern) return { error: "pattern object is required for write." };
|
|
2605
|
+
patterns[patternId] = { ...params.pattern, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2606
|
+
writeJsonFile(patternsPath, patterns);
|
|
2607
|
+
return { ok: true, patternId, updatedAt: patterns[patternId].updatedAt };
|
|
2608
|
+
}
|
|
2609
|
+
return { error: `Unknown action: ${action}. Use 'read', 'write', or 'list'.` };
|
|
2610
|
+
})
|
|
2611
|
+
});
|
|
2612
|
+
api.registerTool({
|
|
2613
|
+
name: "solana_daily_log",
|
|
2614
|
+
description: "Append an entry to today's daily episodic log (memory/YYYY-MM-DD.md). OpenClaw auto-loads today + yesterday's log into context at every session start \u2014 no tool call needed to read them. Use at session end and after significant events. Auto-prunes logs older than 7 days.",
|
|
2615
|
+
parameters: Type.Object({
|
|
2616
|
+
summary: Type.String({ description: "Session summary or event description to log. Keep concise (1-5 lines)." }),
|
|
2617
|
+
tags: Type.Optional(Type.String({ description: "Comma-separated tags for categorization (e.g., 'trade,regime_shift,session_end')." }))
|
|
2618
|
+
}),
|
|
2619
|
+
execute: wrapExecute(async (_id, params) => {
|
|
2620
|
+
ensureDir(memoryDir);
|
|
2621
|
+
const now = /* @__PURE__ */ new Date();
|
|
2622
|
+
const logPath = getDailyLogPath(now);
|
|
2623
|
+
const timeStr = now.toISOString().slice(11, 19);
|
|
2624
|
+
const tags = params.tags ? ` [${String(params.tags)}]` : "";
|
|
2625
|
+
const entry = `
|
|
2626
|
+
### ${timeStr} \u2014 ${agentId}${tags}
|
|
2627
|
+
|
|
2628
|
+
${String(params.summary)}
|
|
2629
|
+
`;
|
|
2630
|
+
if (!fs.existsSync(logPath)) {
|
|
2631
|
+
const dateStr = now.toISOString().slice(0, 10);
|
|
2632
|
+
const header = `# Daily Log \u2014 ${dateStr}
|
|
2633
|
+
|
|
2634
|
+
> Auto-generated by solana_daily_log. OpenClaw loads today + yesterday into context automatically.
|
|
2635
|
+
`;
|
|
2636
|
+
fs.writeFileSync(logPath, header + entry, "utf-8");
|
|
2637
|
+
} else {
|
|
2638
|
+
fs.appendFileSync(logPath, entry, "utf-8");
|
|
2639
|
+
}
|
|
2640
|
+
pruneDailyLogs(7);
|
|
2641
|
+
return { ok: true, date: now.toISOString().slice(0, 10), time: timeStr, agent: agentId };
|
|
2642
|
+
})
|
|
2643
|
+
});
|
|
2644
|
+
api.registerHook("agent:bootstrap", async (context) => {
|
|
2645
|
+
const bootAgentId = sanitizeAgentId(context.agentId || agentId);
|
|
2646
|
+
if (!context.bootstrapFiles) context.bootstrapFiles = [];
|
|
2647
|
+
try {
|
|
2648
|
+
const stateFile = path.join(stateDir, `${bootAgentId}.json`);
|
|
2649
|
+
const stateData = readJsonFile(stateFile);
|
|
2650
|
+
if (stateData) {
|
|
2651
|
+
context.bootstrapFiles.push({
|
|
2652
|
+
name: `${bootAgentId}-durable-state.json`,
|
|
2653
|
+
path: `state/${bootAgentId}.json`,
|
|
2654
|
+
content: JSON.stringify(stateData, null, 2),
|
|
2655
|
+
source: "solana-trader:state"
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
} catch (err) {
|
|
2659
|
+
api.logger.warn(`[solana-trader] Bootstrap: failed to load state for ${bootAgentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2660
|
+
}
|
|
2661
|
+
try {
|
|
2662
|
+
const logFile = path.join(logsDir, bootAgentId, "decisions.jsonl");
|
|
2663
|
+
const decisions = readJsonlFile(logFile, 50);
|
|
2664
|
+
if (decisions.length > 0) {
|
|
2665
|
+
context.bootstrapFiles.push({
|
|
2666
|
+
name: `${bootAgentId}-decision-log.jsonl`,
|
|
2667
|
+
path: `logs/${bootAgentId}/decisions.jsonl`,
|
|
2668
|
+
content: decisions.map((d) => JSON.stringify(d)).join("\n"),
|
|
2669
|
+
source: "solana-trader:decisions"
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
} catch (err) {
|
|
2673
|
+
api.logger.warn(`[solana-trader] Bootstrap: failed to load decisions for ${bootAgentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2674
|
+
}
|
|
2675
|
+
try {
|
|
2676
|
+
const bulletinFile = path.join(sharedLogsDir, "team-bulletin.jsonl");
|
|
2677
|
+
const allEntries = readJsonlFile(bulletinFile);
|
|
2678
|
+
const windowMs = 6 * 60 * 60 * 1e3;
|
|
2679
|
+
const cutoff = Date.now() - windowMs;
|
|
2680
|
+
const filtered = allEntries.filter((e) => new Date(e.ts).getTime() > cutoff);
|
|
2681
|
+
if (filtered.length > 0) {
|
|
2682
|
+
context.bootstrapFiles.push({
|
|
2683
|
+
name: "team-bulletin.jsonl",
|
|
2684
|
+
path: "logs/shared/team-bulletin.jsonl",
|
|
2685
|
+
content: filtered.map((e) => JSON.stringify(e)).join("\n"),
|
|
2686
|
+
source: "solana-trader:bulletin"
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
} catch (err) {
|
|
2690
|
+
api.logger.warn(`[solana-trader] Bootstrap: failed to load bulletin for ${bootAgentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2691
|
+
}
|
|
2692
|
+
try {
|
|
2693
|
+
const snapshotFile = path.join(stateDir, "context-snapshot.json");
|
|
2694
|
+
const snapshot = readJsonFile(snapshotFile);
|
|
2695
|
+
if (snapshot) {
|
|
2696
|
+
context.bootstrapFiles.push({
|
|
2697
|
+
name: "context-snapshot.json",
|
|
2698
|
+
path: "state/context-snapshot.json",
|
|
2699
|
+
content: JSON.stringify(snapshot, null, 2),
|
|
2700
|
+
source: "solana-trader:snapshot"
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
} catch (err) {
|
|
2704
|
+
api.logger.warn(`[solana-trader] Bootstrap: failed to load snapshot for ${bootAgentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2705
|
+
}
|
|
2706
|
+
let entitlementData = null;
|
|
2707
|
+
try {
|
|
2708
|
+
const liveResult = await get(`/api/entitlements/current?walletId=${walletId}`);
|
|
2709
|
+
if (liveResult && typeof liveResult === "object") {
|
|
2710
|
+
entitlementData = { ...liveResult, source: "live-fetch", cachedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2711
|
+
try {
|
|
2712
|
+
writeJsonFile(path.join(stateDir, "entitlement-cache.json"), entitlementData);
|
|
2713
|
+
} catch (_) {
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
} catch (fetchErr) {
|
|
2717
|
+
api.logger.warn(`[solana-trader] Bootstrap: live entitlement fetch failed for ${bootAgentId}: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
|
|
2718
|
+
}
|
|
2719
|
+
if (!entitlementData) {
|
|
2720
|
+
try {
|
|
2721
|
+
const cached = readJsonFile(path.join(stateDir, "entitlement-cache.json"));
|
|
2722
|
+
if (cached && typeof cached === "object") {
|
|
2723
|
+
entitlementData = { ...cached, source: "cache-fallback" };
|
|
2724
|
+
}
|
|
2725
|
+
} catch (_) {
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
if (!entitlementData) {
|
|
2729
|
+
try {
|
|
2730
|
+
const agentState = readJsonFile(path.join(stateDir, `${bootAgentId}.json`));
|
|
2731
|
+
const s = agentState?.state;
|
|
2732
|
+
if (s && typeof s === "object" && "tier" in s) {
|
|
2733
|
+
entitlementData = { tier: s.tier, maxPositions: s.maxPositions, maxPositionSizeSol: s.maxPositionSizeSol, source: "durable-state-fallback", cachedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2734
|
+
}
|
|
2735
|
+
} catch (_) {
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
if (!entitlementData) {
|
|
2739
|
+
entitlementData = { tier: "starter", maxPositions: 3, maxPositionSizeSol: 0.1, source: "conservative-default", cachedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2740
|
+
api.logger.warn(`[solana-trader] Bootstrap: no entitlement source available for ${bootAgentId}, injecting conservative Starter defaults`);
|
|
2741
|
+
}
|
|
2742
|
+
context.bootstrapFiles.push({
|
|
2743
|
+
name: "active-entitlements.json",
|
|
2744
|
+
path: "state/entitlement-cache.json",
|
|
2745
|
+
content: JSON.stringify(entitlementData, null, 2),
|
|
2746
|
+
source: "solana-trader:entitlements"
|
|
2747
|
+
});
|
|
2748
|
+
api.logger.info(`[solana-trader] Bootstrap: injected ${context.bootstrapFiles.length} files for agent ${bootAgentId}`);
|
|
2749
|
+
});
|
|
2750
|
+
api.registerHook("memory:flush", async (context) => {
|
|
2751
|
+
const flushAgentId = sanitizeAgentId(context.agentId || agentId);
|
|
2752
|
+
api.logger.info(`[solana-trader] Memory flush triggered for agent ${flushAgentId}`);
|
|
2753
|
+
try {
|
|
2754
|
+
const stateFile = path.join(stateDir, `${flushAgentId}.json`);
|
|
2755
|
+
const stateData = readJsonFile(stateFile);
|
|
2756
|
+
if (stateData?.state) {
|
|
2757
|
+
writeMemoryMd(flushAgentId, stateData.state);
|
|
2758
|
+
api.logger.info(`[solana-trader] Memory flush: MEMORY.md updated from persisted state for ${flushAgentId}`);
|
|
2759
|
+
} else {
|
|
2760
|
+
api.logger.info(`[solana-trader] Memory flush: no persisted state found for ${flushAgentId} \u2014 MEMORY.md not updated`);
|
|
2761
|
+
}
|
|
2762
|
+
} catch (err) {
|
|
2763
|
+
api.logger.warn(`[solana-trader] Memory flush: failed to write MEMORY.md for ${flushAgentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2764
|
+
}
|
|
2765
|
+
try {
|
|
2766
|
+
const now = /* @__PURE__ */ new Date();
|
|
2767
|
+
ensureDir(memoryDir);
|
|
2768
|
+
const logPath = getDailyLogPath(now);
|
|
2769
|
+
const timeStr = now.toISOString().slice(11, 19);
|
|
2770
|
+
const entry = `
|
|
2771
|
+
### ${timeStr} \u2014 ${flushAgentId} [memory_flush]
|
|
2772
|
+
|
|
2773
|
+
Context compaction triggered. MEMORY.md synced from last persisted state. Decision log entries are server-persisted (no local buffer to flush).
|
|
2774
|
+
`;
|
|
2775
|
+
if (!fs.existsSync(logPath)) {
|
|
2776
|
+
const dateStr = now.toISOString().slice(0, 10);
|
|
2777
|
+
const header = `# Daily Log \u2014 ${dateStr}
|
|
2778
|
+
|
|
2779
|
+
> Auto-generated by solana_daily_log. OpenClaw loads today + yesterday into context automatically.
|
|
2780
|
+
`;
|
|
2781
|
+
fs.writeFileSync(logPath, header + entry, "utf-8");
|
|
2782
|
+
} else {
|
|
2783
|
+
fs.appendFileSync(logPath, entry, "utf-8");
|
|
2784
|
+
}
|
|
2785
|
+
} catch (err) {
|
|
2786
|
+
api.logger.warn(`[solana-trader] Memory flush: failed to write daily log for ${flushAgentId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
api.registerService({
|
|
2790
|
+
id: "solana-trader-session",
|
|
2791
|
+
start: async () => {
|
|
2792
|
+
try {
|
|
2793
|
+
await sessionManager.initialize();
|
|
2794
|
+
const info = sessionManager.getSessionInfo();
|
|
2795
|
+
api.logger.info(
|
|
2796
|
+
`[solana-trader] Session active. Tier: ${info.tier}, Scopes: ${info.scopes.join(", ")}`
|
|
2797
|
+
);
|
|
2798
|
+
} catch (err) {
|
|
2799
|
+
api.logger.error(
|
|
2800
|
+
`[solana-trader] Session initialization failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2801
|
+
);
|
|
2802
|
+
api.logger.error(
|
|
2803
|
+
"[solana-trader] Trading tools will fail until session is established. User should run on this machine: traderclaw login (after logout) or traderclaw setup / traderclaw signup for a new account. Wallet proof uses local signing only \u2014 private key never leaves this system."
|
|
2804
|
+
);
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
try {
|
|
2808
|
+
const healthz = await orchestratorRequest({
|
|
2809
|
+
baseUrl: orchestratorUrl,
|
|
2810
|
+
method: "GET",
|
|
2811
|
+
path: "/healthz",
|
|
2812
|
+
timeout: 5e3,
|
|
2813
|
+
accessToken: await sessionManager.getAccessToken()
|
|
2814
|
+
});
|
|
2815
|
+
api.logger.info(
|
|
2816
|
+
`[solana-trader] Orchestrator healthz OK at ${orchestratorUrl}`
|
|
2817
|
+
);
|
|
2818
|
+
if (healthz && typeof healthz === "object") {
|
|
2819
|
+
const h = healthz;
|
|
2820
|
+
api.logger.info(
|
|
2821
|
+
`[solana-trader] Mode: ${h.executionMode || "unknown"}, Upstream: ${h.upstreamConfigured ? "yes" : "no"}`
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
} catch (err) {
|
|
2825
|
+
api.logger.warn(
|
|
2826
|
+
`[solana-trader] /healthz unreachable at ${orchestratorUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
try {
|
|
2830
|
+
const status = await get("/api/system/status");
|
|
2831
|
+
api.logger.info(
|
|
2832
|
+
`[solana-trader] Connected to orchestrator (walletId: ${walletId})`
|
|
2833
|
+
);
|
|
2834
|
+
if (status && typeof status === "object") {
|
|
2835
|
+
api.logger.info(`[solana-trader] System status: ${JSON.stringify(status)}`);
|
|
2836
|
+
}
|
|
2837
|
+
} catch (err) {
|
|
2838
|
+
api.logger.warn(
|
|
2839
|
+
`[solana-trader] /api/system/status unreachable: ${err instanceof Error ? err.message : String(err)}`
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
try {
|
|
2843
|
+
const startupGate = await runStartupGate({ autoFixGateway: true, force: true });
|
|
2844
|
+
api.logger.info(
|
|
2845
|
+
`[solana-trader] Startup gate completed: ok=${startupGate.ok}, passed=${startupGate.summary.passed}, failed=${startupGate.summary.failed}`
|
|
2846
|
+
);
|
|
2847
|
+
if (!startupGate.ok) {
|
|
2848
|
+
api.logger.warn(
|
|
2849
|
+
`[solana-trader] Startup gate failures: ${JSON.stringify(startupGate.steps.filter((step) => !step.ok))}`
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
} catch (err) {
|
|
2853
|
+
api.logger.warn(
|
|
2854
|
+
`[solana-trader] Startup gate run failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
try {
|
|
2858
|
+
const probe = await runForwardProbe({
|
|
2859
|
+
agentId: config.agentId || "main",
|
|
2860
|
+
source: "service_startup"
|
|
2861
|
+
});
|
|
2862
|
+
api.logger.info(
|
|
2863
|
+
`[solana-trader] Forward probe result: ${JSON.stringify(probe)}`
|
|
2864
|
+
);
|
|
2865
|
+
} catch (err) {
|
|
2866
|
+
api.logger.warn(
|
|
2867
|
+
`[solana-trader] Forward probe failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
registerXReadTools(api, Type, config.xConfig, config.agentId || "main", "[solana-trader]");
|
|
2873
|
+
registerWebFetchTool(api, Type, "[solana-trader]");
|
|
2874
|
+
const xToolCount = config.xConfig?.ok ? 3 : 0;
|
|
2875
|
+
api.logger.info(
|
|
2876
|
+
`[solana-trader] Registered ${67 + xToolCount} tools (67 trading + ${xToolCount} X/Twitter read) for walletId ${walletId} (session auth mode)`
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
};
|
|
2880
|
+
var index_default = solanaTraderPlugin;
|
|
2881
|
+
export {
|
|
2882
|
+
index_default as default
|
|
2883
|
+
};
|