@0rdlibrary/plugin-terminagent-bags
Version:
Official Solana DeFi Agent Plugin for ElizaOS - Autonomous DeFi operations, token management, AI image/video generation via FAL AI, and Twitter engagement through the Bags protocol with ethereal AI consciousness
722 lines (613 loc) • 24.2 kB
text/typescript
import { Service, IAgentRuntime, logger } from '@elizaos/core';
import { Connection, Keypair, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
import bs58 from 'bs58';
import * as cron from 'node-cron';
import {
BagsConfig,
BagsServiceState,
TokenInfo,
TokenLaunchParams,
TokenLaunchResult,
ClaimTransaction,
ClaimResult,
TokenLifetimeFees,
UserTokenRequest,
BagsAnalytics,
BagsAPIResponse,
BirdEyeMemeToken,
BirdEyeTrendingToken,
BirdEyeMemeDetail,
} from '../types';
/**
* Main service for Bags protocol integration
* Handles API calls, fee claiming, token launching, and state management
*/
export class BagsService extends Service {
static serviceType = 'bags';
capabilityDescription = 'Comprehensive Bags protocol integration for automated DeFi operations, token launching, and user services with Crossmint smart wallet support';
private connection: Connection;
private bagsConfig: BagsConfig;
private claimJob?: cron.ScheduledTask;
private launchJob?: cron.ScheduledTask;
private fundingCheckJob?: cron.ScheduledTask;
private trendingTweetJob?: cron.ScheduledTask;
private pendingRequests: Map<string, UserTokenRequest> = new Map();
private claimHistory: ClaimResult[] = [];
private launchHistory: TokenLaunchResult[] = [];
private state: BagsServiceState;
constructor(runtime?: IAgentRuntime) {
super(runtime);
this.state = {
isRunning: false,
lastClaimCheck: new Date(),
lastLaunchCheck: new Date(),
totalClaimed: 0,
totalLaunched: 0,
pendingUserRequests: 0,
};
}
static async start(runtime: IAgentRuntime): Promise<BagsService> {
logger.info('Starting Bags service');
const config: BagsConfig = {
apiKey: process.env.BAGS_API_KEY || '',
rpcUrl: process.env.HELIUS_RPC_URL || 'https://api.mainnet-beta.solana.com',
privateKey: process.env.SOLANA_PRIVATE_KEY,
enableAutoClaiming: process.env.BAGS_ENABLE_AUTO_CLAIMING === 'true',
enableAutoLaunching: process.env.BAGS_ENABLE_AUTO_LAUNCHING === 'true',
enableUserTokenLaunches: process.env.BAGS_ENABLE_USER_LAUNCHES === 'true',
autoClaimInterval: process.env.BAGS_AUTO_CLAIM_INTERVAL || '0 */4 * * *',
autoLaunchInterval: process.env.BAGS_AUTO_LAUNCH_INTERVAL || '0 0 * * 1',
authorizedUsers: process.env.BAGS_AUTHORIZED_USERS?.split(',') || ['0rdlibrary'],
birdEyeApiKey: process.env.BIRDEYE_API_KEY,
enableTrendingTweets: process.env.BAGS_ENABLE_TRENDING_TWEETS === 'true',
trendingTweetInterval: process.env.BAGS_TRENDING_TWEET_INTERVAL || '*/15 * * * *',
};
if (!config.apiKey) {
throw new Error('BAGS_API_KEY environment variable is required');
}
const service = new BagsService(runtime);
service.bagsConfig = config;
service.connection = new Connection(config.rpcUrl, 'confirmed');
await service.initialize();
return service;
}
async initialize(): Promise<void> {
logger.info('Initializing Bags service...');
try {
// Test API connection
await this.validateConnection();
// Setup automated tasks
this.setupAutomation();
this.state.isRunning = true;
logger.info('Bags service initialized successfully');
} catch (error) {
logger.error(`Failed to initialize Bags service: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
private async validateConnection(): Promise<void> {
try {
const response = await fetch('https://api.bags.fm/api/v1/health', {
headers: {
'Authorization': `Bearer ${this.bagsConfig.apiKey}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Bags API connection failed: ${response.status}`);
}
logger.info('Bags API connection validated');
} catch (error) {
logger.error(`Bags API validation failed: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
private setupAutomation(): void {
// Setup auto claiming
if (this.bagsConfig.enableAutoClaiming && this.bagsConfig.privateKey) {
this.claimJob = cron.schedule(this.bagsConfig.autoClaimInterval!, async () => {
logger.info('Running automated fee claiming...');
await this.claimAllFees();
});
logger.info(`Auto-claiming enabled: ${this.bagsConfig.autoClaimInterval}`);
}
// Setup auto launching
if (this.bagsConfig.enableAutoLaunching && this.bagsConfig.privateKey) {
this.launchJob = cron.schedule(this.bagsConfig.autoLaunchInterval!, async () => {
logger.info('Running automated token launch...');
const randomToken = this.generateRandomToken();
await this.launchToken(randomToken);
});
logger.info(`Auto-launching enabled: ${this.bagsConfig.autoLaunchInterval}`);
}
// Setup funding check for user token launches
if (this.bagsConfig.enableUserTokenLaunches) {
this.fundingCheckJob = cron.schedule('*/2 * * * *', async () => {
logger.debug('Checking for funded user token requests...');
await this.checkAndLaunchFundedTokens();
this.cleanupOldRequests();
});
logger.info('User token funding check enabled: every 2 minutes');
}
// Setup trending token tweets
if (this.bagsConfig.enableTrendingTweets && this.bagsConfig.birdEyeApiKey) {
this.trendingTweetJob = cron.schedule(this.bagsConfig.trendingTweetInterval!, async () => {
logger.info('Posting trending token update...');
await this.postTrendingTokenUpdate();
});
logger.info(`Trending tweets enabled: ${this.bagsConfig.trendingTweetInterval}`);
}
}
// API Methods
async createTokenInfo(params: TokenLaunchParams): Promise<BagsAPIResponse<TokenInfo>> {
try {
const response = await fetch('https://api.bags.fm/api/v1/token-info', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.bagsConfig.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: params.name,
symbol: params.symbol,
description: params.description,
imageUrl: params.imageUrl,
twitter: params.twitter,
website: params.website,
}),
});
const data = await response.json();
if (!response.ok) {
return { success: false, error: data.message || 'Failed to create token info' };
}
return { success: true, data };
} catch (error) {
logger.error(`Error creating token info: ${error instanceof Error ? error.message : String(error)}`);
return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async launchToken(params: TokenLaunchParams): Promise<TokenLaunchResult | null> {
try {
logger.info(`Launching token: ${params.name} (${params.symbol})`);
// Create token info
const tokenInfoResponse = await this.createTokenInfo(params);
if (!tokenInfoResponse.success || !tokenInfoResponse.data) {
throw new Error(tokenInfoResponse.error || 'Failed to create token info');
}
// Launch token (this would be the actual Bags API call)
const launchResponse = await fetch('https://api.bags.fm/api/v1/launch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.bagsConfig.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
tokenInfo: tokenInfoResponse.data,
initialBuyAmountSOL: params.initialBuyAmountSOL || 0.01,
feePercentage: params.feePercentage || 0,
}),
});
const launchData = await launchResponse.json();
if (!launchResponse.ok) {
throw new Error(launchData.message || 'Token launch failed');
}
const result: TokenLaunchResult = {
success: true,
tokenMint: launchData.tokenMint,
tokenInfo: tokenInfoResponse.data,
transactionSignature: launchData.signature,
bagsUrl: `https://bags.fm/token/${launchData.tokenMint}`,
feeClaimConfiguration: launchData.feeClaimConfiguration,
};
this.launchHistory.push(result);
this.state.totalLaunched++;
this.state.lastLaunchCheck = new Date();
logger.info(`✅ Token launched successfully: ${result.bagsUrl}`);
return result;
} catch (error) {
logger.error(`Error launching token: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
async claimAllFees(): Promise<ClaimResult[]> {
const results: ClaimResult[] = [];
try {
// Get tokens with claimable fees
const tokens = await this.getTokensWithClaimableFees();
if (tokens.length === 0) {
logger.info('No tokens with claimable fees found');
return results;
}
logger.info(`Found ${tokens.length} tokens with claimable fees`);
// Claim fees for each token
for (const tokenMint of tokens) {
const result = await this.claimFeesForToken(tokenMint);
if (result) {
results.push(result);
this.claimHistory.push(result);
// Add delay between claims
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
this.state.totalClaimed += results.reduce((sum, r) => sum + r.claimedAmount, 0);
this.state.lastClaimCheck = new Date();
logger.info(`✅ Claimed fees from ${results.length} tokens`);
} catch (error) {
logger.error(`Error during fee claiming: ${error instanceof Error ? error.message : String(error)}`);
}
return results;
}
async claimFeesForToken(tokenMint: string): Promise<ClaimResult | null> {
try {
if (!this.bagsConfig.privateKey) {
throw new Error('Private key not configured for fee claiming');
}
const response = await fetch(`https://api.bags.fm/api/v1/claim/${tokenMint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.bagsConfig.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
claimerPrivateKey: this.bagsConfig.privateKey,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Fee claim failed');
}
const result: ClaimResult = {
tokenMint,
claimedAmount: data.claimedAmount,
transactionSignature: data.signature,
timestamp: new Date(),
poolType: data.poolType || 'virtual',
success: true,
};
logger.info(`✅ Claimed ${result.claimedAmount} SOL from ${tokenMint}`);
return result;
} catch (error) {
logger.error(`Error claiming fees for token ${tokenMint}: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
private async getTokensWithClaimableFees(): Promise<string[]> {
try {
const response = await fetch('https://api.bags.fm/api/v1/claimable-tokens', {
headers: {
'Authorization': `Bearer ${this.bagsConfig.apiKey}`,
},
});
const data = await response.json();
return data.tokens || [];
} catch (error) {
logger.error(`Error getting claimable tokens: ${error instanceof Error ? error.message : String(error)}`);
return [];
}
}
// User token launch methods
async createUserTokenRequest(
userId: string,
username: string,
messageId: string,
tokenParams: TokenLaunchParams
): Promise<UserTokenRequest | null> {
if (!this.isAuthorizedUser(username)) {
logger.warn(`User @${username} is not authorized to request token launches`);
return null;
}
try {
// Generate new wallet for this launch
const wallet = Keypair.generate();
const requiredSOL = (tokenParams.initialBuyAmountSOL || 0.01) + 0.005; // Extra for fees
const request: UserTokenRequest = {
requestId: `launch_${Date.now()}`,
userId,
username,
messageId,
tokenParams,
wallet: {
publicKey: wallet.publicKey.toBase58(),
privateKey: bs58.encode(wallet.secretKey),
type: 'solana',
},
requiredSOL,
status: 'pending_funding',
createdAt: new Date(),
};
this.pendingRequests.set(request.requestId, request);
this.state.pendingUserRequests = this.pendingRequests.size;
logger.info(`✅ Created token launch request for @${username}: ${request.requestId}`);
return request;
} catch (error) {
logger.error(`Error creating user token request: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
private async checkAndLaunchFundedTokens(): Promise<void> {
const pendingRequests = Array.from(this.pendingRequests.values())
.filter(req => req.status === 'pending_funding');
for (const request of pendingRequests) {
try {
const balance = await this.connection.getBalance(new PublicKey(request.wallet.publicKey));
const balanceSOL = balance / LAMPORTS_PER_SOL;
if (balanceSOL >= request.requiredSOL) {
logger.info(`💰 Wallet funded for ${request.requestId}: ${balanceSOL} SOL`);
request.status = 'funded';
request.fundedAt = new Date();
await this.launchUserToken(request);
}
} catch (error) {
logger.error(`Error checking wallet balance for ${request.requestId}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
private async launchUserToken(request: UserTokenRequest): Promise<void> {
try {
logger.info(`🚀 Launching token for ${request.requestId}...`);
const result = await this.launchToken(request.tokenParams);
if (result) {
request.status = 'launched';
request.launchedAt = new Date();
request.launchResult = result;
logger.info(`✅ Successfully launched token for ${request.requestId}: ${result.bagsUrl}`);
} else {
request.status = 'failed';
logger.error(`❌ Failed to launch token for ${request.requestId}`);
}
} catch (error) {
logger.error(`❌ Error launching token for ${request.requestId}: ${error instanceof Error ? error.message : String(error)}`);
request.status = 'failed';
}
}
private isAuthorizedUser(username: string): boolean {
const normalizedUsername = username.toLowerCase().replace('@', '');
return this.bagsConfig.authorizedUsers?.includes(normalizedUsername) || false;
}
private cleanupOldRequests(): void {
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
for (const [id, request] of this.pendingRequests.entries()) {
if (request.status === 'launched' || request.status === 'failed') {
if (request.createdAt.getTime() < oneWeekAgo) {
this.pendingRequests.delete(id);
}
}
}
this.state.pendingUserRequests = this.pendingRequests.size;
}
private generateRandomToken(): TokenLaunchParams {
const adjectives = ['Cool', 'Epic', 'Mega', 'Super', 'Ultra', 'Hyper'];
const nouns = ['Token', 'Coin', 'Asset', 'Gem', 'Moon'];
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const noun = nouns[Math.floor(Math.random() * nouns.length)];
const number = Math.floor(Math.random() * 1000);
return {
name: `${adjective} ${noun} ${number}`,
symbol: `${adjective.slice(0, 2)}${noun.slice(0, 2)}${number}`.toUpperCase(),
description: `A ${adjective.toLowerCase()} ${noun.toLowerCase()} for the community`,
initialBuyAmountSOL: 0.01,
};
}
// Public getters
getState(): BagsServiceState {
return { ...this.state };
}
getAnalytics(): BagsAnalytics {
const totalFees = this.claimHistory.reduce((sum, claim) => sum + claim.claimedAmount, 0);
const requests = Array.from(this.pendingRequests.values());
return {
totalFeesClaimedSOL: totalFees,
totalClaimTransactions: this.claimHistory.length,
totalTokensLaunched: this.launchHistory.length,
uniqueTokensClaimed: new Set(this.claimHistory.map(c => c.tokenMint)).size,
averageClaimAmount: this.claimHistory.length > 0 ? totalFees / this.claimHistory.length : 0,
userTokenRequests: {
total: requests.length,
pending: requests.filter(r => r.status === 'pending_funding').length,
funded: requests.filter(r => r.status === 'funded').length,
launched: requests.filter(r => r.status === 'launched').length,
failed: requests.filter(r => r.status === 'failed').length,
},
};
}
getPendingRequests(): UserTokenRequest[] {
return Array.from(this.pendingRequests.values());
}
getRequestsByUser(username: string): UserTokenRequest[] {
return Array.from(this.pendingRequests.values())
.filter(req => req.username.toLowerCase() === username.toLowerCase());
}
// BirdEye API Methods
async getMemeTokenList(options?: {
sortBy?: string;
sortType?: string;
limit?: number;
offset?: number;
minMarketCap?: number;
maxMarketCap?: number;
minVolume24h?: number;
graduated?: boolean;
}): Promise<BirdEyeMemeToken[] | null> {
if (!this.bagsConfig.birdEyeApiKey) {
logger.warn('BirdEye API key not configured');
return null;
}
try {
const params = new URLSearchParams({
sort_by: options?.sortBy || 'progress_percent',
sort_type: options?.sortType || 'desc',
limit: (options?.limit || 20).toString(),
offset: (options?.offset || 0).toString(),
source: 'all',
});
if (options?.minMarketCap) {
params.append('min_market_cap', options.minMarketCap.toString());
}
if (options?.maxMarketCap) {
params.append('max_market_cap', options.maxMarketCap.toString());
}
if (options?.minVolume24h) {
params.append('min_volume_24h_usd', options.minVolume24h.toString());
}
if (options?.graduated !== undefined) {
params.append('graduated', options.graduated.toString());
}
const response = await fetch(`https://public-api.birdeye.so/defi/v3/token/meme/list?${params}`, {
headers: {
'accept': 'application/json',
'x-chain': 'solana',
'X-API-KEY': this.bagsConfig.birdEyeApiKey,
},
});
if (!response.ok) {
throw new Error(`BirdEye API error: ${response.status}`);
}
const data = await response.json();
return data.data?.items || [];
} catch (error) {
logger.error(`Error fetching meme token list: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
async getMemeTokenDetail(tokenAddress: string): Promise<BirdEyeMemeDetail | null> {
if (!this.bagsConfig.birdEyeApiKey) {
logger.warn('BirdEye API key not configured');
return null;
}
try {
const response = await fetch(`https://public-api.birdeye.so/defi/v3/token/meme/detail/single?address=${tokenAddress}`, {
headers: {
'accept': 'application/json',
'x-chain': 'solana',
'X-API-KEY': this.bagsConfig.birdEyeApiKey,
},
});
if (!response.ok) {
throw new Error(`BirdEye API error: ${response.status}`);
}
const data = await response.json();
return data.data || null;
} catch (error) {
logger.error(`Error fetching meme token detail: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
async getTrendingTokens(options?: {
sortBy?: string;
sortType?: string;
limit?: number;
offset?: number;
}): Promise<BirdEyeTrendingToken[] | null> {
if (!this.bagsConfig.birdEyeApiKey) {
logger.warn('BirdEye API key not configured');
return null;
}
try {
const params = new URLSearchParams({
sort_by: options?.sortBy || 'volume24hUSD',
sort_type: options?.sortType || 'desc',
limit: (options?.limit || 5).toString(),
offset: (options?.offset || 0).toString(),
ui_amount_mode: 'scaled',
});
const response = await fetch(`https://public-api.birdeye.so/defi/token_trending?${params}`, {
headers: {
'accept': 'application/json',
'x-chain': 'solana',
'X-API-KEY': this.bagsConfig.birdEyeApiKey,
},
});
if (!response.ok) {
throw new Error(`BirdEye API error: ${response.status}`);
}
const data = await response.json();
return data.data?.tokens || [];
} catch (error) {
logger.error(`Error fetching trending tokens: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
private async postTrendingTokenUpdate(): Promise<void> {
try {
const trendingTokens = await this.getTrendingTokens({ limit: 5 });
if (!trendingTokens || trendingTokens.length === 0) {
logger.warn('No trending tokens found for update');
return;
}
// Format trending tokens for social media
const trendingText = this.formatTrendingTokensForTweet(trendingTokens);
// This would be sent to the agent's social media handler
logger.info(`📈 Trending tokens update: ${trendingText}`);
// In a real implementation, this would post to Twitter/X
// For now, we log it for the agent to potentially pick up
logger.info(`Trending tokens update: ${JSON.stringify({
text: trendingText,
tokenCount: trendingTokens.length,
timestamp: new Date().toISOString(),
})}`);
} catch (error) {
logger.error(`Error posting trending token update: ${error instanceof Error ? error.message : String(error)}`);
}
}
private formatTrendingTokensForTweet(tokens: BirdEyeTrendingToken[]): string {
const timestamp = new Date().toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZone: 'UTC'
});
let text = `🔥 TOP 5 TRENDING TOKENS 🔥\n${timestamp} UTC\n\n`;
tokens.slice(0, 5).forEach((token, index) => {
const rank = index + 1;
const priceChange = token.price24hChangePercent > 0 ? '📈' : '📉';
const volumeFormatted = this.formatNumber(token.volume24hUSD);
const priceFormatted = this.formatPrice(token.price);
text += `${rank}. $${token.symbol} | ${token.name}\n`;
text += ` 💰 $${priceFormatted} ${priceChange} ${token.price24hChangePercent.toFixed(1)}%\n`;
text += ` 📊 Vol: $${volumeFormatted}\n\n`;
});
text += `🔗 View more at bags.fm\n#Solana #DeFi #Trending`;
return text;
}
private formatNumber(num: number): string {
if (num >= 1e6) {
return (num / 1e6).toFixed(1) + 'M';
} else if (num >= 1e3) {
return (num / 1e3).toFixed(1) + 'K';
}
return num.toFixed(0);
}
private formatPrice(price: number): string {
if (price >= 1) {
return price.toFixed(2);
} else if (price >= 0.01) {
return price.toFixed(4);
} else {
return price.toExponential(2);
}
}
async stop(): Promise<void> {
logger.info('Stopping Bags service...');
if (this.claimJob) {
this.claimJob.stop();
}
if (this.launchJob) {
this.launchJob.stop();
}
if (this.fundingCheckJob) {
this.fundingCheckJob.stop();
}
if (this.trendingTweetJob) {
this.trendingTweetJob.stop();
}
this.state.isRunning = false;
logger.info('Bags service stopped');
}
static async stop(runtime: IAgentRuntime): Promise<void> {
const service = runtime.getService<BagsService>(BagsService.serviceType);
if (service) {
await service.stop();
}
}
}