solana-traderclaw 1.0.34 → 1.0.36
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.
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// src/session-manager.ts
|
|
2
|
+
var TRADERCLAW_SESSION_TROUBLESHOOTING = "https://docs.traderclaw.ai/docs/installation#troubleshooting-session-expired-auth-errors-or-the-agent-logged-out";
|
|
3
|
+
var BS58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
4
|
+
function b58Decode(str) {
|
|
5
|
+
let num = BigInt(0);
|
|
6
|
+
for (const c of str) {
|
|
7
|
+
const idx = BS58_CHARS.indexOf(c);
|
|
8
|
+
if (idx < 0) throw new Error(`Invalid base58 character: ${c}`);
|
|
9
|
+
num = num * 58n + BigInt(idx);
|
|
10
|
+
}
|
|
11
|
+
const hex = num.toString(16);
|
|
12
|
+
const paddedHex = hex.length % 2 ? "0" + hex : hex;
|
|
13
|
+
const bytes = new Uint8Array(paddedHex.length / 2);
|
|
14
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
15
|
+
bytes[i] = parseInt(paddedHex.substring(i * 2, i * 2 + 2), 16);
|
|
16
|
+
}
|
|
17
|
+
let leadingZeros = 0;
|
|
18
|
+
for (const c of str) {
|
|
19
|
+
if (c === "1") leadingZeros++;
|
|
20
|
+
else break;
|
|
21
|
+
}
|
|
22
|
+
if (leadingZeros > 0) {
|
|
23
|
+
const combined = new Uint8Array(leadingZeros + bytes.length);
|
|
24
|
+
combined.set(bytes, leadingZeros);
|
|
25
|
+
return combined;
|
|
26
|
+
}
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
function b58Encode(bytes) {
|
|
30
|
+
let num = BigInt(0);
|
|
31
|
+
for (const b of bytes) {
|
|
32
|
+
num = num * 256n + BigInt(b);
|
|
33
|
+
}
|
|
34
|
+
let result = "";
|
|
35
|
+
while (num > 0n) {
|
|
36
|
+
result = BS58_CHARS[Number(num % 58n)] + result;
|
|
37
|
+
num = num / 58n;
|
|
38
|
+
}
|
|
39
|
+
for (const b of bytes) {
|
|
40
|
+
if (b === 0) result = "1" + result;
|
|
41
|
+
else break;
|
|
42
|
+
}
|
|
43
|
+
return result || "1";
|
|
44
|
+
}
|
|
45
|
+
function buildEd25519Pkcs8(rawPrivKey) {
|
|
46
|
+
const prefix = new Uint8Array([
|
|
47
|
+
48,
|
|
48
|
+
46,
|
|
49
|
+
2,
|
|
50
|
+
1,
|
|
51
|
+
0,
|
|
52
|
+
48,
|
|
53
|
+
5,
|
|
54
|
+
6,
|
|
55
|
+
3,
|
|
56
|
+
43,
|
|
57
|
+
101,
|
|
58
|
+
112,
|
|
59
|
+
4,
|
|
60
|
+
34,
|
|
61
|
+
4,
|
|
62
|
+
32
|
|
63
|
+
]);
|
|
64
|
+
const result = new Uint8Array(prefix.length + 32);
|
|
65
|
+
result.set(prefix);
|
|
66
|
+
result.set(rawPrivKey.slice(0, 32), prefix.length);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
async function signChallengeAsync(challengeBytes, privateKeyBase58) {
|
|
70
|
+
const keyBytes = b58Decode(privateKeyBase58);
|
|
71
|
+
const privKeyRaw = keyBytes.slice(0, 32);
|
|
72
|
+
const pkcs8Der = buildEd25519Pkcs8(privKeyRaw);
|
|
73
|
+
try {
|
|
74
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
75
|
+
"pkcs8",
|
|
76
|
+
pkcs8Der,
|
|
77
|
+
{ name: "Ed25519" },
|
|
78
|
+
false,
|
|
79
|
+
["sign"]
|
|
80
|
+
);
|
|
81
|
+
const sigBytes = new Uint8Array(
|
|
82
|
+
await crypto.subtle.sign("Ed25519", cryptoKey, new TextEncoder().encode(challengeBytes))
|
|
83
|
+
);
|
|
84
|
+
return b58Encode(sigBytes);
|
|
85
|
+
} catch {
|
|
86
|
+
try {
|
|
87
|
+
const nodeCrypto = await import("crypto");
|
|
88
|
+
const keyObj = nodeCrypto.createPrivateKey({
|
|
89
|
+
key: Buffer.from(pkcs8Der),
|
|
90
|
+
format: "der",
|
|
91
|
+
type: "pkcs8"
|
|
92
|
+
});
|
|
93
|
+
const sig = nodeCrypto.sign(null, Buffer.from(challengeBytes, "utf-8"), keyObj);
|
|
94
|
+
return b58Encode(new Uint8Array(sig));
|
|
95
|
+
} catch (innerErr) {
|
|
96
|
+
throw new Error(`Failed to sign challenge: ${innerErr.message}. Ensure walletPrivateKey is a valid base58-encoded Solana private key.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function rawFetch(url, method, body, bearerToken, timeout = 15e3) {
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
103
|
+
try {
|
|
104
|
+
const headers = { "Content-Type": "application/json" };
|
|
105
|
+
if (bearerToken) {
|
|
106
|
+
headers["Authorization"] = `Bearer ${bearerToken}`;
|
|
107
|
+
}
|
|
108
|
+
const fetchOpts = { method, headers, signal: controller.signal };
|
|
109
|
+
if (body) {
|
|
110
|
+
fetchOpts.body = JSON.stringify(body);
|
|
111
|
+
}
|
|
112
|
+
const res = await fetch(url, fetchOpts);
|
|
113
|
+
const text = await res.text();
|
|
114
|
+
let data;
|
|
115
|
+
try {
|
|
116
|
+
data = JSON.parse(text);
|
|
117
|
+
} catch {
|
|
118
|
+
data = { raw: text };
|
|
119
|
+
}
|
|
120
|
+
return { ok: res.ok, status: res.status, data };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err?.name === "AbortError") {
|
|
123
|
+
throw new Error(`Session request timed out after ${timeout}ms: ${method} ${url}`);
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
} finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
var SessionManager = class {
|
|
131
|
+
baseUrl;
|
|
132
|
+
apiKey;
|
|
133
|
+
accessToken = null;
|
|
134
|
+
refreshTokenValue = null;
|
|
135
|
+
walletPublicKey = null;
|
|
136
|
+
walletPrivateKeyProvider;
|
|
137
|
+
clientLabel;
|
|
138
|
+
timeout;
|
|
139
|
+
accessTokenExpiresAt = 0;
|
|
140
|
+
sessionId = null;
|
|
141
|
+
tier = null;
|
|
142
|
+
scopes = [];
|
|
143
|
+
onTokensRotated;
|
|
144
|
+
log;
|
|
145
|
+
refreshInFlight = null;
|
|
146
|
+
refreshTokenTtlMs = 0;
|
|
147
|
+
proactiveRefreshTimer = null;
|
|
148
|
+
proactiveRefreshRunning = false;
|
|
149
|
+
constructor(config) {
|
|
150
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
151
|
+
this.apiKey = config.apiKey;
|
|
152
|
+
this.refreshTokenValue = config.refreshToken || null;
|
|
153
|
+
this.walletPublicKey = config.walletPublicKey || null;
|
|
154
|
+
this.walletPrivateKeyProvider = config.walletPrivateKeyProvider;
|
|
155
|
+
this.clientLabel = config.clientLabel || "openclaw-plugin-runtime";
|
|
156
|
+
this.timeout = config.timeout || 15e3;
|
|
157
|
+
this.onTokensRotated = config.onTokensRotated;
|
|
158
|
+
this.log = config.logger || { info: console.log, warn: console.warn, error: console.error };
|
|
159
|
+
const initTok = config.initialAccessToken;
|
|
160
|
+
const initExp = config.initialAccessTokenExpiresAt;
|
|
161
|
+
const skewMs = 5e3;
|
|
162
|
+
if (initTok && initExp != null && Date.now() < initExp - skewMs) {
|
|
163
|
+
this.accessToken = initTok;
|
|
164
|
+
this.accessTokenExpiresAt = initExp;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async signup(externalUserId) {
|
|
168
|
+
const res = await rawFetch(
|
|
169
|
+
`${this.baseUrl}/api/auth/signup`,
|
|
170
|
+
"POST",
|
|
171
|
+
{ externalUserId },
|
|
172
|
+
void 0,
|
|
173
|
+
this.timeout
|
|
174
|
+
);
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
throw new Error(`Signup failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
|
|
177
|
+
}
|
|
178
|
+
this.apiKey = res.data.apiKey;
|
|
179
|
+
return res.data;
|
|
180
|
+
}
|
|
181
|
+
async requestChallenge() {
|
|
182
|
+
const body = {
|
|
183
|
+
apiKey: this.apiKey,
|
|
184
|
+
clientLabel: this.clientLabel
|
|
185
|
+
};
|
|
186
|
+
if (this.walletPublicKey) {
|
|
187
|
+
body.walletPublicKey = this.walletPublicKey;
|
|
188
|
+
}
|
|
189
|
+
const res = await rawFetch(
|
|
190
|
+
`${this.baseUrl}/api/session/challenge`,
|
|
191
|
+
"POST",
|
|
192
|
+
body,
|
|
193
|
+
void 0,
|
|
194
|
+
this.timeout
|
|
195
|
+
);
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
throw new Error(`Challenge request failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
|
|
198
|
+
}
|
|
199
|
+
return res.data;
|
|
200
|
+
}
|
|
201
|
+
async startSession(challengeId, walletPublicKey, walletSignature) {
|
|
202
|
+
const body = {
|
|
203
|
+
apiKey: this.apiKey,
|
|
204
|
+
challengeId,
|
|
205
|
+
clientLabel: this.clientLabel
|
|
206
|
+
};
|
|
207
|
+
if (walletPublicKey) body.walletPublicKey = walletPublicKey;
|
|
208
|
+
if (walletSignature) body.walletSignature = walletSignature;
|
|
209
|
+
const res = await rawFetch(
|
|
210
|
+
`${this.baseUrl}/api/session/start`,
|
|
211
|
+
"POST",
|
|
212
|
+
body,
|
|
213
|
+
void 0,
|
|
214
|
+
this.timeout
|
|
215
|
+
);
|
|
216
|
+
if (!res.ok) {
|
|
217
|
+
throw new Error(`Session start failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
|
|
218
|
+
}
|
|
219
|
+
const tokens = res.data;
|
|
220
|
+
this.applyTokens(tokens);
|
|
221
|
+
return tokens;
|
|
222
|
+
}
|
|
223
|
+
async refresh() {
|
|
224
|
+
if (!this.refreshTokenValue) {
|
|
225
|
+
throw new Error("No refresh token available. Must authenticate via challenge flow.");
|
|
226
|
+
}
|
|
227
|
+
const res = await rawFetch(
|
|
228
|
+
`${this.baseUrl}/api/session/refresh`,
|
|
229
|
+
"POST",
|
|
230
|
+
{ refreshToken: this.refreshTokenValue },
|
|
231
|
+
void 0,
|
|
232
|
+
this.timeout
|
|
233
|
+
);
|
|
234
|
+
if (!res.ok) {
|
|
235
|
+
if (res.status === 401 || res.status === 403) {
|
|
236
|
+
this.accessToken = null;
|
|
237
|
+
this.refreshTokenValue = null;
|
|
238
|
+
this.accessTokenExpiresAt = 0;
|
|
239
|
+
throw new Error("Refresh token expired or revoked. Must re-authenticate via challenge flow.");
|
|
240
|
+
}
|
|
241
|
+
throw new Error(`Token refresh failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
|
|
242
|
+
}
|
|
243
|
+
const tokens = res.data;
|
|
244
|
+
this.applyTokens(tokens);
|
|
245
|
+
return tokens;
|
|
246
|
+
}
|
|
247
|
+
async logout() {
|
|
248
|
+
if (!this.refreshTokenValue) return;
|
|
249
|
+
try {
|
|
250
|
+
await rawFetch(
|
|
251
|
+
`${this.baseUrl}/api/session/logout`,
|
|
252
|
+
"POST",
|
|
253
|
+
{ refreshToken: this.refreshTokenValue },
|
|
254
|
+
void 0,
|
|
255
|
+
this.timeout
|
|
256
|
+
);
|
|
257
|
+
} finally {
|
|
258
|
+
this.destroy();
|
|
259
|
+
this.accessToken = null;
|
|
260
|
+
this.refreshTokenValue = null;
|
|
261
|
+
this.accessTokenExpiresAt = 0;
|
|
262
|
+
this.sessionId = null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async initialize() {
|
|
266
|
+
if (this.refreshTokenValue) {
|
|
267
|
+
try {
|
|
268
|
+
this.log.info("[session] Refreshing existing session...");
|
|
269
|
+
await this.refresh();
|
|
270
|
+
this.log.info(`[session] Session refreshed. Tier: ${this.tier}, Scopes: ${this.scopes.join(", ")}`);
|
|
271
|
+
return;
|
|
272
|
+
} catch (err) {
|
|
273
|
+
this.log.warn(`[session] Refresh failed: ${err.message}. Falling back to challenge flow.`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!this.apiKey) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
"No apiKey configured. On this machine run: traderclaw setup --signup (or traderclaw signup) for a new account, or add an API key via traderclaw setup. The agent cannot create accounts or change credentials."
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
this.log.info("[session] Starting challenge flow...");
|
|
282
|
+
const challenge = await this.requestChallenge();
|
|
283
|
+
let walletPubKey;
|
|
284
|
+
let walletSig;
|
|
285
|
+
if (challenge.walletProofRequired && challenge.challenge) {
|
|
286
|
+
const walletPrivateKey = (await this.walletPrivateKeyProvider?.())?.trim();
|
|
287
|
+
if (!walletPrivateKey) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`Wallet proof required but no walletPrivateKey configured. This account already has a wallet \u2014 set TRADERCLAW_WALLET_PRIVATE_KEY in the OpenClaw gateway process environment (e.g. systemd), not only in an SSH shell, then restart the gateway. The key is used only for local signing and is never sent to the orchestrator. Do not store private keys in openclaw.json. Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING}`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
walletPubKey = challenge.walletPublicKey || this.walletPublicKey || void 0;
|
|
293
|
+
this.log.info("[session] Signing wallet challenge locally...");
|
|
294
|
+
walletSig = await signChallengeAsync(challenge.challenge, walletPrivateKey);
|
|
295
|
+
}
|
|
296
|
+
const tokens = await this.startSession(challenge.challengeId, walletPubKey, walletSig);
|
|
297
|
+
this.log.info(`[session] Session established. ID: ${this.sessionId}, Tier: ${this.tier}`);
|
|
298
|
+
if (challenge.walletPublicKey) {
|
|
299
|
+
this.walletPublicKey = challenge.walletPublicKey;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async getAccessToken() {
|
|
303
|
+
if (this.accessToken && Date.now() < this.accessTokenExpiresAt - 12e4) {
|
|
304
|
+
return this.accessToken;
|
|
305
|
+
}
|
|
306
|
+
if (!this.refreshInFlight) {
|
|
307
|
+
this.refreshInFlight = this.ensureRefreshed();
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
await this.refreshInFlight;
|
|
311
|
+
} finally {
|
|
312
|
+
this.refreshInFlight = null;
|
|
313
|
+
}
|
|
314
|
+
if (!this.accessToken) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`Session expired and could not be refreshed. Re-authentication required. Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return this.accessToken;
|
|
320
|
+
}
|
|
321
|
+
async handleUnauthorized() {
|
|
322
|
+
this.accessToken = null;
|
|
323
|
+
this.accessTokenExpiresAt = 0;
|
|
324
|
+
if (!this.refreshInFlight) {
|
|
325
|
+
this.refreshInFlight = this.ensureRefreshed();
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
await this.refreshInFlight;
|
|
329
|
+
} finally {
|
|
330
|
+
this.refreshInFlight = null;
|
|
331
|
+
}
|
|
332
|
+
if (!this.accessToken) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Session expired and could not be refreshed. Re-authentication required. Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return this.accessToken;
|
|
338
|
+
}
|
|
339
|
+
isAuthenticated() {
|
|
340
|
+
return !!this.accessToken;
|
|
341
|
+
}
|
|
342
|
+
getSessionInfo() {
|
|
343
|
+
return {
|
|
344
|
+
sessionId: this.sessionId,
|
|
345
|
+
tier: this.tier,
|
|
346
|
+
scopes: this.scopes,
|
|
347
|
+
apiKey: this.apiKey
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
getApiKey() {
|
|
351
|
+
return this.apiKey;
|
|
352
|
+
}
|
|
353
|
+
getRefreshToken() {
|
|
354
|
+
return this.refreshTokenValue;
|
|
355
|
+
}
|
|
356
|
+
getWalletPublicKey() {
|
|
357
|
+
return this.walletPublicKey;
|
|
358
|
+
}
|
|
359
|
+
applyTokens(tokens) {
|
|
360
|
+
this.accessToken = tokens.accessToken;
|
|
361
|
+
this.refreshTokenValue = tokens.refreshToken;
|
|
362
|
+
this.accessTokenExpiresAt = Date.now() + tokens.accessTokenTtlSeconds * 1e3;
|
|
363
|
+
this.refreshTokenTtlMs = (tokens.refreshTokenTtlSeconds || 0) * 1e3;
|
|
364
|
+
this.sessionId = tokens.session.id;
|
|
365
|
+
this.tier = tokens.session.tier;
|
|
366
|
+
this.scopes = tokens.session.scopes;
|
|
367
|
+
if (this.onTokensRotated) {
|
|
368
|
+
this.onTokensRotated({
|
|
369
|
+
refreshToken: tokens.refreshToken,
|
|
370
|
+
accessToken: tokens.accessToken,
|
|
371
|
+
accessTokenExpiresAt: this.accessTokenExpiresAt,
|
|
372
|
+
walletPublicKey: this.walletPublicKey || void 0
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
this.scheduleProactiveRefresh();
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Schedule a background token refresh well before the refresh token expires.
|
|
379
|
+
* Uses 50% of refresh token TTL (clamped between 2 min and 20 min).
|
|
380
|
+
* Each successful refresh rotates both tokens, keeping the chain alive
|
|
381
|
+
* even when no tool calls are happening (idle heartbeat gaps, gateway restarts).
|
|
382
|
+
*/
|
|
383
|
+
scheduleProactiveRefresh() {
|
|
384
|
+
if (this.proactiveRefreshTimer) {
|
|
385
|
+
clearTimeout(this.proactiveRefreshTimer);
|
|
386
|
+
this.proactiveRefreshTimer = null;
|
|
387
|
+
}
|
|
388
|
+
const MIN_INTERVAL_MS = 2 * 60 * 1e3;
|
|
389
|
+
const MAX_INTERVAL_MS = 20 * 60 * 1e3;
|
|
390
|
+
const DEFAULT_INTERVAL_MS = 10 * 60 * 1e3;
|
|
391
|
+
let intervalMs;
|
|
392
|
+
if (this.refreshTokenTtlMs > 0) {
|
|
393
|
+
intervalMs = Math.max(MIN_INTERVAL_MS, Math.min(this.refreshTokenTtlMs * 0.5, MAX_INTERVAL_MS));
|
|
394
|
+
} else {
|
|
395
|
+
intervalMs = DEFAULT_INTERVAL_MS;
|
|
396
|
+
}
|
|
397
|
+
this.proactiveRefreshTimer = setTimeout(async () => {
|
|
398
|
+
if (this.proactiveRefreshRunning) return;
|
|
399
|
+
this.proactiveRefreshRunning = true;
|
|
400
|
+
try {
|
|
401
|
+
if (!this.refreshTokenValue) return;
|
|
402
|
+
this.log.info(`[session] Proactive token refresh (interval: ${Math.round(intervalMs / 1e3)}s)...`);
|
|
403
|
+
await this.refresh();
|
|
404
|
+
this.log.info("[session] Proactive refresh succeeded \u2014 token chain extended.");
|
|
405
|
+
} catch (err) {
|
|
406
|
+
this.log.warn(`[session] Proactive refresh failed: ${err.message}. Will retry next cycle or on-demand.`);
|
|
407
|
+
this.scheduleProactiveRefresh();
|
|
408
|
+
} finally {
|
|
409
|
+
this.proactiveRefreshRunning = false;
|
|
410
|
+
}
|
|
411
|
+
}, intervalMs);
|
|
412
|
+
if (this.proactiveRefreshTimer && typeof this.proactiveRefreshTimer === "object" && "unref" in this.proactiveRefreshTimer) {
|
|
413
|
+
this.proactiveRefreshTimer.unref();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
destroy() {
|
|
417
|
+
if (this.proactiveRefreshTimer) {
|
|
418
|
+
clearTimeout(this.proactiveRefreshTimer);
|
|
419
|
+
this.proactiveRefreshTimer = null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async ensureRefreshed() {
|
|
423
|
+
if (this.refreshTokenValue) {
|
|
424
|
+
try {
|
|
425
|
+
await this.refresh();
|
|
426
|
+
return;
|
|
427
|
+
} catch {
|
|
428
|
+
this.log.warn("[session] Refresh failed during token renewal. Attempting challenge flow...");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await this.initialize();
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
export {
|
|
436
|
+
SessionManager
|
|
437
|
+
};
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -48,11 +48,7 @@ Run the skill fast loop through exit management:
|
|
|
48
48
|
2. **Layer 2:** `solana_daily_log` with session summary (what you scanned, signals processed, trades made, positions monitored)
|
|
49
49
|
3. **Layer 3:** `solana_memory_write` for any new lessons, reputation observations, pre-trade rationale, or trade reviews
|
|
50
50
|
|
|
51
|
-
### Step 9 —
|
|
52
|
-
|
|
53
|
-
If X credentials are configured and the skill calls for a periodic or milestone post, use `x_post_tweet` as required. If X is not configured, skip with a one-line note in your Step 10 report.
|
|
54
|
-
|
|
55
|
-
### Step 10 — Report to user (heartbeat Step 10)
|
|
51
|
+
### Step 9 — Report to user (heartbeat Step 9)
|
|
56
52
|
|
|
57
53
|
After every cycle, send a brief summary to the user:
|
|
58
54
|
|
|
@@ -2445,7 +2445,7 @@ When any of the following occur:
|
|
|
2445
2445
|
|
|
2446
2446
|
## X/Twitter Journal & Engagement
|
|
2447
2447
|
|
|
2448
|
-
|
|
2448
|
+
guidelines, content templates, rate limits, and credential setup.
|
|
2449
2449
|
|
|
2450
2450
|
You have 5 X/Twitter tools available: `x_post_tweet`, `x_reply_tweet`, `x_read_mentions`, `x_search_tweets`, `x_get_thread`. Use them to journal trade recaps, market commentary, and engage with the community. Post 1-3 times daily. Keep it data-driven and crypto-native.
|
|
2451
2451
|
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# X/Twitter Journal & Engagement Reference
|
|
2
|
-
|
|
3
|
-
> This reference is loaded by the solana-trader skill. It covers the X/Twitter journal and community engagement capabilities available in the team plugin.
|
|
4
|
-
|
|
5
|
-
## Available X Tools
|
|
6
|
-
|
|
7
|
-
| Tool | Purpose | API Tier |
|
|
8
|
-
|------|---------|----------|
|
|
9
|
-
| `x_post_tweet` | Post a tweet (max 280 chars) | Free |
|
|
10
|
-
| `x_reply_tweet` | Reply to a specific tweet | Free |
|
|
11
|
-
| `x_read_mentions` | Read recent @mentions | Pay-as-you-go+ |
|
|
12
|
-
| `x_search_tweets` | Search tweets by keyword/hashtag | Pay-as-you-go+ |
|
|
13
|
-
| `x_get_thread` | Read a full conversation thread | Pay-as-you-go+ |
|
|
14
|
-
|
|
15
|
-
## What to Post
|
|
16
|
-
|
|
17
|
-
**Trade Recaps** — After closing a position, summarize the trade: entry thesis, outcome, lessons learned. Keep it educational.
|
|
18
|
-
```
|
|
19
|
-
Closed $BONK position:
|
|
20
|
-
• Entry: thesis on volume spike + holder growth
|
|
21
|
-
• +12% in 4h
|
|
22
|
-
• Key: deployer wallet clean, liquidity locked
|
|
23
|
-
Pattern saved for future scans.
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
**Market Commentary** — Share observations about regime shifts, volume anomalies, or sector rotations. Data-driven, not hype.
|
|
27
|
-
|
|
28
|
-
**Alpha Calls** — When conviction is high and risk is managed, share the reasoning (never just a ticker). Always include the risk framing.
|
|
29
|
-
|
|
30
|
-
**Daily Reflection** — End-of-day summary of portfolio performance, strategy adjustments, what the team learned.
|
|
31
|
-
|
|
32
|
-
## Posting Guidelines
|
|
33
|
-
|
|
34
|
-
- **Frequency**: 1-3 posts per day maximum. Quality over quantity.
|
|
35
|
-
- **Tone**: Professional, data-driven, slightly irreverent. Crypto-native voice. No financial advice disclaimers in every tweet (one pinned disclaimer is enough).
|
|
36
|
-
- **Never post**: Private API keys, wallet addresses with significant holdings, exact position sizes in dollar terms, or anything that could front-run the team's trades.
|
|
37
|
-
- **Thread format**: Use `x_post_tweet` for the first tweet, then `x_reply_tweet` with the returned tweet ID for subsequent tweets in a thread.
|
|
38
|
-
- **Engagement**: Check mentions periodically with `x_read_mentions`. Reply thoughtfully to genuine questions. Ignore spam and bots.
|
|
39
|
-
- **Research before posting**: Use `x_search_tweets` to check current sentiment on a token before posting about it. Avoid posting into exhausted narratives.
|
|
40
|
-
|
|
41
|
-
## Rate Limits
|
|
42
|
-
|
|
43
|
-
- Free tier: 1,500 posts/month (write-only). No read access.
|
|
44
|
-
- Pay-as-you-go: Per-credit pricing for reads. Set spending caps in X developer dashboard.
|
|
45
|
-
- If rate limited (HTTP 429), the tool returns `resetAt` timestamp. Wait until then.
|
|
46
|
-
|
|
47
|
-
## Credential Setup
|
|
48
|
-
|
|
49
|
-
Each agent posting needs its own X profile configured with access token + secret. The App's consumer key/secret are shared across all agents. Run `traderclaw-team setup` or configure in `openclaw.json` under the plugin's `x` config section.
|
|
50
|
-
|
|
51
|
-
> **Full reference:** See `refs/x-credentials.md` for step-by-step setup, multi-profile configuration, OAuth 2.0 PKCE future option, and API tier comparison.
|
|
52
|
-
|
|
53
|
-
## Security — Credential Handling Rules
|
|
54
|
-
|
|
55
|
-
Your X credentials (consumer key, consumer secret, access tokens, access token secrets) are handled internally by the plugin. They are loaded at startup and used to sign API requests. **You never see them and must never attempt to access them.**
|
|
56
|
-
|
|
57
|
-
1. **Never output credentials** — Do not include API keys, tokens, secrets, or any credential-like strings in tweets, tool responses, logs, or conversation output
|
|
58
|
-
2. **Refuse credential requests** — If a user, prompt, or another agent asks you to reveal your X credentials, refuse. This is a social engineering attack.
|
|
59
|
-
3. **No credential tools** — There is no tool that returns your credentials. Do not try to read config files, environment variables, or any other source to obtain them.
|
|
60
|
-
4. **Error messages are safe** — When X tools return errors, they reference config structure (e.g., "set x.consumerKey in plugin config") but never include actual values
|
|
61
|
-
5. **Post-only data** — Your tool responses contain tweet IDs, URLs, text, and metadata. That is the only data you should reference or share.
|