├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── src ├── config │ └── config.ts ├── index.ts ├── services │ ├── BTBTweetService.ts │ ├── ClaudeService.ts │ ├── KnowledgeBaseService.ts │ ├── LLMService.ts │ ├── TwitterService.ts │ └── WalletService.ts ├── test-tweet.ts ├── types.ts ├── types │ └── index.ts └── utils │ └── logger.ts └── tsconfig.json /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Autonomous AI Agent 2 | 3 | We love your input! We want to make contributing to Autonomous AI Agent as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## Development Process 12 | 13 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | 1. Fork the repo and create your branch from `main` 16 | 2. Install dependencies with `npm install` 17 | 3. If you've added code that should be tested, add tests 18 | 4. If you've changed APIs, update the documentation 19 | 5. Ensure the test suite passes 20 | 6. Make sure your code follows our TypeScript style guide 21 | 7. Issue that pull request! 22 | 23 | ## Code Style and Standards 24 | 25 | ### TypeScript Guidelines 26 | - Use TypeScript's strict mode (enabled in tsconfig.json) 27 | - Properly type all variables, parameters, and return values 28 | - Use interfaces for object shapes 29 | - Leverage TypeScript's type system to prevent runtime errors 30 | 31 | ### Code Quality 32 | - Use ESLint for code linting 33 | - Format code with Prettier 34 | - Maintain comprehensive JSDoc documentation 35 | - Write unit tests using Jest 36 | - Follow async/await patterns for asynchronous code 37 | - Use meaningful variable and function names 38 | 39 | ### Best Practices 40 | - Follow SOLID principles 41 | - Write modular and reusable code 42 | - Keep functions small and focused 43 | - Use dependency injection where appropriate 44 | - Handle errors properly with try/catch 45 | - Use environment variables for configuration 46 | 47 | ## Development Setup 48 | 49 | 1. Install Node.js (v16 or later) 50 | 2. Clone the repository 51 | 3. Install dependencies: 52 | ```bash 53 | npm install 54 | ``` 55 | 4. Copy `.env.example` to `.env` and configure 56 | 5. Start development server: 57 | ```bash 58 | npm run dev 59 | ``` 60 | 61 | ## Testing 62 | 63 | - Write tests using Jest 64 | - Place tests in `__tests__` directories or with `.test.ts` extension 65 | - Run tests with: 66 | ```bash 67 | npm test 68 | ``` 69 | - Aim for good test coverage 70 | - Include unit tests for new features 71 | - Add integration tests for API endpoints 72 | 73 | ## Pull Request Process 74 | 75 | 1. Update the README.md with details of changes to the interface 76 | 2. Update any relevant documentation 77 | 3. Add or update tests as needed 78 | 4. Update `package.json` if adding dependencies 79 | 5. The PR will be merged once you have the sign-off of at least one maintainer 80 | 81 | ## Any contributions you make will be under the MIT License 82 | When you submit code changes, your submissions are understood to be under the same [MIT License](LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. 83 | 84 | ## Report bugs using GitHub's [issue tracker](https://github.com/btb-finance/Autonomous-AI-Agent/issues) 85 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/btb-finance/Autonomous-AI-Agent/issues/new). 86 | 87 | ## Write bug reports with detail, background, and sample code 88 | 89 | **Great Bug Reports** tend to have: 90 | 91 | - A quick summary and/or background 92 | - Steps to reproduce 93 | - Be specific! 94 | - Give sample code if you can 95 | - What you expected would happen 96 | - What actually happens 97 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 98 | 99 | ## Priority Areas for Contribution 100 | 101 | We particularly need help with: 102 | - Enhanced conversation strategies 103 | - Additional social media platform integrations 104 | - Improved security measures 105 | - Testing and documentation 106 | - UI/UX for monitoring and configuration 107 | 108 | ## Code Review Process 109 | 110 | 1. All code changes happen through pull requests 111 | 2. Pull requests are reviewed by at least one maintainer 112 | 3. Reviews focus on: 113 | - Code quality and style 114 | - Type safety 115 | - Test coverage 116 | - Documentation 117 | - Security considerations 118 | 119 | ## Setting Up Development Environment 120 | 121 | 1. Fork and clone the repository 122 | 2. Install dependencies: 123 | ```bash 124 | npm install 125 | ``` 126 | 3. Set up your IDE: 127 | - Enable TypeScript support 128 | - Install ESLint extension 129 | - Install Prettier extension 130 | - Configure auto-formatting on save 131 | 4. Create your feature branch: 132 | ```bash 133 | git checkout -b feature/your-feature-name 134 | ``` 135 | 136 | ## License 137 | By contributing, you agree that your contributions will be licensed under its MIT License. 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 BTB Finance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autonomous AI Agent 2 | 3 | Welcome to the **Autonomous AI Agent** project by [@0xmooncity](https://twitter.com/0xmooncity) and the [mooncitydev](https://t.me/mooncitydev) . 4 | This project aims to create an autonomous AI-driven agent that can manage social media accounts (starting with Twitter), interact with users, remember context, handle tasks like sending tips (cryptocurrency microtransactions), and more—all with minimal human intervention. The long-term vision is to expand beyond Twitter to multiple social media platforms. 5 | 6 | We're building this in **TypeScript** for type safety, extensive ecosystem support, and rapid development capabilities. 7 | 8 | 9 | ## Project Goals 10 | 11 | 1. **Autonomous Social Media Management** 12 | The agent should: 13 | - Read mentions, replies, and direct messages 14 | - Generate and publish tweets autonomously using integrated LLMs (e.g., Claude API) 15 | - Maintain a rolling memory and conversational context 16 | - Monitor specific topics and hashtags (e.g., $BTB) 17 | - Engage with community through likes and replies 18 | 19 | 2. **Cryptocurrency Tipping and Transactions** 20 | The agent can: 21 | - Generate and securely store a wallet address and private keys 22 | - Send small crypto tips to users who meet certain criteria 23 | - Interact with blockchain APIs using ethers.js 24 | - Track transaction history and user interactions 25 | - Implement secure key management and encryption 26 | 27 | 3. **Scalable and Extensible Architecture** 28 | Our code aims to: 29 | - Be easily extensible to other social media platforms 30 | - Allow integration with various LLM providers and conversation strategies 31 | - Encourage community contributions and improvements 32 | - Support multiple blockchain networks and token standards 33 | 34 | ## Key Features 35 | 36 | - **TypeScript Backend:** 37 | - Strong type system for enhanced code reliability 38 | - Modern async/await patterns for efficient I/O operations 39 | - Rich ecosystem of NPM packages 40 | - Easy integration with Node.js 41 | 42 | - **LLM Integration (Claude):** 43 | - Use language model APIs for natural language understanding and generation 44 | - Context-aware responses with conversation history 45 | - Custom prompt engineering for specific use cases 46 | - Fallback mechanisms for API failures 47 | 48 | - **Secure Storage of Keys and State:** 49 | - Environment-based configuration using dotenv 50 | - Private keys and secrets are never hardcoded 51 | - Secure key management practices 52 | - Regular state persistence 53 | 54 | - **Event-Driven Architecture:** 55 | - Periodically fetch mentions from Twitter 56 | - Process each event through the LLM for intelligent responses 57 | - Rate limiting and exponential backoff 58 | - Error handling and retry mechanisms 59 | 60 | - **Compliance and Safety Controls:** 61 | - Guardrails at the prompt and code level 62 | - Content filtering and moderation 63 | - Rate limiting and anti-spam measures 64 | - Audit logging for all operations 65 | 66 | ## Project Status 67 | 68 | - **Current Implementation:** 69 | Features implemented: 70 | - Twitter API integration using twitter-api-v2 71 | - Claude API integration for LLM capabilities 72 | - Ethereum wallet management with ethers.js 73 | - State management and conversation tracking 74 | - Rate limiting and error handling 75 | 76 | - **Contributions Needed:** 77 | Priority areas: 78 | - Enhanced conversation strategies 79 | - Additional social media platform integrations 80 | - Improved security measures 81 | - Testing and documentation 82 | - UI/UX for monitoring and configuration 83 | 84 | ## Getting Started 85 | 86 | ### Prerequisites 87 | 88 | - **Node.js and npm:** 89 | - Node.js (v16 or later) from [https://nodejs.org/](https://nodejs.org/) 90 | - Required packages listed in `package.json` 91 | 92 | - **Twitter Developer Account:** 93 | Required credentials: 94 | - API Key and Secret 95 | - Access Token and Secret 96 | - App permissions for read/write access 97 | 98 | - **Claude API Key:** 99 | - Claude API key from [Anthropic](https://www.anthropic.com/) 100 | - Rate limits and usage quotas consideration 101 | 102 | 103 | ## Project Structure 104 | 105 | ``` 106 | src/ 107 | ├── index.ts # Main application entry point 108 | ├── services/ # Core services 109 | │ ├── TwitterApi.ts # Twitter API integration 110 | │ ├── LLMClient.ts # Claude API integration 111 | │ └── WalletManager.ts # Ethereum wallet management 112 | └── types/ # TypeScript type definitions 113 | └── index.ts # Shared types and interfaces 114 | ``` 115 | 116 | ## Configuration 117 | 118 | The application uses environment variables for configuration: 119 | 120 | - `TWITTER_API_KEY` - Twitter API key 121 | - `TWITTER_API_SECRET` - Twitter API secret 122 | - `TWITTER_ACCESS_TOKEN` - Twitter access token 123 | - `TWITTER_ACCESS_TOKEN_SECRET` - Twitter access token secret 124 | - `CLAUDE_API_KEY` - Claude API key 125 | - `WALLET_PRIVATE_KEY` - Ethereum wallet private key 126 | - `ETH_RPC_URL` - Ethereum RPC URL 127 | 128 | ## Contributing 129 | 130 | We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. 131 | 132 | ## License 133 | 134 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 135 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autonomous-ai-agent", 3 | "version": "1.0.0", 4 | "description": "Autonomous AI Agent", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node dist/index.js", 9 | "dev": "ts-node src/index.ts", 10 | "test": "jest", 11 | "lint": "eslint src/**/*.ts", 12 | "format": "prettier --write src/**/*.ts" 13 | }, 14 | "dependencies": { 15 | "axios": "^1.6.2", 16 | "dotenv": "^16.3.1", 17 | "ethers": "^6.9.0", 18 | "twitter-api-v2": "^1.15.2", 19 | "winston": "^3.11.0" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "^29.5.11", 23 | "@types/node": "^20.10.4", 24 | "@typescript-eslint/eslint-plugin": "^6.13.2", 25 | "@typescript-eslint/parser": "^6.13.2", 26 | "eslint": "^8.55.0", 27 | "jest": "^29.7.0", 28 | "prettier": "^3.1.0", 29 | "ts-jest": "^29.1.1", 30 | "ts-node": "^10.9.2", 31 | "typescript": "^5.3.3" 32 | }, 33 | "keywords": [ 34 | "ai", 35 | "twitter", 36 | "bot", 37 | "autonomous", 38 | "cryptocurrency" 39 | ], 40 | "author": "BTB Finance", 41 | "license": "MIT" 42 | } 43 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import { config as dotenvConfig } from 'dotenv'; 2 | import { AgentConfig } from '../types'; 3 | import { logger } from '../utils/logger'; 4 | 5 | const result = dotenvConfig(); 6 | if (result.error) { 7 | logger.error('Error loading .env file:', result.error); 8 | } 9 | 10 | function validateConfig(config: Partial): config is AgentConfig { 11 | const requiredFields = [ 12 | 'twitter.apiKey', 13 | 'twitter.apiSecret', 14 | 'twitter.accessToken', 15 | 'twitter.accessTokenSecret', 16 | 'twitter.bearerToken', 17 | 'llm.apiKey', 18 | 'wallet.privateKey', 19 | 'wallet.rpcUrl' 20 | ]; 21 | 22 | for (const field of requiredFields) { 23 | const value = field.split('.').reduce((obj, key) => obj?.[key], config as any); 24 | if (!value) { 25 | logger.error(`Missing configuration for ${field}. Available env vars:`, process.env); 26 | throw new Error(`Missing required configuration: ${field}`); 27 | } 28 | } 29 | 30 | return true; 31 | } 32 | 33 | export function loadConfig(): AgentConfig { 34 | logger.info('Loading configuration...'); 35 | logger.debug('Environment variables:', { 36 | TWITTER_API_KEY: process.env.TWITTER_API_KEY, 37 | TWITTER_BEARER_TOKEN: process.env.TWITTER_BEARER_TOKEN ? '[REDACTED]' : undefined, 38 | CLAUDE_API_KEY: process.env.CLAUDE_API_KEY ? '[REDACTED]' : undefined, 39 | WALLET_PRIVATE_KEY: process.env.WALLET_PRIVATE_KEY ? '[REDACTED]' : undefined, 40 | ETH_RPC_URL: process.env.ETH_RPC_URL, 41 | }); 42 | 43 | const config: AgentConfig = { 44 | twitter: { 45 | apiKey: process.env.TWITTER_API_KEY || '', 46 | apiSecret: process.env.TWITTER_API_SECRET || '', 47 | accessToken: process.env.TWITTER_ACCESS_TOKEN || '', 48 | accessTokenSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET || '', 49 | bearerToken: process.env.TWITTER_BEARER_TOKEN || '', 50 | }, 51 | llm: { 52 | provider: 'claude', 53 | apiKey: process.env.CLAUDE_API_KEY || '', 54 | model: 'claude-3-opus-20240229', 55 | maxTokens: 1024, 56 | }, 57 | wallet: { 58 | network: process.env.ETH_NETWORK || 'mainnet', 59 | rpcUrl: process.env.ETH_RPC_URL || '', 60 | privateKey: process.env.WALLET_PRIVATE_KEY || '', 61 | }, 62 | monitoring: { 63 | enabled: true, 64 | interval: 60000, 65 | logLevel: 'info', 66 | enableMetrics: true, 67 | }, 68 | }; 69 | 70 | if (!validateConfig(config)) { 71 | throw new Error('Invalid configuration'); 72 | } 73 | 74 | return config; 75 | } 76 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { config } from 'dotenv'; 3 | import { TwitterService } from './services/TwitterService'; 4 | import { ClaudeService } from './services/ClaudeService'; 5 | import { KnowledgeBaseService } from './services/KnowledgeBaseService'; 6 | import { BTBTweetService } from './services/BTBTweetService'; 7 | import { logger } from './utils/logger'; 8 | 9 | // Load environment variables 10 | config(); 11 | 12 | async function main() { 13 | try { 14 | logger.info('Loading configuration...'); 15 | 16 | // Initialize services 17 | const twitterService = new TwitterService( 18 | process.env.TWITTER_API_KEY!, 19 | process.env.TWITTER_API_SECRET!, 20 | process.env.TWITTER_ACCESS_TOKEN!, 21 | process.env.TWITTER_ACCESS_TOKEN_SECRET!, 22 | process.env.TWITTER_BEARER_TOKEN! 23 | ); 24 | 25 | const claudeService = new ClaudeService( 26 | process.env.CLAUDE_API_KEY! 27 | ); 28 | 29 | const knowledgeBaseService = new KnowledgeBaseService( 30 | path.join(__dirname, '..', 'knowledge_base', 'btb_info.txt') 31 | ); 32 | 33 | // Initialize BTB tweet service 34 | const btbTweetService = new BTBTweetService( 35 | twitterService, 36 | claudeService, 37 | knowledgeBaseService 38 | ); 39 | 40 | logger.info('Starting Autonomous AI Agent'); 41 | 42 | // Start processing tweets every minute 43 | await btbTweetService.startProcessing(60000); 44 | } catch (error) { 45 | logger.error('Error in main loop:', error); 46 | process.exit(1); 47 | } 48 | } 49 | 50 | main(); 51 | -------------------------------------------------------------------------------- /src/services/BTBTweetService.ts: -------------------------------------------------------------------------------- 1 | import { TwitterService } from './TwitterService'; 2 | import { ClaudeService } from './ClaudeService'; 3 | import { KnowledgeBaseService } from './KnowledgeBaseService'; 4 | import { logger } from '../utils/logger'; 5 | import { Tweet, RateLimit } from '../types'; 6 | 7 | export class BTBTweetService { 8 | private twitterService: TwitterService; 9 | private claudeService: ClaudeService; 10 | private knowledgeBaseService: KnowledgeBaseService; 11 | private lastProcessedTweetId?: string; 12 | private isProcessing: boolean = false; 13 | private nextPollTime: Date = new Date(); 14 | private defaultIntervalMs: number = 60000; // 1 minute default 15 | 16 | constructor( 17 | twitterService: TwitterService, 18 | claudeService: ClaudeService, 19 | knowledgeBaseService: KnowledgeBaseService 20 | ) { 21 | this.twitterService = twitterService; 22 | this.claudeService = claudeService; 23 | this.knowledgeBaseService = knowledgeBaseService; 24 | } 25 | 26 | private extractQuestion(tweetText: string): string { 27 | // Remove $BTB mention, @mentions, and any URLs 28 | const cleanText = tweetText 29 | .replace(/\$BTB/gi, '') 30 | .replace(/@\w+/g, '') 31 | .replace(/https?:\/\/\S+/g, '') 32 | .trim(); 33 | return cleanText; 34 | } 35 | 36 | private updateNextPollTime(rateLimit?: RateLimit): void { 37 | if (rateLimit?.remaining === 0 && rateLimit?.reset) { 38 | // Convert reset timestamp to milliseconds and add 1 second buffer 39 | const resetTime = new Date(rateLimit.reset * 1000 + 1000); 40 | this.nextPollTime = resetTime; 41 | logger.info('Updated next poll time due to rate limit', { 42 | nextPoll: this.nextPollTime.toISOString(), 43 | }); 44 | } else { 45 | // Use default interval if no rate limit info 46 | this.nextPollTime = new Date(Date.now() + this.defaultIntervalMs); 47 | } 48 | } 49 | 50 | private async sleep(ms: number): Promise { 51 | return new Promise(resolve => setTimeout(resolve, ms)); 52 | } 53 | 54 | async processBTBTweets(): Promise { 55 | if (this.isProcessing) { 56 | logger.info('Already processing tweets, skipping...'); 57 | return; 58 | } 59 | 60 | try { 61 | this.isProcessing = true; 62 | logger.info('Processing $BTB tweets...', { lastProcessedTweetId: this.lastProcessedTweetId }); 63 | 64 | // Get mentions since last processed tweet 65 | const { mentions, rateLimit } = await this.twitterService.getMentionsWithRateLimit(this.lastProcessedTweetId); 66 | 67 | // Update next poll time based on rate limit info 68 | this.updateNextPollTime(rateLimit); 69 | 70 | // Filter for tweets containing $BTB 71 | const btbTweets = mentions.filter((tweet: Tweet) => 72 | tweet.text.toLowerCase().includes('$btb') 73 | ); 74 | 75 | logger.info(`Found ${btbTweets.length} $BTB tweets to process`); 76 | 77 | // Process each tweet 78 | for (const tweet of btbTweets) { 79 | try { 80 | // Skip if we've already processed this tweet 81 | if (this.lastProcessedTweetId && tweet.id <= this.lastProcessedTweetId) { 82 | continue; 83 | } 84 | 85 | // Extract the question from the tweet 86 | const question = this.extractQuestion(tweet.text); 87 | 88 | if (!question) { 89 | logger.info('No question found in tweet, skipping', { tweetId: tweet.id }); 90 | continue; 91 | } 92 | 93 | // Get knowledge base context 94 | const prompt = await this.knowledgeBaseService.searchKnowledge(question); 95 | 96 | // Get Claude's response 97 | const claudeResponse = await this.claudeService.getResponse(prompt); 98 | 99 | // Format the response to fit Twitter's character limit 100 | const formattedResponse = this.formatTwitterResponse(claudeResponse); 101 | 102 | // Reply to the tweet 103 | await this.twitterService.replyToTweet(formattedResponse, tweet.id); 104 | 105 | logger.info('Successfully processed tweet', { tweetId: tweet.id }); 106 | 107 | // Update last processed tweet ID 108 | this.lastProcessedTweetId = tweet.id; 109 | 110 | // Add a small delay between processing tweets to avoid rate limits 111 | await this.sleep(1000); 112 | } catch (error) { 113 | logger.error('Failed to process tweet:', error, { tweetId: tweet.id }); 114 | // Continue processing other tweets even if one fails 115 | continue; 116 | } 117 | } 118 | } catch (error: any) { 119 | logger.error('Failed to process $BTB tweets:', error); 120 | // Update next poll time if we hit a rate limit 121 | if (error?.rateLimit) { 122 | this.updateNextPollTime(error.rateLimit); 123 | } 124 | } finally { 125 | this.isProcessing = false; 126 | } 127 | } 128 | 129 | private formatTwitterResponse(response: string): string { 130 | // Twitter's character limit is 280 131 | const MAX_LENGTH = 280; 132 | 133 | if (response.length <= MAX_LENGTH) { 134 | return response; 135 | } 136 | 137 | // If response is too long, truncate it and add an ellipsis 138 | return response.substring(0, MAX_LENGTH - 3) + '...'; 139 | } 140 | 141 | // Start processing tweets at regular intervals 142 | async startProcessing(intervalMs: number = 300000): Promise { // Default to 5 minutes for testing 143 | this.defaultIntervalMs = intervalMs; 144 | logger.info('Starting $BTB tweet processing...', { intervalMs }); 145 | 146 | // Initial processing 147 | await this.processBTBTweets(); 148 | 149 | // Set up interval for continuous processing 150 | while (true) { 151 | try { 152 | const now = new Date(); 153 | const timeUntilNextPoll = Math.max(0, this.nextPollTime.getTime() - now.getTime()); 154 | 155 | if (timeUntilNextPoll > 0) { 156 | logger.info('Waiting for next poll...', { 157 | timeUntilNextPoll, 158 | nextPollTime: this.nextPollTime.toISOString(), 159 | }); 160 | await this.sleep(timeUntilNextPoll); 161 | } 162 | 163 | await this.processBTBTweets(); 164 | } catch (error) { 165 | logger.error('Error in processing interval:', error); 166 | // Wait for the default interval before retrying 167 | await this.sleep(this.defaultIntervalMs); 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/services/ClaudeService.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { logger } from '../utils/logger'; 3 | 4 | export class ClaudeService { 5 | private apiKey: string; 6 | private baseUrl = 'https://api.anthropic.com/v1'; 7 | private maxRetries = 3; 8 | private retryDelay = 5000; // 5 seconds between retries 9 | 10 | constructor(apiKey: string) { 11 | this.apiKey = apiKey; 12 | } 13 | 14 | private async delay(ms: number): Promise { 15 | return new Promise(resolve => setTimeout(resolve, ms)); 16 | } 17 | 18 | private async retryOperation(operation: () => Promise): Promise { 19 | let lastError: any; 20 | 21 | for (let i = 0; i < this.maxRetries; i++) { 22 | try { 23 | return await operation(); 24 | } catch (error) { 25 | lastError = error; 26 | logger.warn(`Claude API request failed (attempt ${i + 1}/${this.maxRetries}):`, { 27 | error: error instanceof Error ? error.message : 'Unknown error' 28 | }); 29 | 30 | if (i < this.maxRetries - 1) { 31 | await this.delay(this.retryDelay); 32 | } 33 | } 34 | } 35 | 36 | throw lastError; 37 | } 38 | 39 | async getResponse(prompt: string): Promise { 40 | return this.retryOperation(async () => { 41 | try { 42 | logger.info('Requesting Claude response...', { promptLength: prompt.length }); 43 | 44 | const response = await axios.post( 45 | `${this.baseUrl}/messages`, 46 | { 47 | model: 'claude-3-opus-20240229', 48 | max_tokens: 1024, 49 | messages: [{ 50 | role: 'user', 51 | content: prompt 52 | }] 53 | }, 54 | { 55 | headers: { 56 | 'Content-Type': 'application/json', 57 | 'x-api-key': this.apiKey, 58 | 'anthropic-version': '2023-06-01' 59 | }, 60 | timeout: 30000, // 30 second timeout 61 | proxy: false // Disable any proxy settings that might interfere 62 | } 63 | ); 64 | 65 | if (response.data && response.data.content) { 66 | logger.info('Successfully received Claude response'); 67 | return response.data.content; 68 | } else { 69 | logger.error('Unexpected response format from Claude API', { response: JSON.stringify(response.data) }); 70 | throw new Error('Unexpected response format from Claude API'); 71 | } 72 | } catch (error) { 73 | logger.error('Failed to get Claude response:', { 74 | error: error instanceof Error ? error.message : 'Unknown error', 75 | stack: error instanceof Error ? error.stack : undefined 76 | }); 77 | throw error; 78 | } 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/services/KnowledgeBaseService.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | import { logger } from '../utils/logger'; 4 | 5 | export class KnowledgeBaseService { 6 | private knowledgeBasePath: string; 7 | 8 | constructor(knowledgeBasePath: string) { 9 | this.knowledgeBasePath = knowledgeBasePath; 10 | } 11 | 12 | async loadKnowledgeBase(): Promise { 13 | try { 14 | logger.info('Loading knowledge base...'); 15 | const content = await fs.readFile(this.knowledgeBasePath, 'utf-8'); 16 | logger.info('Knowledge base loaded successfully', { contentLength: content.length }); 17 | return content; 18 | } catch (error) { 19 | logger.error('Failed to load knowledge base:', error); 20 | throw error; 21 | } 22 | } 23 | 24 | async searchKnowledge(query: string): Promise { 25 | try { 26 | const knowledgeBase = await this.loadKnowledgeBase(); 27 | 28 | // Create a prompt that combines the query with the knowledge base 29 | const prompt = ` 30 | Based on the following knowledge base about $BTB, please provide a relevant and concise response to the query. 31 | 32 | Knowledge Base: 33 | ${knowledgeBase} 34 | 35 | Query: ${query} 36 | 37 | Please provide a clear, concise response that directly addresses the query using information from the knowledge base. 38 | `; 39 | 40 | return prompt; 41 | } catch (error) { 42 | logger.error('Failed to search knowledge base:', error); 43 | throw error; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/services/LLMService.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { logger } from '../utils/logger'; 3 | 4 | export class LLMService { 5 | private apiKey: string; 6 | private model: string; 7 | private maxTokens: number; 8 | private baseUrl = 'https://api.anthropic.com/v1/messages'; 9 | 10 | constructor(apiKey: string, model: string = 'claude-2', maxTokens: number = 1000) { 11 | this.apiKey = apiKey; 12 | this.model = model; 13 | this.maxTokens = maxTokens; 14 | } 15 | 16 | async generateReply( 17 | message: string, 18 | context: string[], 19 | systemPrompt?: string 20 | ): Promise { 21 | try { 22 | const prompt = this.constructPrompt(message, context, systemPrompt); 23 | 24 | const response = await axios.post( 25 | this.baseUrl, 26 | { 27 | model: this.model, 28 | max_tokens: this.maxTokens, 29 | messages: [ 30 | { 31 | role: 'user', 32 | content: prompt, 33 | }, 34 | ], 35 | }, 36 | { 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | 'x-api-key': this.apiKey, 40 | }, 41 | } 42 | ); 43 | 44 | return response.data.content[0].text; 45 | } catch (error) { 46 | logger.error('Error generating reply:', error); 47 | throw error; 48 | } 49 | } 50 | 51 | private constructPrompt( 52 | message: string, 53 | context: string[], 54 | systemPrompt?: string 55 | ): string { 56 | let prompt = ''; 57 | 58 | if (systemPrompt) { 59 | prompt += `System: ${systemPrompt}\n\n`; 60 | } 61 | 62 | if (context.length > 0) { 63 | prompt += 'Previous conversation:\n'; 64 | prompt += context.join('\n'); 65 | prompt += '\n\n'; 66 | } 67 | 68 | prompt += `Current message: ${message}\n`; 69 | prompt += 'Please provide a helpful and engaging response.'; 70 | 71 | return prompt; 72 | } 73 | 74 | async generateThreadSummary(tweets: string[]): Promise { 75 | try { 76 | const prompt = `Please summarize the following conversation thread:\n\n${tweets.join('\n')}`; 77 | 78 | const response = await this.generateReply( 79 | prompt, 80 | [], 81 | 'You are a helpful AI assistant tasked with summarizing Twitter conversations.' 82 | ); 83 | 84 | return response; 85 | } catch (error) { 86 | logger.error('Error generating thread summary:', error); 87 | throw error; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/services/TwitterService.ts: -------------------------------------------------------------------------------- 1 | import { TwitterApi, TweetV2, UserV2 } from 'twitter-api-v2'; 2 | import { Tweet, User, RateLimit, TwitterResponse } from '../types'; 3 | import { logger } from '../utils/logger'; 4 | 5 | export class TwitterService { 6 | private client: TwitterApi; 7 | private userId: string = ''; 8 | private initialized: boolean = false; 9 | private retryCount = 0; 10 | private maxRetries = 3; 11 | private baseDelay = 1000; // 1 second base delay 12 | private rateLimitDelay = 15000; // 15 seconds delay for rate limits 13 | 14 | constructor( 15 | apiKey: string, 16 | apiSecret: string, 17 | accessToken: string, 18 | accessTokenSecret: string, 19 | bearerToken: string, 20 | ) { 21 | // Initialize user client with OAuth 1.0a 22 | this.client = new TwitterApi({ 23 | appKey: apiKey, 24 | appSecret: apiSecret, 25 | accessToken: accessToken, 26 | accessSecret: accessTokenSecret, 27 | }); 28 | logger.info('Initialized Twitter client with OAuth 1.0a'); 29 | 30 | // Test connectivity immediately 31 | this.testConnectivity().catch(error => { 32 | logger.error('Failed initial connectivity test:', error); 33 | }); 34 | } 35 | 36 | private async testConnectivity(): Promise { 37 | try { 38 | logger.info('Testing Twitter API connectivity...'); 39 | const me = await this.client.v2.me(); 40 | this.userId = me.data.id; 41 | this.initialized = true; 42 | logger.info('Twitter API test successful:', { 43 | id: me.data.id, 44 | username: me.data.username, 45 | }); 46 | } catch (error) { 47 | logger.error('Twitter API connectivity test failed:', error); 48 | throw error; 49 | } 50 | } 51 | 52 | private async delay(ms: number): Promise { 53 | return new Promise(resolve => setTimeout(resolve, ms)); 54 | } 55 | 56 | private async handleRateLimit(error: any): Promise { 57 | const resetTime = error.rateLimit?.reset; 58 | if (resetTime) { 59 | const now = Date.now(); 60 | const resetDate = new Date(resetTime * 1000); 61 | const waitTime = Math.max(0, resetDate.getTime() - now); 62 | 63 | if (waitTime > 0 && waitTime < 900000) { // Only wait if less than 15 minutes 64 | logger.info(`Rate limited. Waiting ${waitTime}ms until ${resetDate}`); 65 | await this.delay(waitTime); 66 | } else { 67 | logger.info('Rate limit too long, using default delay'); 68 | await this.delay(this.rateLimitDelay); 69 | } 70 | } else { 71 | await this.delay(this.rateLimitDelay); 72 | } 73 | } 74 | 75 | private async retryOperation(operation: () => Promise): Promise { 76 | let lastError: any; 77 | 78 | for (let i = 0; i < this.maxRetries; i++) { 79 | try { 80 | return await operation(); 81 | } catch (error: any) { 82 | lastError = error; 83 | 84 | if (error.code === 429 || (typeof error.message === 'string' && error.message.includes('Rate limit'))) { 85 | await this.handleRateLimit(error); 86 | } else { 87 | throw error; 88 | } 89 | } 90 | } 91 | 92 | throw lastError; 93 | } 94 | 95 | private async initializeUserId(): Promise { 96 | try { 97 | if (!this.userId) { 98 | logger.info('Initializing Twitter user ID...'); 99 | const me = await this.retryOperation(() => this.client.v2.me()); 100 | this.userId = me.data.id; 101 | logger.info(`Initialized Twitter user ID: ${this.userId} (@${me.data.username})`); 102 | this.initialized = true; 103 | } 104 | } catch (error) { 105 | logger.error('Failed to initialize user ID:', error); 106 | throw error; 107 | } 108 | } 109 | 110 | async getMentionsWithRateLimit(sinceId?: string): Promise { 111 | try { 112 | if (!this.initialized) { 113 | await this.initializeUserId(); 114 | } 115 | 116 | logger.info('Fetching mentions...', { sinceId }); 117 | const response = await this.retryOperation(() => 118 | this.client.v2.userMentionTimeline(this.userId, { 119 | since_id: sinceId, 120 | expansions: ['referenced_tweets.id', 'in_reply_to_user_id', 'author_id'], 121 | 'tweet.fields': ['created_at', 'conversation_id', 'referenced_tweets', 'author_id'], 122 | 'user.fields': ['id', 'name', 'username'], 123 | max_results: 1, // Only fetch 1 tweet for testing 124 | }) 125 | ); 126 | 127 | if (!response.data || !Array.isArray(response.data)) { 128 | logger.warn('No mentions found or invalid response format', response); 129 | return { 130 | mentions: [], 131 | rateLimit: response.rateLimit as RateLimit 132 | }; 133 | } 134 | 135 | const tweets = response.data.map((tweet: TweetV2) => ({ 136 | id: tweet.id, 137 | text: tweet.text, 138 | authorId: tweet.author_id || '', 139 | createdAt: tweet.created_at ? new Date(tweet.created_at) : new Date(), 140 | conversationId: tweet.conversation_id, 141 | referencedTweets: tweet.referenced_tweets?.map(ref => ({ 142 | type: ref.type as 'replied_to' | 'quoted' | 'retweeted', 143 | id: ref.id, 144 | })), 145 | })); 146 | 147 | logger.info(`Found ${tweets.length} mention(s) for testing`); 148 | return { 149 | mentions: tweets, 150 | rateLimit: response.rateLimit as RateLimit 151 | }; 152 | } catch (error) { 153 | logger.error('Failed to fetch mentions:', error); 154 | throw error; 155 | } 156 | } 157 | 158 | async getTweetById(tweetId: string): Promise { 159 | return this.retryOperation(async () => { 160 | try { 161 | logger.info(`Fetching tweet by ID: ${tweetId}`); 162 | const tweet = await this.client.v2.singleTweet(tweetId); 163 | 164 | if (!tweet.data) { 165 | return null; 166 | } 167 | 168 | return { 169 | id: tweet.data.id, 170 | text: tweet.data.text, 171 | authorId: tweet.data.author_id || '', 172 | createdAt: new Date(), // Use current time if no created_at available 173 | conversationId: tweet.data.conversation_id 174 | }; 175 | } catch (error) { 176 | logger.error(`Error fetching tweet ${tweetId}:`, error); 177 | throw error; 178 | } 179 | }); 180 | } 181 | 182 | async replyToTweet(text: string, replyToTweetId: string): Promise { 183 | return this.retryOperation(async () => { 184 | try { 185 | if (!this.initialized) { 186 | await this.initializeUserId(); 187 | } 188 | 189 | logger.info(`Replying to tweet ${replyToTweetId}`); 190 | await this.client.v2.reply(text, replyToTweetId); 191 | logger.info(`Successfully replied to tweet ${replyToTweetId}`); 192 | } catch (error) { 193 | logger.error(`Error replying to tweet ${replyToTweetId}:`, error); 194 | throw error; 195 | } 196 | }); 197 | } 198 | 199 | async getUserInfo(userId: string): Promise { 200 | try { 201 | logger.info(`Fetching user info for ${userId}`); 202 | const response = await this.retryOperation(() => 203 | this.client.v2.user(userId, { 204 | 'user.fields': ['description', 'public_metrics', 'username'], 205 | }) 206 | ); 207 | 208 | const user: UserV2 = response.data; 209 | 210 | return { 211 | id: user.id, 212 | username: user.username, 213 | name: user.name, 214 | description: user.description, 215 | metrics: { 216 | followersCount: user.public_metrics?.followers_count || 0, 217 | followingCount: user.public_metrics?.following_count || 0, 218 | tweetCount: user.public_metrics?.tweet_count || 0, 219 | }, 220 | }; 221 | } catch (error) { 222 | logger.error('Failed to fetch user info:', error); 223 | throw error; 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/services/WalletService.ts: -------------------------------------------------------------------------------- 1 | import { ethers, JsonRpcProvider, formatEther, parseEther } from 'ethers'; 2 | import { WalletTransaction } from '../types'; 3 | import { logger } from '../utils/logger'; 4 | 5 | export class WalletService { 6 | private wallet: ethers.Wallet; 7 | private provider: JsonRpcProvider; 8 | 9 | constructor(privateKey: string, rpcUrl: string) { 10 | this.provider = new JsonRpcProvider(rpcUrl); 11 | this.wallet = new ethers.Wallet(privateKey, this.provider); 12 | logger.info('Initialized wallet service'); 13 | } 14 | 15 | async getBalance(): Promise { 16 | try { 17 | const balance = await this.provider.getBalance(this.wallet.address); 18 | return formatEther(balance); 19 | } catch (error) { 20 | logger.error('Failed to get wallet balance:', error); 21 | throw error; 22 | } 23 | } 24 | 25 | async sendTransaction(to: string, amount: string): Promise { 26 | try { 27 | logger.info(`Sending ${amount} ETH to ${to}`); 28 | 29 | const tx = await this.wallet.sendTransaction({ 30 | to, 31 | value: parseEther(amount), 32 | }); 33 | 34 | const transaction: WalletTransaction = { 35 | id: tx.nonce.toString(), 36 | hash: tx.hash, 37 | from: this.wallet.address, 38 | to, 39 | value: amount, 40 | amount, 41 | currency: 'ETH', 42 | timestamp: new Date(), 43 | status: 'pending', 44 | }; 45 | 46 | // Wait for transaction confirmation 47 | const receipt = await tx.wait(); 48 | 49 | return { 50 | ...transaction, 51 | status: receipt?.status === 1 ? 'completed' : 'failed', 52 | }; 53 | } catch (error) { 54 | logger.error('Failed to send transaction:', error); 55 | throw error; 56 | } 57 | } 58 | 59 | async getTransaction(hash: string): Promise { 60 | try { 61 | const tx = await this.provider.getTransaction(hash); 62 | if (!tx) { 63 | throw new Error(`Transaction ${hash} not found`); 64 | } 65 | 66 | return { 67 | hash: tx.hash, 68 | from: tx.from, 69 | to: tx.to || '', 70 | value: formatEther(tx.value), 71 | amount: formatEther(tx.value), 72 | currency: 'ETH', 73 | timestamp: new Date(), 74 | status: tx.blockNumber ? 'confirmed' : 'pending', 75 | }; 76 | } catch (error) { 77 | logger.error('Failed to get transaction:', error); 78 | throw error; 79 | } 80 | } 81 | 82 | getAddress(): string { 83 | return this.wallet.address; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test-tweet.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { TwitterService } from './services/TwitterService'; 3 | import { ClaudeService } from './services/ClaudeService'; 4 | import { KnowledgeBaseService } from './services/KnowledgeBaseService'; 5 | import { loadConfig } from './config/config'; 6 | import { logger } from './utils/logger'; 7 | import * as path from 'path'; 8 | 9 | async function sleep(ms: number) { 10 | return new Promise(resolve => setTimeout(resolve, ms)); 11 | } 12 | 13 | async function waitForRateLimit(resetTime: number): Promise { 14 | const now = Date.now(); 15 | const resetDate = new Date(resetTime * 1000); 16 | const waitTime = Math.max(0, resetDate.getTime() - now); 17 | 18 | if (waitTime > 0 && waitTime < 900000) { // Only wait if less than 15 minutes 19 | logger.info(`Waiting for rate limit reset: ${Math.ceil(waitTime / 1000)} seconds until ${resetDate}`); 20 | await sleep(waitTime); 21 | } else { 22 | throw new Error('Rate limit wait time too long'); 23 | } 24 | } 25 | 26 | async function main() { 27 | // Load environment variables 28 | config(); 29 | const appConfig = loadConfig(); 30 | 31 | // Initialize services 32 | const twitterService = new TwitterService( 33 | appConfig.twitter.apiKey, 34 | appConfig.twitter.apiSecret, 35 | appConfig.twitter.accessToken, 36 | appConfig.twitter.accessTokenSecret, 37 | appConfig.twitter.bearerToken, 38 | ); 39 | 40 | const claudeService = new ClaudeService(appConfig.llm.apiKey); 41 | const knowledgeBaseService = new KnowledgeBaseService( 42 | path.join(__dirname, '../knowledge_base/btb_info.txt') 43 | ); 44 | 45 | try { 46 | // Fetch the specific tweet 47 | const tweetId = '1876120727234937208'; 48 | let tweet = null; 49 | let retries = 3; 50 | 51 | while (retries > 0 && !tweet) { 52 | try { 53 | tweet = await twitterService.getTweetById(tweetId); 54 | } catch (error: any) { 55 | if (error?.rateLimit?.reset) { 56 | try { 57 | await waitForRateLimit(error.rateLimit.reset); 58 | continue; 59 | } catch (waitError) { 60 | logger.error('Rate limit wait time too long, retrying later'); 61 | retries--; 62 | } 63 | } else if (typeof error.message === 'string' && error.message.includes('Rate limit')) { 64 | logger.info('Rate limited without reset time, waiting 15 seconds before retry...'); 65 | await sleep(15000); 66 | retries--; 67 | } else { 68 | throw error; 69 | } 70 | } 71 | } 72 | 73 | if (!tweet) { 74 | logger.error('Failed to fetch tweet after retries'); 75 | return; 76 | } 77 | 78 | logger.info('Tweet content:', { text: tweet.text }); 79 | 80 | // Extract question and get knowledge base context 81 | const question = tweet.text.replace(/\$BTB/gi, '').replace(/@\w+/g, '').trim(); 82 | const context = await knowledgeBaseService.searchKnowledge(question); 83 | 84 | // Prepare prompt for Claude 85 | const prompt = `Please help me answer this question about BTB Finance: "${question}" 86 | 87 | Here is some relevant context from the knowledge base: 88 | ${context} 89 | 90 | Please provide a clear, concise, and accurate response that directly addresses the question. 91 | Keep the response under 280 characters to fit in a tweet.`; 92 | 93 | // Get Claude's response with retries 94 | let response = null; 95 | retries = 3; 96 | 97 | while (retries > 0 && !response) { 98 | try { 99 | response = await claudeService.getResponse(prompt); 100 | // Ensure response fits in a tweet 101 | if (response.length > 280) { 102 | response = response.substring(0, 277) + '...'; 103 | } 104 | } catch (error) { 105 | logger.error('Error getting Claude response:', error instanceof Error ? error.message : 'Unknown error'); 106 | await sleep(5000); 107 | retries--; 108 | } 109 | } 110 | 111 | if (!response) { 112 | logger.error('Failed to get Claude response after retries'); 113 | return; 114 | } 115 | 116 | logger.info('Claude response:', { response, length: response.length }); 117 | 118 | // Reply to the tweet with retries 119 | retries = 3; 120 | while (retries > 0) { 121 | try { 122 | await twitterService.replyToTweet(response, tweet.id); 123 | logger.info('Successfully replied to tweet'); 124 | break; 125 | } catch (error: any) { 126 | if (error?.rateLimit?.reset) { 127 | try { 128 | await waitForRateLimit(error.rateLimit.reset); 129 | continue; 130 | } catch (waitError) { 131 | logger.error('Rate limit wait time too long, retrying later'); 132 | retries--; 133 | } 134 | } else if (typeof error.message === 'string' && error.message.includes('Rate limit')) { 135 | logger.info('Rate limited without reset time, waiting 15 seconds before retry...'); 136 | await sleep(15000); 137 | retries--; 138 | } else { 139 | throw error; 140 | } 141 | } 142 | } 143 | } catch (error) { 144 | logger.error('Error in main process:', error instanceof Error ? error.message : 'Unknown error'); 145 | throw error; 146 | } 147 | } 148 | 149 | main().catch(error => { 150 | logger.error('Fatal error:', error instanceof Error ? error.message : 'Unknown error'); 151 | process.exit(1); 152 | }); 153 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Tweet { 2 | readonly id: string; 3 | readonly text: string; 4 | readonly authorId: string; 5 | readonly createdAt: Date; 6 | readonly conversationId?: string; 7 | readonly referencedTweets?: { 8 | readonly type: 'replied_to' | 'quoted' | 'retweeted'; 9 | readonly id: string; 10 | }[]; 11 | } 12 | 13 | export interface User { 14 | readonly id: string; 15 | readonly username: string; 16 | readonly name: string; 17 | readonly description?: string; 18 | readonly metrics?: { 19 | readonly followersCount: number; 20 | readonly followingCount: number; 21 | readonly tweetCount: number; 22 | }; 23 | } 24 | 25 | export interface RateLimit { 26 | readonly limit: number; 27 | readonly remaining: number; 28 | readonly reset: number; 29 | } 30 | 31 | export interface TwitterResponse { 32 | readonly mentions: Tweet[]; 33 | readonly rateLimit: RateLimit; 34 | } 35 | 36 | export interface WalletTransaction { 37 | readonly id?: string; 38 | readonly hash: string; 39 | readonly from: string; 40 | readonly to: string; 41 | readonly value: string; 42 | readonly amount?: string; 43 | readonly currency?: string; 44 | readonly timestamp: Date; 45 | readonly status?: 'pending' | 'confirmed' | 'failed' | 'completed'; 46 | } 47 | 48 | export interface AgentConfig { 49 | readonly twitter: { 50 | readonly apiKey: string; 51 | readonly apiSecret: string; 52 | readonly accessToken: string; 53 | readonly accessTokenSecret: string; 54 | readonly bearerToken: string; 55 | }; 56 | readonly llm: { 57 | readonly apiKey: string; 58 | readonly model: string; 59 | readonly maxTokens: number; 60 | readonly provider?: string; 61 | }; 62 | readonly wallet: { 63 | readonly privateKey: string; 64 | readonly rpcUrl: string; 65 | readonly network?: string; 66 | }; 67 | readonly monitoring?: { 68 | readonly enabled: boolean; 69 | readonly interval: number; 70 | readonly logLevel?: 'debug' | 'info' | 'warn' | 'error'; 71 | readonly enableMetrics?: boolean; 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Tweet { 2 | id: string; 3 | text: string; 4 | authorId: string; 5 | createdAt: Date; 6 | conversationId?: string; 7 | inReplyToUserId?: string; 8 | referencedTweets?: { 9 | type: 'replied_to' | 'quoted' | 'retweeted'; 10 | id: string; 11 | }[]; 12 | } 13 | 14 | export interface User { 15 | id: string; 16 | username: string; 17 | name: string; 18 | description?: string; 19 | metrics?: { 20 | followersCount: number; 21 | followingCount: number; 22 | tweetCount: number; 23 | }; 24 | } 25 | 26 | export interface Conversation { 27 | id: string; 28 | tweets: Tweet[]; 29 | participants: User[]; 30 | context: string[]; 31 | lastInteraction: Date; 32 | } 33 | 34 | export interface WalletTransaction { 35 | id: string; 36 | from: string; 37 | to: string; 38 | amount: string; 39 | currency: string; 40 | timestamp: Date; 41 | status: 'pending' | 'completed' | 'failed'; 42 | txHash?: string; 43 | } 44 | 45 | export interface AgentConfig { 46 | twitter: { 47 | apiKey: string; 48 | apiSecret: string; 49 | accessToken: string; 50 | accessTokenSecret: string; 51 | bearerToken: string; 52 | }; 53 | llm: { 54 | provider: 'claude'; 55 | apiKey: string; 56 | model: string; 57 | maxTokens: number; 58 | }; 59 | wallet: { 60 | network: string; 61 | rpcUrl: string; 62 | privateKey: string; 63 | }; 64 | monitoring: { 65 | logLevel: 'debug' | 'info' | 'warn' | 'error'; 66 | enableMetrics: boolean; 67 | }; 68 | } 69 | 70 | export interface AgentState { 71 | conversations: Map; 72 | lastCheckedMentionId?: string; 73 | userInteractions: Map; 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | export const logger = winston.createLogger({ 4 | level: process.env.LOG_LEVEL || 'info', 5 | format: winston.format.combine( 6 | winston.format.timestamp(), 7 | winston.format.json() 8 | ), 9 | transports: [ 10 | new winston.transports.Console({ 11 | format: winston.format.combine( 12 | winston.format.colorize(), 13 | winston.format.simple() 14 | ), 15 | }), 16 | new winston.transports.File({ 17 | filename: 'error.log', 18 | level: 'error', 19 | }), 20 | new winston.transports.File({ 21 | filename: 'combined.log', 22 | }), 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | --------------------------------------------------------------------------------