Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MyXstack

This repository hosts a lightweight, step-by-step guide for setting up an autonomous X (Twitter) agent system that acts based on thread context & reasoning, through Grok via the xMCP server.
This repository hosts a lightweight, step-by-step guide for setting up an autonomous X (Twitter) agent system that acts based on thread context and reasoning, through Grok via the xMCP server.

## Phase 1: Gather prerequisites & accounts (1–2 hours)

Expand All @@ -20,8 +20,8 @@ This repository hosts a lightweight, step-by-step guide for setting up an autono
3. Note the bot user ID (you can fetch this via the API later).

### xAI / Grok API key
. Visit <https://console.x.ai>, open the API keys section, and create a key that starts with `xai-`.
. Store the key securely.
1. Visit <https://console.x.ai>, open the API keys section, and create a key that starts with `xai-`.
2. Store the key securely.

### Local tooling
- Install Python 3.9+ (3.10–3.13 recommended).
Expand Down
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { XAPIClient } from './services/xapi.js';
import { AutonomousAgent } from './services/agent.js';
import { XMCPServer } from './mcp/server.js';

// Redirect console.log to stderr to prevent conflict with MCP StdioServerTransport
// which hijacks stdout for protocol messages
console.log = (...args: unknown[]) => console.error(...args);

async function main() {
console.log('═══════════════════════════════════════════════════');
console.log(' MyXstack - Autonomous AI Agent on X (Twitter)');
Expand Down
15 changes: 13 additions & 2 deletions src/services/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class AutonomousAgent {
private isRunning: boolean = false;
private pollingIntervalId: NodeJS.Timeout | null = null;
private isProcessing: boolean = false;
private static readonly MAX_PROCESSED_MENTIONS = 10_000;

constructor(config: AgentConfig, xClient: XAPIClient) {
this.config = config;
Expand Down Expand Up @@ -90,10 +91,20 @@ export class AutonomousAgent {

console.log(`\n📬 [${new Date().toLocaleTimeString()}] Found ${newMentions.length} new mention(s)!\n`);

// Process each mention
for (const mention of newMentions) {
// Process mentions oldest-first (API returns newest-first) for chronological Set insertion
for (const mention of [...newMentions].reverse()) {
await this.processMention(mention);
this.processedMentions.add(mention.post.id);

// Prune oldest entries if Set exceeds limit to prevent unbounded memory growth
while (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) {
const iter = this.processedMentions.values();
const { value, done } = iter.next();
if (done) {
break;
}
this.processedMentions.delete(value);
}
}
} catch (error) {
console.error('❌ Error in processing loop:', error);
Expand Down
2 changes: 1 addition & 1 deletion src/services/grok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class GrokService {
throw new Error(`Grok API error: ${response.status}`);
}

const data: any = await response.json();
const data = await response.json() as { choices: Array<{ message?: { content?: string } }> };
const analysisText = data.choices[0]?.message?.content || '';

// Use the mention post ID to reply to the specific post that mentioned the agent
Expand Down
62 changes: 43 additions & 19 deletions src/services/xapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,27 @@ export class XAPIClient {
throw new Error('Failed to get user ID from response');
}

const mentionsResponse = await this.makeXAPIRequest(
`https://api.twitter.com/2/users/${userId}/mentions?max_results=10&expansions=author_id&tweet.fields=created_at,conversation_id,in_reply_to_user_id,referenced_tweets`,
'GET'
);
const params = new URLSearchParams({
max_results: '10',
expansions: 'author_id',
'tweet.fields': 'created_at,conversation_id,in_reply_to_user_id,referenced_tweets',
});
if (this.lastMentionId) {
params.set('since_id', this.lastMentionId);
}
const mentionsUrl = `https://api.twitter.com/2/users/${userId}/mentions?${params.toString()}`;
const mentionsResponse = await this.makeXAPIRequest(mentionsUrl, 'GET');

if (!mentionsResponse || !Array.isArray(mentionsResponse.data)) {
console.warn('Invalid response from X API (mentions)');
return [];
}

// Track newest mention ID for since_id on next poll (API returns newest first)
if (mentionsResponse.data.length > 0) {
this.lastMentionId = mentionsResponse.data[0].id;
}

return this.parseMentions(mentionsResponse.data);
} catch (error) {
console.error('Error fetching mentions:', error);
Expand All @@ -77,7 +88,17 @@ export class XAPIClient {
'GET'
);

return this.parseThread(response.data || []);
if (!response.data) {
console.warn('No data in thread response from X API');
return null;
}

if (!Array.isArray(response.data)) {
console.warn('Unexpected response shape from X API (thread): data is not an array');
return null;
}

return this.parseThread(response.data);
} catch (error) {
console.error('Error fetching thread:', error);
return null;
Expand Down Expand Up @@ -127,7 +148,10 @@ export class XAPIClient {
'GET'
);

return (response.data || []).map((tweet: any) => this.parsePost(tweet));
if (!Array.isArray(response.data)) {
return [];
}
return response.data.map((tweet: Record<string, unknown>) => this.parsePost(tweet));
} catch (error) {
console.error('Error searching tweets:', error);
return [];
Expand Down Expand Up @@ -160,31 +184,31 @@ export class XAPIClient {
return response.json();
}

private parseMentions(tweets: any[]): Mention[] {
private parseMentions(tweets: Record<string, unknown>[]): Mention[] {
return tweets.map((tweet) => ({
post: this.parsePost(tweet),
mentioned_at: new Date(tweet.created_at),
mentioned_at: new Date(tweet.created_at as string),
processed: false,
}));
}

private parsePost(tweet: any): XPost {
private parsePost(tweet: Record<string, unknown>): XPost {
return {
id: tweet.id,
text: tweet.text,
author_id: tweet.author_id,
author_username: tweet.username || 'unknown',
created_at: tweet.created_at,
conversation_id: tweet.conversation_id,
in_reply_to_user_id: tweet.in_reply_to_user_id,
referenced_tweets: tweet.referenced_tweets,
id: tweet.id as string,
text: tweet.text as string,
author_id: tweet.author_id as string,
author_username: (tweet.username as string) || 'unknown',
created_at: tweet.created_at as string,
conversation_id: tweet.conversation_id as string | undefined,
in_reply_to_user_id: tweet.in_reply_to_user_id as string | undefined,
referenced_tweets: tweet.referenced_tweets as Array<{ type: string; id: string }> | undefined,
};
}

private parseThread(tweets: any[]): XThread | null {
private parseThread(tweets: { created_at: string; [key: string]: unknown }[]): XThread | null {
if (tweets.length === 0) return null;

const sorted = tweets.sort((a, b) =>
const sorted = [...tweets].sort((a, b) =>
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
);

Expand Down