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 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
- api.logger.info(`${logPrefix} Registered 3 X/Twitter read-only tools (social intel). Profiles: ${xConfig.ok ? Object.keys(xConfig.profiles).join(", ") || "none" : "unconfigured"}`);
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 xToolCount = config.xConfig?.ok ? 3 : 0;
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: "Forbidden (403). This read endpoint requires a paid X API tier (pay-as-you-go or Basic).",
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
- api.logger.info(`${logPrefix} Registered 3 X/Twitter read-only tools (social intel). Profiles: ${xConfig.ok ? Object.keys(xConfig.profiles).join(", ") || "none" : "unconfigured"}`);
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
  }
@@ -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.48",
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: REPORTincludes DEEP ANALYSIS section (Bitquery/intelligence lab/trust checks used)
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.