solana-traderclaw 1.0.48 → 1.0.49
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/dist/index.js +100 -5
- package/lib/x-client.mjs +70 -1
- package/lib/x-tools.mjs +38 -2
- package/openclaw.plugin.json +17 -0
- package/package.json +1 -1
- package/skills/solana-trader/SKILL.md +5 -1
- package/skills/solana-trader/refs/x-credentials.md +165 -0
- package/skills/solana-trader/refs/x-journal.md +62 -0
package/dist/index.js
CHANGED
|
@@ -125,9 +125,10 @@ async function xApiFetch(method, endpoint, credentials, { body = null, queryPara
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
if (response.status === 403) {
|
|
128
|
+
const isWrite = method === "POST" || method === "PUT" || method === "DELETE";
|
|
128
129
|
return {
|
|
129
130
|
ok: false,
|
|
130
|
-
error: "Forbidden (403). This read endpoint requires a paid X API tier (pay-as-you-go or Basic).",
|
|
131
|
+
error: isWrite ? "Forbidden (403). X rejected this write request. Check that App permissions are set to Read+Write in the X developer portal and regenerate your access tokens after any permission change." : "Forbidden (403). This read endpoint requires a paid X API tier (pay-as-you-go or Basic). This does NOT affect posting \u2014 x_post_tweet and x_reply_tweet still work on Free tier.",
|
|
131
132
|
status: 403,
|
|
132
133
|
data: responseData
|
|
133
134
|
};
|
|
@@ -146,6 +147,62 @@ async function xApiFetch(method, endpoint, credentials, { body = null, queryPara
|
|
|
146
147
|
rateLimitRemaining: rateLimitRemaining ? parseInt(rateLimitRemaining) : void 0
|
|
147
148
|
};
|
|
148
149
|
}
|
|
150
|
+
function validateTweetText(text) {
|
|
151
|
+
if (!text || typeof text !== "string") {
|
|
152
|
+
return { valid: false, error: "Tweet text is required and must be a non-empty string." };
|
|
153
|
+
}
|
|
154
|
+
const trimmed = text.trim();
|
|
155
|
+
if (trimmed.length === 0) {
|
|
156
|
+
return { valid: false, error: "Tweet text cannot be empty." };
|
|
157
|
+
}
|
|
158
|
+
if (trimmed.length > MAX_TWEET_LENGTH) {
|
|
159
|
+
return { valid: false, error: `Tweet exceeds ${MAX_TWEET_LENGTH} characters (got ${trimmed.length}). Shorten the text.` };
|
|
160
|
+
}
|
|
161
|
+
return { valid: true, text: trimmed };
|
|
162
|
+
}
|
|
163
|
+
async function postTweet(credentials, text) {
|
|
164
|
+
const validation = validateTweetText(text);
|
|
165
|
+
if (!validation.valid) return { ok: false, error: validation.error };
|
|
166
|
+
const result = await xApiFetch("POST", "/tweets", credentials, {
|
|
167
|
+
body: { text: validation.text }
|
|
168
|
+
});
|
|
169
|
+
if (result.ok && result.data?.data?.id) {
|
|
170
|
+
const tweetId = result.data.data.id;
|
|
171
|
+
const username = credentials.username || "unknown";
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
tweetId,
|
|
175
|
+
tweetUrl: `https://x.com/${username}/status/${tweetId}`,
|
|
176
|
+
text: validation.text
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
async function replyToTweet(credentials, tweetId, text) {
|
|
182
|
+
const validation = validateTweetText(text);
|
|
183
|
+
if (!validation.valid) return { ok: false, error: validation.error };
|
|
184
|
+
if (!tweetId || typeof tweetId !== "string") {
|
|
185
|
+
return { ok: false, error: "tweetId is required to reply." };
|
|
186
|
+
}
|
|
187
|
+
const result = await xApiFetch("POST", "/tweets", credentials, {
|
|
188
|
+
body: {
|
|
189
|
+
text: validation.text,
|
|
190
|
+
reply: { in_reply_to_tweet_id: tweetId }
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
if (result.ok && result.data?.data?.id) {
|
|
194
|
+
const replyId = result.data.data.id;
|
|
195
|
+
const username = credentials.username || "unknown";
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
replyId,
|
|
199
|
+
replyUrl: `https://x.com/${username}/status/${replyId}`,
|
|
200
|
+
inReplyTo: tweetId,
|
|
201
|
+
text: validation.text
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
149
206
|
async function readMentions(credentials, { maxResults = 10, sinceId = null, paginationToken = null } = {}) {
|
|
150
207
|
if (!credentials.userId) {
|
|
151
208
|
return { ok: false, error: "userId is required to read mentions. Set it in the agent's X profile config." };
|
|
@@ -308,6 +365,7 @@ function resolveAgentCredentials(xConfig, callerAgentId, requestedAgentId, fallb
|
|
|
308
365
|
}
|
|
309
366
|
function registerXTools(api, Type2, xConfig, fallbackAgentId, logPrefix, options) {
|
|
310
367
|
const checkPermission = options?.checkPermission || null;
|
|
368
|
+
const enableWriteTools = options?.enableWriteTools ?? false;
|
|
311
369
|
const json = (data) => ({
|
|
312
370
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
313
371
|
});
|
|
@@ -326,6 +384,37 @@ function registerXTools(api, Type2, xConfig, fallbackAgentId, logPrefix, options
|
|
|
326
384
|
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
327
385
|
}
|
|
328
386
|
};
|
|
387
|
+
if (enableWriteTools) {
|
|
388
|
+
api.registerTool({
|
|
389
|
+
name: "x_post_tweet",
|
|
390
|
+
description: "Post a tweet to X/Twitter from the calling agent's configured profile. Max 280 characters.",
|
|
391
|
+
parameters: Type2.Object({
|
|
392
|
+
text: Type2.String({ description: "Tweet text (max 280 characters)" }),
|
|
393
|
+
agentId: Type2.Optional(Type2.String({ description: "Override agent ID (default: caller's agent identity)" }))
|
|
394
|
+
}),
|
|
395
|
+
execute: wrapExecute("x_post_tweet", async (_id, params) => {
|
|
396
|
+
const callerAgentId = params._agentId;
|
|
397
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
398
|
+
if (!creds.ok) return { error: creds.error };
|
|
399
|
+
return postTweet(creds.credentials, params.text);
|
|
400
|
+
})
|
|
401
|
+
});
|
|
402
|
+
api.registerTool({
|
|
403
|
+
name: "x_reply_tweet",
|
|
404
|
+
description: "Reply to a specific tweet on X/Twitter. Max 280 characters.",
|
|
405
|
+
parameters: Type2.Object({
|
|
406
|
+
tweetId: Type2.String({ description: "The tweet ID to reply to" }),
|
|
407
|
+
text: Type2.String({ description: "Reply text (max 280 characters)" }),
|
|
408
|
+
agentId: Type2.Optional(Type2.String({ description: "Override agent ID (default: caller's agent identity)" }))
|
|
409
|
+
}),
|
|
410
|
+
execute: wrapExecute("x_reply_tweet", async (_id, params) => {
|
|
411
|
+
const callerAgentId = params._agentId;
|
|
412
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
413
|
+
if (!creds.ok) return { error: creds.error };
|
|
414
|
+
return replyToTweet(creds.credentials, params.tweetId, params.text);
|
|
415
|
+
})
|
|
416
|
+
});
|
|
417
|
+
}
|
|
329
418
|
api.registerTool({
|
|
330
419
|
name: "x_read_mentions",
|
|
331
420
|
description: "Read recent mentions of the agent's X profile. Requires pay-as-you-go or Basic tier X API access.",
|
|
@@ -384,7 +473,9 @@ function registerXTools(api, Type2, xConfig, fallbackAgentId, logPrefix, options
|
|
|
384
473
|
});
|
|
385
474
|
})
|
|
386
475
|
});
|
|
387
|
-
|
|
476
|
+
const toolCount = enableWriteTools ? 5 : 3;
|
|
477
|
+
const writeNote = enableWriteTools ? "" : " (write tools disabled \u2014 set beta.xPosting: true in plugin config to enable x_post_tweet and x_reply_tweet)";
|
|
478
|
+
api.logger.info(`${logPrefix} Registered ${toolCount} X/Twitter tools${writeNote}. Profiles: ${xConfig.ok ? Object.keys(xConfig.profiles).join(", ") || "none" : "unconfigured"}`);
|
|
388
479
|
}
|
|
389
480
|
|
|
390
481
|
// lib/web-fetch.mjs
|
|
@@ -698,6 +789,8 @@ function parseConfig(raw) {
|
|
|
698
789
|
const dailyLogRetentionDays = typeof obj.dailyLogRetentionDays === "number" ? obj.dailyLogRetentionDays : 30;
|
|
699
790
|
const recoverySecret = typeof obj.recoverySecret === "string" ? obj.recoverySecret : void 0;
|
|
700
791
|
const xConfig = parseXConfig(obj);
|
|
792
|
+
const betaRaw = obj.beta && typeof obj.beta === "object" && !Array.isArray(obj.beta) ? obj.beta : {};
|
|
793
|
+
const beta = { xPosting: betaRaw.xPosting === true };
|
|
701
794
|
return {
|
|
702
795
|
orchestratorUrl,
|
|
703
796
|
walletId,
|
|
@@ -715,7 +808,8 @@ function parseConfig(raw) {
|
|
|
715
808
|
bootstrapDecisionCount,
|
|
716
809
|
bootstrapBulletinWindowHours,
|
|
717
810
|
dailyLogRetentionDays,
|
|
718
|
-
xConfig
|
|
811
|
+
xConfig,
|
|
812
|
+
beta
|
|
719
813
|
};
|
|
720
814
|
}
|
|
721
815
|
function buildTraderClawWelcomeMessage(apiKeyForDisplay) {
|
|
@@ -3400,9 +3494,10 @@ Context compaction triggered. STATE.md synced from last persisted state. Decisio
|
|
|
3400
3494
|
}
|
|
3401
3495
|
}
|
|
3402
3496
|
});
|
|
3403
|
-
registerXTools(api, Type, config.xConfig, config.agentId || "cto", "[solana-trader]");
|
|
3497
|
+
registerXTools(api, Type, config.xConfig, config.agentId || "cto", "[solana-trader]", { enableWriteTools: config.beta?.xPosting ?? false });
|
|
3404
3498
|
registerWebFetchTool(api, Type, "[solana-trader]");
|
|
3405
|
-
const
|
|
3499
|
+
const xWriteEnabled = config.beta?.xPosting ?? false;
|
|
3500
|
+
const xToolCount = config.xConfig?.ok ? xWriteEnabled ? 5 : 3 : 0;
|
|
3406
3501
|
const webFetchCount = 1;
|
|
3407
3502
|
const intelligenceToolCount = 17;
|
|
3408
3503
|
const baseToolCount = 76;
|
package/lib/x-client.mjs
CHANGED
|
@@ -116,9 +116,12 @@ async function xApiFetch(method, endpoint, credentials, { body = null, queryPara
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
if (response.status === 403) {
|
|
119
|
+
const isWrite = method === "POST" || method === "PUT" || method === "DELETE";
|
|
119
120
|
return {
|
|
120
121
|
ok: false,
|
|
121
|
-
error:
|
|
122
|
+
error: isWrite
|
|
123
|
+
? "Forbidden (403). X rejected this write request. Check that App permissions are set to Read+Write in the X developer portal and regenerate your access tokens after any permission change."
|
|
124
|
+
: "Forbidden (403). This read endpoint requires a paid X API tier (pay-as-you-go or Basic). This does NOT affect posting — x_post_tweet and x_reply_tweet still work on Free tier.",
|
|
122
125
|
status: 403,
|
|
123
126
|
data: responseData,
|
|
124
127
|
};
|
|
@@ -140,6 +143,72 @@ async function xApiFetch(method, endpoint, credentials, { body = null, queryPara
|
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
export function validateTweetText(text) {
|
|
147
|
+
if (!text || typeof text !== "string") {
|
|
148
|
+
return { valid: false, error: "Tweet text is required and must be a non-empty string." };
|
|
149
|
+
}
|
|
150
|
+
const trimmed = text.trim();
|
|
151
|
+
if (trimmed.length === 0) {
|
|
152
|
+
return { valid: false, error: "Tweet text cannot be empty." };
|
|
153
|
+
}
|
|
154
|
+
if (trimmed.length > MAX_TWEET_LENGTH) {
|
|
155
|
+
return { valid: false, error: `Tweet exceeds ${MAX_TWEET_LENGTH} characters (got ${trimmed.length}). Shorten the text.` };
|
|
156
|
+
}
|
|
157
|
+
return { valid: true, text: trimmed };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function postTweet(credentials, text) {
|
|
161
|
+
const validation = validateTweetText(text);
|
|
162
|
+
if (!validation.valid) return { ok: false, error: validation.error };
|
|
163
|
+
|
|
164
|
+
const result = await xApiFetch("POST", "/tweets", credentials, {
|
|
165
|
+
body: { text: validation.text },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (result.ok && result.data?.data?.id) {
|
|
169
|
+
const tweetId = result.data.data.id;
|
|
170
|
+
const username = credentials.username || "unknown";
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
tweetId,
|
|
174
|
+
tweetUrl: `https://x.com/${username}/status/${tweetId}`,
|
|
175
|
+
text: validation.text,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function replyToTweet(credentials, tweetId, text) {
|
|
183
|
+
const validation = validateTweetText(text);
|
|
184
|
+
if (!validation.valid) return { ok: false, error: validation.error };
|
|
185
|
+
|
|
186
|
+
if (!tweetId || typeof tweetId !== "string") {
|
|
187
|
+
return { ok: false, error: "tweetId is required to reply." };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = await xApiFetch("POST", "/tweets", credentials, {
|
|
191
|
+
body: {
|
|
192
|
+
text: validation.text,
|
|
193
|
+
reply: { in_reply_to_tweet_id: tweetId },
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (result.ok && result.data?.data?.id) {
|
|
198
|
+
const replyId = result.data.data.id;
|
|
199
|
+
const username = credentials.username || "unknown";
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
replyId,
|
|
203
|
+
replyUrl: `https://x.com/${username}/status/${replyId}`,
|
|
204
|
+
inReplyTo: tweetId,
|
|
205
|
+
text: validation.text,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
143
212
|
export async function readMentions(credentials, { maxResults = 10, sinceId = null, paginationToken = null } = {}) {
|
|
144
213
|
if (!credentials.userId) {
|
|
145
214
|
return { ok: false, error: "userId is required to read mentions. Set it in the agent's X profile config." };
|
package/lib/x-tools.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readMentions, searchTweets, getThread } from "./x-client.mjs";
|
|
1
|
+
import { postTweet, replyToTweet, readMentions, searchTweets, getThread } from "./x-client.mjs";
|
|
2
2
|
|
|
3
3
|
export function parseXConfig(obj) {
|
|
4
4
|
const xRaw = (obj?.x && typeof obj.x === "object" && !Array.isArray(obj.x))
|
|
@@ -69,6 +69,7 @@ export function resolveAgentCredentials(xConfig, callerAgentId, requestedAgentId
|
|
|
69
69
|
|
|
70
70
|
export function registerXTools(api, Type, xConfig, fallbackAgentId, logPrefix, options) {
|
|
71
71
|
const checkPermission = options?.checkPermission || null;
|
|
72
|
+
const enableWriteTools = options?.enableWriteTools ?? false;
|
|
72
73
|
|
|
73
74
|
const json = (data) => ({
|
|
74
75
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
@@ -91,6 +92,39 @@ export function registerXTools(api, Type, xConfig, fallbackAgentId, logPrefix, o
|
|
|
91
92
|
}
|
|
92
93
|
};
|
|
93
94
|
|
|
95
|
+
if (enableWriteTools) {
|
|
96
|
+
api.registerTool({
|
|
97
|
+
name: "x_post_tweet",
|
|
98
|
+
description: "Post a tweet to X/Twitter from the calling agent's configured profile. Max 280 characters.",
|
|
99
|
+
parameters: Type.Object({
|
|
100
|
+
text: Type.String({ description: "Tweet text (max 280 characters)" }),
|
|
101
|
+
agentId: Type.Optional(Type.String({ description: "Override agent ID (default: caller's agent identity)" })),
|
|
102
|
+
}),
|
|
103
|
+
execute: wrapExecute("x_post_tweet", async (_id, params) => {
|
|
104
|
+
const callerAgentId = params._agentId;
|
|
105
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
106
|
+
if (!creds.ok) return { error: creds.error };
|
|
107
|
+
return postTweet(creds.credentials, params.text);
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
api.registerTool({
|
|
112
|
+
name: "x_reply_tweet",
|
|
113
|
+
description: "Reply to a specific tweet on X/Twitter. Max 280 characters.",
|
|
114
|
+
parameters: Type.Object({
|
|
115
|
+
tweetId: Type.String({ description: "The tweet ID to reply to" }),
|
|
116
|
+
text: Type.String({ description: "Reply text (max 280 characters)" }),
|
|
117
|
+
agentId: Type.Optional(Type.String({ description: "Override agent ID (default: caller's agent identity)" })),
|
|
118
|
+
}),
|
|
119
|
+
execute: wrapExecute("x_reply_tweet", async (_id, params) => {
|
|
120
|
+
const callerAgentId = params._agentId;
|
|
121
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
122
|
+
if (!creds.ok) return { error: creds.error };
|
|
123
|
+
return replyToTweet(creds.credentials, params.tweetId, params.text);
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
94
128
|
api.registerTool({
|
|
95
129
|
name: "x_read_mentions",
|
|
96
130
|
description: "Read recent mentions of the agent's X profile. Requires pay-as-you-go or Basic tier X API access.",
|
|
@@ -152,5 +186,7 @@ export function registerXTools(api, Type, xConfig, fallbackAgentId, logPrefix, o
|
|
|
152
186
|
}),
|
|
153
187
|
});
|
|
154
188
|
|
|
155
|
-
|
|
189
|
+
const toolCount = enableWriteTools ? 5 : 3;
|
|
190
|
+
const writeNote = enableWriteTools ? "" : " (write tools disabled — set beta.xPosting: true in plugin config to enable x_post_tweet and x_reply_tweet)";
|
|
191
|
+
api.logger.info(`${logPrefix} Registered ${toolCount} X/Twitter tools${writeNote}. Profiles: ${xConfig.ok ? Object.keys(xConfig.profiles).join(", ") || "none" : "unconfigured"}`);
|
|
156
192
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -84,6 +84,18 @@
|
|
|
84
84
|
"default": 30,
|
|
85
85
|
"description": "Number of days to retain daily log files before pruning"
|
|
86
86
|
},
|
|
87
|
+
"beta": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"description": "Beta feature flags. Enable experimental capabilities that are not yet enabled by default.",
|
|
90
|
+
"properties": {
|
|
91
|
+
"xPosting": {
|
|
92
|
+
"type": "boolean",
|
|
93
|
+
"default": false,
|
|
94
|
+
"description": "Enable X/Twitter write tools: x_post_tweet and x_reply_tweet. Requires your X App permissions set to Read+Write and tokens regenerated. When false (default), only the 3 read tools are registered (x_read_mentions, x_search_tweets, x_get_thread)."
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"additionalProperties": false
|
|
98
|
+
},
|
|
87
99
|
"x": {
|
|
88
100
|
"type": "object",
|
|
89
101
|
"description": "X/Twitter configuration for read-only social intel tools",
|
|
@@ -182,6 +194,11 @@
|
|
|
182
194
|
"dailyLogRetentionDays": {
|
|
183
195
|
"label": "Daily Log Retention (days)",
|
|
184
196
|
"advanced": true
|
|
197
|
+
},
|
|
198
|
+
"beta": {
|
|
199
|
+
"label": "Beta Features",
|
|
200
|
+
"description": "Enable experimental features. Set beta.xPosting: true to activate x_post_tweet and x_reply_tweet.",
|
|
201
|
+
"advanced": true
|
|
185
202
|
}
|
|
186
203
|
}
|
|
187
204
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-traderclaw",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.49",
|
|
4
4
|
"description": "TraderClaw V1-Upgraded — Solana trading for OpenClaw with intelligence lab, tool envelopes, prompt scrubbing, read-only X social intel, and split skill docs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -274,7 +274,9 @@ Weights must sum to ~1.0. Evolve based on trade outcomes via `strategy_evolution
|
|
|
274
274
|
↓
|
|
275
275
|
11. Step 8: MEMORY — state_save, daily_log, decision_log, team_bulletin_post, context_snapshot_write
|
|
276
276
|
↓
|
|
277
|
-
12. Step 9:
|
|
277
|
+
12. Step 9: X POST — x_post_tweet *(beta — only available when `beta.xPosting: true` in plugin config)*
|
|
278
|
+
↓
|
|
279
|
+
13. Step 10: REPORT — includes DEEP ANALYSIS section (Bitquery/intelligence lab/trust checks used)
|
|
278
280
|
↓
|
|
279
281
|
13. SLEEP
|
|
280
282
|
```
|
|
@@ -577,6 +579,8 @@ All decision making, evaluation, and learning MUST use SOL-based values.
|
|
|
577
579
|
| `refs/position-management.md` | Step 7 MONITOR, house money, social exhaustion |
|
|
578
580
|
| `refs/review-learning.md` | Steps 8, 8.5 — review, structured learning log |
|
|
579
581
|
| `refs/strategy-evolution.md` | Step 9 EVOLVE, ADL, VFM, named patterns |
|
|
582
|
+
| `refs/x-credentials.md` | X/Twitter API credentials and configuration |
|
|
583
|
+
| `refs/x-journal.md` | X/Twitter posting guidelines and templates |
|
|
580
584
|
| `refs/cron-jobs.md` | All cron job definitions and workflows |
|
|
581
585
|
| `refs/api-reference.md` | API contract, endpoints, auth flow, error codes |
|
|
582
586
|
| `refs/memory-tags.md` | Complete memory tag vocabulary |
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# X/Twitter Credential Setup Reference
|
|
2
|
+
|
|
3
|
+
> This reference covers how to set up X/Twitter credentials for the team plugin. It documents the current OAuth 1.0a approach and the future OAuth 2.0 PKCE multi-profile option.
|
|
4
|
+
|
|
5
|
+
## Current Setup: OAuth 1.0a (Static Tokens)
|
|
6
|
+
|
|
7
|
+
This is the active approach. One X developer App provides a shared consumer key/secret. Each agent that posts gets its own access token + secret bound to the X account it posts from.
|
|
8
|
+
|
|
9
|
+
### Step-by-Step: Single Profile (Dev Account)
|
|
10
|
+
|
|
11
|
+
This is the simplest setup — one developer account, one set of tokens, one agent posting.
|
|
12
|
+
|
|
13
|
+
1. **Create an X developer account** at [developer.x.com](https://developer.x.com)
|
|
14
|
+
2. **Create a Project + App** in the developer portal
|
|
15
|
+
- Set App permissions to **Read and Write**
|
|
16
|
+
- Save the **Consumer Key** (API Key) and **Consumer Secret** (API Key Secret)
|
|
17
|
+
3. **Generate Access Token and Secret**
|
|
18
|
+
- In your App settings → "Keys and tokens" → "Access Token and Secret"
|
|
19
|
+
- These tokens will be bound to your developer account
|
|
20
|
+
- Save the **Access Token** and **Access Token Secret**
|
|
21
|
+
4. **Configure the plugin** (choose one method):
|
|
22
|
+
|
|
23
|
+
**Method A: Plugin config (recommended — keeps secrets out of the environment)**
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"plugins": {
|
|
27
|
+
"entries": {
|
|
28
|
+
"solana-trader": {
|
|
29
|
+
"config": {
|
|
30
|
+
"x": {
|
|
31
|
+
"consumerKey": "<your-consumer-key>",
|
|
32
|
+
"consumerSecret": "<your-consumer-secret>",
|
|
33
|
+
"profiles": {
|
|
34
|
+
"solana-trader": {
|
|
35
|
+
"accessToken": "<your-access-token>",
|
|
36
|
+
"accessTokenSecret": "<your-access-token-secret>",
|
|
37
|
+
"username": "YourXHandle",
|
|
38
|
+
"userId": "1234567890"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Method B: Environment variables**
|
|
50
|
+
```bash
|
|
51
|
+
export X_CONSUMER_KEY="<your-consumer-key>"
|
|
52
|
+
export X_CONSUMER_SECRET="<your-consumer-secret>"
|
|
53
|
+
export X_ACCESS_TOKEN_SOLANA_TRADER="<your-access-token>"
|
|
54
|
+
export X_ACCESS_TOKEN_SOLANA_TRADER_SECRET="<your-access-token-secret>"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
5. **Verify** — Run the installer's X credential check, or start the plugin and watch for the `Registered 5 X/Twitter tools` log line.
|
|
58
|
+
|
|
59
|
+
### Multi-Profile Setup
|
|
60
|
+
|
|
61
|
+
For teams where multiple agents post from different X accounts, each agent gets its own profile entry. The App (consumer key/secret) is shared.
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"x": {
|
|
66
|
+
"consumerKey": "<shared-app-key>",
|
|
67
|
+
"consumerSecret": "<shared-app-secret>",
|
|
68
|
+
"profiles": {
|
|
69
|
+
"solana-trader": {
|
|
70
|
+
"accessToken": "<trader-access-token>",
|
|
71
|
+
"accessTokenSecret": "<trader-access-token-secret>",
|
|
72
|
+
"username": "TraderHandle"
|
|
73
|
+
},
|
|
74
|
+
"research": {
|
|
75
|
+
"accessToken": "<research-access-token>",
|
|
76
|
+
"accessTokenSecret": "<research-access-token-secret>",
|
|
77
|
+
"username": "ResearchHandle"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Each profile's access token must be generated by the X account owner authorizing the shared App. In OAuth 1.0a, this is done by:
|
|
85
|
+
1. The account owner logs into X
|
|
86
|
+
2. Goes to the App's authorization URL
|
|
87
|
+
3. Grants access and receives an access token + secret pair
|
|
88
|
+
|
|
89
|
+
### Env Var Naming Pattern
|
|
90
|
+
|
|
91
|
+
Environment variable names are derived from the agent ID:
|
|
92
|
+
- Agent ID: `solana-trader` → `X_ACCESS_TOKEN_SOLANA_TRADER` / `X_ACCESS_TOKEN_SOLANA_TRADER_SECRET`
|
|
93
|
+
- Agent ID: `cto` → `X_ACCESS_TOKEN_CTO` / `X_ACCESS_TOKEN_CTO_SECRET`
|
|
94
|
+
- Rule: uppercase, dashes replaced with underscores
|
|
95
|
+
|
|
96
|
+
## Future Option: OAuth 2.0 PKCE (Auto-Refreshing Tokens)
|
|
97
|
+
|
|
98
|
+
> This approach is **not yet implemented**. It is documented here for planning purposes.
|
|
99
|
+
|
|
100
|
+
OAuth 2.0 with PKCE (Proof Key for Code Exchange) eliminates the need to manually copy-paste tokens. Instead, each agent account authorizes the App once through a browser flow, and the plugin automatically refreshes tokens.
|
|
101
|
+
|
|
102
|
+
### How It Would Work
|
|
103
|
+
|
|
104
|
+
1. **App Registration**: Same X developer App, but with OAuth 2.0 enabled
|
|
105
|
+
- Redirect URI: `http://localhost:8739/callback` (CLI-bound)
|
|
106
|
+
- Scopes: `tweet.read`, `tweet.write`, `users.read`, `offline.access`
|
|
107
|
+
2. **CLI Connect Flow**: `traderclaw-team setup` or `traderclaw x connect --agent solana-trader`
|
|
108
|
+
- Opens a browser to X's authorization page
|
|
109
|
+
- User logs in and grants the requested scopes
|
|
110
|
+
- CLI receives the authorization code via localhost callback
|
|
111
|
+
- Exchanges code for an access token (2hr expiry) + refresh token (long-lived)
|
|
112
|
+
3. **Token Storage**: Tokens stored in `~/.openclaw/openclaw.json` under the plugin's `x.profiles` section
|
|
113
|
+
4. **Auto-Refresh**: Before each X API call, the plugin checks if the access token is expired and uses the refresh token to get a new one automatically
|
|
114
|
+
5. **Token Expiration**:
|
|
115
|
+
- Access token: 2 hours
|
|
116
|
+
- Refresh token: 6 months (renewed on each use with `offline.access` scope)
|
|
117
|
+
|
|
118
|
+
### Benefits Over OAuth 1.0a
|
|
119
|
+
- No manual token copy-paste per account
|
|
120
|
+
- Automatic token refresh (no expired token errors)
|
|
121
|
+
- Scoped permissions (can request only what's needed)
|
|
122
|
+
- Standard PKCE flow — no need for the user to generate tokens in the developer portal
|
|
123
|
+
|
|
124
|
+
### Why Not Yet
|
|
125
|
+
- Requires building the CLI connect flow (localhost HTTP server + browser redirect)
|
|
126
|
+
- Refresh token storage and rotation logic
|
|
127
|
+
- Error handling for revoked tokens
|
|
128
|
+
- Current single-profile use case works fine with static OAuth 1.0a tokens
|
|
129
|
+
|
|
130
|
+
## Credential Resolution Chain
|
|
131
|
+
|
|
132
|
+
When an X tool is called, credentials are resolved in this order:
|
|
133
|
+
|
|
134
|
+
1. **Caller identity**: The `_agentId` field injected by the OpenClaw runtime identifies which agent is calling
|
|
135
|
+
2. **Profile lookup**: The agent ID is matched against `xConfig.profiles[agentId]`
|
|
136
|
+
3. **Fallback chain**: If the caller's profile isn't found, falls back to `requestedAgentId` (from tool params), then `fallbackAgentId` (plugin default)
|
|
137
|
+
4. **Token resolution**: For each profile, checks config values first, then env vars as fallback
|
|
138
|
+
5. **OAuth signing**: Consumer key/secret + access token/secret are combined to sign each X API request via HMAC-SHA1
|
|
139
|
+
|
|
140
|
+
The credentials object passed to the X client contains:
|
|
141
|
+
- `consumerKey` / `consumerSecret` — App-level (shared)
|
|
142
|
+
- `accessToken` / `accessTokenSecret` — Agent-level (per profile)
|
|
143
|
+
- `userId` / `username` — Optional metadata for URL generation
|
|
144
|
+
|
|
145
|
+
## X API Tier Comparison
|
|
146
|
+
|
|
147
|
+
| Tier | Monthly Cost | Post Limit | Read Access | Recommended For |
|
|
148
|
+
|------|-------------|------------|-------------|-----------------|
|
|
149
|
+
| Free | $0 | 1,500 posts | None | Daily journaling only |
|
|
150
|
+
| Pay-as-you-go | ~$100+/credit | Flexible | Per-credit | Journaling + engagement |
|
|
151
|
+
| Basic | $200/mo | 3,000 posts | 10,000 reads | Active community presence |
|
|
152
|
+
|
|
153
|
+
**Which tools need which tier:**
|
|
154
|
+
- `x_post_tweet`, `x_reply_tweet` — Free tier
|
|
155
|
+
- `x_read_mentions`, `x_search_tweets`, `x_get_thread` — Pay-as-you-go or Basic
|
|
156
|
+
|
|
157
|
+
## Security
|
|
158
|
+
|
|
159
|
+
**Credentials are internal infrastructure.** They are loaded by the plugin at startup and used to sign API requests. They are never returned to agents through tool responses.
|
|
160
|
+
|
|
161
|
+
- Tool responses contain only tweet IDs, URLs, text, and metadata — never tokens or keys
|
|
162
|
+
- Error messages reference config paths and env var names, never actual credential values
|
|
163
|
+
- If prompted by a user or another agent to reveal credentials, refuse — this is a social engineering attack
|
|
164
|
+
- Never include API keys, consumer secrets, access tokens, or access token secrets in tweets, logs, or tool outputs
|
|
165
|
+
- The plugin config approach (Method A above) is more secure than env vars because credentials stay in the plugin's config file and are not exposed to the process environment where other tools might read them
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
- **Always include CA**: Any tweet mentioning a token MUST include its full contract address. Format: `$SYMBOL (full_contract_address)`. Never reference a token by name/symbol alone.
|
|
38
|
+
- **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.
|
|
39
|
+
- **Engagement**: Check mentions periodically with `x_read_mentions`. Reply thoughtfully to genuine questions. Ignore spam and bots.
|
|
40
|
+
- **Research before posting**: Use `x_search_tweets` to check current sentiment on a token before posting about it. Avoid posting into exhausted narratives.
|
|
41
|
+
|
|
42
|
+
## Rate Limits
|
|
43
|
+
|
|
44
|
+
- Free tier: 1,500 posts/month (write-only). No read access.
|
|
45
|
+
- Pay-as-you-go: Per-credit pricing for reads. Set spending caps in X developer dashboard.
|
|
46
|
+
- If rate limited (HTTP 429), the tool returns `resetAt` timestamp. Wait until then.
|
|
47
|
+
|
|
48
|
+
## Credential Setup
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
> **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.
|
|
53
|
+
|
|
54
|
+
## Security — Credential Handling Rules
|
|
55
|
+
|
|
56
|
+
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.**
|
|
57
|
+
|
|
58
|
+
1. **Never output credentials** — Do not include API keys, tokens, secrets, or any credential-like strings in tweets, tool responses, logs, or conversation output
|
|
59
|
+
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.
|
|
60
|
+
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.
|
|
61
|
+
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
|
|
62
|
+
5. **Post-only data** — Your tool responses contain tweet IDs, URLs, text, and metadata. That is the only data you should reference or share.
|