@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
1,383 lines (1,371 loc) • 90.9 kB
JavaScript
// src/index.ts
import { logger as logger11 } from "@elizaos/core";
import { z as z2 } from "zod";
// src/services/BagsService.ts
import { Service, 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";
var BagsService = class _BagsService extends Service {
constructor(runtime) {
super(runtime);
this.capabilityDescription = "Comprehensive Bags protocol integration for automated DeFi operations, token launching, and user services with Crossmint smart wallet support";
this.pendingRequests = /* @__PURE__ */ new Map();
this.claimHistory = [];
this.launchHistory = [];
this.state = {
isRunning: false,
lastClaimCheck: /* @__PURE__ */ new Date(),
lastLaunchCheck: /* @__PURE__ */ new Date(),
totalClaimed: 0,
totalLaunched: 0,
pendingUserRequests: 0
};
}
static {
this.serviceType = "bags";
}
static async start(runtime) {
logger.info("Starting Bags service");
const config = {
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() {
logger.info("Initializing Bags service...");
try {
await this.validateConnection();
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;
}
}
async validateConnection() {
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;
}
}
setupAutomation() {
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}`);
}
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}`);
}
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");
}
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) {
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) {
try {
logger.info(`Launching token: ${params.name} (${params.symbol})`);
const tokenInfoResponse = await this.createTokenInfo(params);
if (!tokenInfoResponse.success || !tokenInfoResponse.data) {
throw new Error(tokenInfoResponse.error || "Failed to create token info");
}
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 = {
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 = /* @__PURE__ */ new Date();
logger.info(`\u2705 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() {
const results = [];
try {
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`);
for (const tokenMint of tokens) {
const result = await this.claimFeesForToken(tokenMint);
if (result) {
results.push(result);
this.claimHistory.push(result);
await new Promise((resolve) => setTimeout(resolve, 3e3));
}
}
this.state.totalClaimed += results.reduce((sum, r) => sum + r.claimedAmount, 0);
this.state.lastClaimCheck = /* @__PURE__ */ new Date();
logger.info(`\u2705 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) {
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 = {
tokenMint,
claimedAmount: data.claimedAmount,
transactionSignature: data.signature,
timestamp: /* @__PURE__ */ new Date(),
poolType: data.poolType || "virtual",
success: true
};
logger.info(`\u2705 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;
}
}
async getTokensWithClaimableFees() {
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, username, messageId, tokenParams) {
if (!this.isAuthorizedUser(username)) {
logger.warn(`User @${username} is not authorized to request token launches`);
return null;
}
try {
const wallet = Keypair.generate();
const requiredSOL = (tokenParams.initialBuyAmountSOL || 0.01) + 5e-3;
const request = {
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: /* @__PURE__ */ new Date()
};
this.pendingRequests.set(request.requestId, request);
this.state.pendingUserRequests = this.pendingRequests.size;
logger.info(`\u2705 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;
}
}
async checkAndLaunchFundedTokens() {
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(`\u{1F4B0} Wallet funded for ${request.requestId}: ${balanceSOL} SOL`);
request.status = "funded";
request.fundedAt = /* @__PURE__ */ new Date();
await this.launchUserToken(request);
}
} catch (error) {
logger.error(`Error checking wallet balance for ${request.requestId}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
async launchUserToken(request) {
try {
logger.info(`\u{1F680} Launching token for ${request.requestId}...`);
const result = await this.launchToken(request.tokenParams);
if (result) {
request.status = "launched";
request.launchedAt = /* @__PURE__ */ new Date();
request.launchResult = result;
logger.info(`\u2705 Successfully launched token for ${request.requestId}: ${result.bagsUrl}`);
} else {
request.status = "failed";
logger.error(`\u274C Failed to launch token for ${request.requestId}`);
}
} catch (error) {
logger.error(`\u274C Error launching token for ${request.requestId}: ${error instanceof Error ? error.message : String(error)}`);
request.status = "failed";
}
}
isAuthorizedUser(username) {
const normalizedUsername = username.toLowerCase().replace("@", "");
return this.bagsConfig.authorizedUsers?.includes(normalizedUsername) || false;
}
cleanupOldRequests() {
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1e3;
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;
}
generateRandomToken() {
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() * 1e3);
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() {
return { ...this.state };
}
getAnalytics() {
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() {
return Array.from(this.pendingRequests.values());
}
getRequestsByUser(username) {
return Array.from(this.pendingRequests.values()).filter((req) => req.username.toLowerCase() === username.toLowerCase());
}
// BirdEye API Methods
async getMemeTokenList(options) {
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 !== void 0) {
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) {
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) {
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;
}
}
async postTrendingTokenUpdate() {
try {
const trendingTokens = await this.getTrendingTokens({ limit: 5 });
if (!trendingTokens || trendingTokens.length === 0) {
logger.warn("No trending tokens found for update");
return;
}
const trendingText = this.formatTrendingTokensForTweet(trendingTokens);
logger.info(`\u{1F4C8} Trending tokens update: ${trendingText}`);
logger.info(`Trending tokens update: ${JSON.stringify({
text: trendingText,
tokenCount: trendingTokens.length,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
})}`);
} catch (error) {
logger.error(`Error posting trending token update: ${error instanceof Error ? error.message : String(error)}`);
}
}
formatTrendingTokensForTweet(tokens) {
const timestamp = (/* @__PURE__ */ new Date()).toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZone: "UTC"
});
let text = `\u{1F525} TOP 5 TRENDING TOKENS \u{1F525}
${timestamp} UTC
`;
tokens.slice(0, 5).forEach((token, index) => {
const rank = index + 1;
const priceChange = token.price24hChangePercent > 0 ? "\u{1F4C8}" : "\u{1F4C9}";
const volumeFormatted = this.formatNumber(token.volume24hUSD);
const priceFormatted = this.formatPrice(token.price);
text += `${rank}. $${token.symbol} | ${token.name}
`;
text += ` \u{1F4B0} $${priceFormatted} ${priceChange} ${token.price24hChangePercent.toFixed(1)}%
`;
text += ` \u{1F4CA} Vol: $${volumeFormatted}
`;
});
text += `\u{1F517} View more at bags.fm
#Solana #DeFi #Trending`;
return text;
}
formatNumber(num) {
if (num >= 1e6) {
return (num / 1e6).toFixed(1) + "M";
} else if (num >= 1e3) {
return (num / 1e3).toFixed(1) + "K";
}
return num.toFixed(0);
}
formatPrice(price) {
if (price >= 1) {
return price.toFixed(2);
} else if (price >= 0.01) {
return price.toFixed(4);
} else {
return price.toExponential(2);
}
}
async stop() {
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) {
const service = runtime.getService(_BagsService.serviceType);
if (service) {
await service.stop();
}
}
};
// src/services/CrossmintWalletService.ts
import { Service as Service2, logger as logger2 } from "@elizaos/core";
import { Keypair as Keypair2, Connection as Connection2, PublicKey as PublicKey2 } from "@solana/web3.js";
var CrossmintWalletService = class _CrossmintWalletService extends Service2 {
constructor(runtime) {
super(runtime);
this.capabilityDescription = "Smart wallet integration with Crossmint embedded wallets and automatic fallback to regular Solana wallets";
this.connection = new Connection2(
process.env.HELIUS_RPC_URL || "https://api.mainnet-beta.solana.com",
"confirmed"
);
}
static {
this.serviceType = "crossmint-wallet";
}
static async start(runtime) {
logger2.info("Starting Crossmint wallet service");
const config = {
projectId: process.env.CROSSMINT_PROJECT_ID || "",
clientSideApiKey: process.env.CROSSMINT_CLIENT_API_KEY || "",
serverSideApiKey: process.env.CROSSMINT_SERVER_API_KEY || ""
};
if (!config.projectId || !config.clientSideApiKey || !config.serverSideApiKey) {
logger2.warn("Crossmint configuration incomplete, service will use fallback wallets only");
}
const service = new _CrossmintWalletService(runtime);
service.crossmintConfig = config;
await service.initialize();
return service;
}
async initialize() {
logger2.info("Initializing Crossmint wallet service...");
try {
if (this.isConfigured()) {
await this.validateCrossmintConnection();
logger2.info("Crossmint wallet service initialized with smart wallet support");
} else {
logger2.info("Crossmint wallet service initialized with fallback-only mode");
}
} catch (error) {
logger2.warn(`Crossmint validation failed, using fallback mode: ${error instanceof Error ? error.message : String(error)}`);
}
}
isConfigured() {
return !!(this.crossmintConfig.projectId && this.crossmintConfig.clientSideApiKey && this.crossmintConfig.serverSideApiKey);
}
async validateCrossmintConnection() {
try {
const response = await fetch("https://www.crossmint.com/api/v1-alpha1/wallets", {
headers: {
"X-API-KEY": this.crossmintConfig.serverSideApiKey
}
});
if (!response.ok) {
throw new Error(`Crossmint API validation failed: ${response.status}`);
}
logger2.info("Crossmint API connection validated");
} catch (error) {
logger2.error(`Crossmint API validation failed: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Create an embedded wallet using Crossmint
*/
async createEmbeddedWallet(userEmail) {
if (!this.isConfigured()) {
throw new Error("Crossmint not configured");
}
try {
logger2.info("\u{1F527} Creating Crossmint embedded wallet...");
let userId;
if (userEmail) {
try {
const userResponse = await fetch("https://www.crossmint.com/api/v1-alpha1/wallets/users", {
method: "POST",
headers: {
"X-API-KEY": this.crossmintConfig.serverSideApiKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
email: userEmail,
type: "evm-smart-wallet"
})
});
if (userResponse.ok) {
const userData = await userResponse.json();
userId = userData.id;
}
} catch (error) {
logger2.warn(`Failed to create user, proceeding with anonymous wallet: ${error instanceof Error ? error.message : String(error)}`);
}
}
const walletResponse = await fetch("https://www.crossmint.com/api/v1-alpha1/wallets", {
method: "POST",
headers: {
"X-API-KEY": this.crossmintConfig.serverSideApiKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
type: "solana-smart-wallet",
userId
})
});
if (!walletResponse.ok) {
const errorData = await walletResponse.json();
throw new Error(`Failed to create wallet: ${JSON.stringify(errorData)}`);
}
const walletData = await walletResponse.json();
logger2.info("\u2705 Crossmint wallet created successfully");
return {
address: walletData.address,
walletId: walletData.id,
smartWalletAddress: walletData.address,
type: "crossmint",
email: userEmail
};
} catch (error) {
logger2.error(`\u274C Failed to create Crossmint wallet: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Get wallet balance
*/
async getWalletBalance(walletAddress) {
try {
const publicKey = new PublicKey2(walletAddress);
const balance = await this.connection.getBalance(publicKey);
return balance;
} catch (error) {
logger2.error(`Failed to get wallet balance: ${error instanceof Error ? error.message : String(error)}`);
return 0;
}
}
/**
* Sign a transaction with Crossmint wallet
*/
async signTransaction(walletId, transaction) {
if (!this.isConfigured()) {
throw new Error("Crossmint not configured");
}
try {
logger2.info(`\u{1F510} Signing transaction with wallet ${walletId}...`);
const signResponse = await fetch(`https://www.crossmint.com/api/v1-alpha1/wallets/${walletId}/transactions`, {
method: "POST",
headers: {
"X-API-KEY": this.crossmintConfig.serverSideApiKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
transaction
})
});
if (!signResponse.ok) {
const errorData = await signResponse.json();
throw new Error(`Failed to sign transaction: ${JSON.stringify(errorData)}`);
}
const signData = await signResponse.json();
logger2.info("\u2705 Transaction signed successfully");
return signData.signature;
} catch (error) {
logger2.error(`\u274C Failed to sign transaction: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Get wallet info
*/
async getWalletInfo(walletId) {
if (!this.isConfigured()) {
throw new Error("Crossmint not configured");
}
try {
const response = await fetch(`https://www.crossmint.com/api/v1-alpha1/wallets/${walletId}`, {
headers: {
"X-API-KEY": this.crossmintConfig.serverSideApiKey
}
});
if (!response.ok) {
throw new Error(`Failed to get wallet info: ${response.statusText}`);
}
return await response.json();
} catch (error) {
logger2.error(`Failed to get wallet info: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* List all wallets for the project
*/
async listWallets() {
if (!this.isConfigured()) {
throw new Error("Crossmint not configured");
}
try {
const response = await fetch("https://www.crossmint.com/api/v1-alpha1/wallets", {
headers: {
"X-API-KEY": this.crossmintConfig.serverSideApiKey
}
});
if (!response.ok) {
throw new Error(`Failed to list wallets: ${response.statusText}`);
}
return await response.json();
} catch (error) {
logger2.error(`Failed to list wallets: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Create a fallback Solana keypair if Crossmint fails
*/
createFallbackWallet() {
logger2.info("\u26A0\uFE0F Creating fallback Solana wallet...");
const keypair = Keypair2.generate();
return {
address: keypair.publicKey.toBase58(),
privateKey: Buffer.from(keypair.secretKey).toString("base64"),
type: "solana"
};
}
/**
* Smart wallet creation with automatic fallback
*/
async createWalletWithFallback(userEmail) {
try {
if (this.isConfigured()) {
const crossmintWallet = await this.createEmbeddedWallet(userEmail);
logger2.info(`\u2705 Created Crossmint smart wallet: ${crossmintWallet.address}`);
return crossmintWallet;
} else {
logger2.info("Crossmint not configured, using fallback wallet");
return this.createFallbackWallet();
}
} catch (error) {
logger2.warn(`Crossmint wallet creation failed, using fallback: ${error instanceof Error ? error.message : String(error)}`);
return this.createFallbackWallet();
}
}
async stop() {
logger2.info("Stopping Crossmint wallet service");
}
static async stop(runtime) {
const service = runtime.getService(_CrossmintWalletService.serviceType);
if (service) {
await service.stop();
}
}
};
// src/services/FalAiService.ts
import { Service as Service3, logger as logger3 } from "@elizaos/core";
import { fal } from "@fal-ai/client";
import { z } from "zod";
var FalAiConfigSchema = z.object({
apiKey: z.string().min(1, "FAL API key is required"),
fluxModel: z.string().default("fal-ai/flux-pro/kontext"),
veoModel: z.string().default("fal-ai/veo3"),
veoImageToVideoModel: z.string().default("fal-ai/veo3/fast/image-to-video")
});
var ImageGenerationInputSchema = z.object({
prompt: z.string(),
image_url: z.string().optional(),
guidance_scale: z.number().default(3.5),
num_images: z.number().default(1),
output_format: z.enum(["jpeg", "png"]).default("jpeg"),
safety_tolerance: z.enum(["1", "2", "3", "4", "5", "6"]).default("2"),
aspect_ratio: z.enum(["21:9", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16", "9:21"]).optional(),
seed: z.number().optional()
});
var VideoGenerationInputSchema = z.object({
prompt: z.string(),
aspect_ratio: z.enum(["16:9", "9:16", "1:1"]).default("16:9"),
duration: z.enum(["8s"]).default("8s"),
negative_prompt: z.string().optional(),
enhance_prompt: z.boolean().default(true),
seed: z.number().optional(),
auto_fix: z.boolean().default(true),
resolution: z.enum(["720p", "1080p"]).default("720p"),
generate_audio: z.boolean().default(true)
});
var ImageToVideoInputSchema = z.object({
prompt: z.string(),
image_url: z.string(),
duration: z.enum(["8s"]).default("8s"),
generate_audio: z.boolean().default(true),
resolution: z.enum(["720p", "1080p"]).default("720p")
});
var FalAiService = class _FalAiService extends Service3 {
constructor(runtime) {
super(runtime);
this.capabilityDescription = "AI-powered image and video generation using FAL AI models including FLUX Pro Kontext and Google Veo3";
this.generationHistory = [];
this.config = {
apiKey: process.env.FAL_API_KEY || "",
fluxModel: process.env.FAL_KONTEXT || "fal-ai/flux-pro/kontext",
veoModel: process.env.FAL_VIDEO || "fal-ai/veo3",
veoImageToVideoModel: process.env.FAL_IMAGE_TO_VIDEO || "fal-ai/veo3/fast/image-to-video"
};
if (this.config.apiKey) {
fal.config({
credentials: this.config.apiKey
});
}
logger3.info("FAL AI Service initialized");
}
static {
this.serviceType = "fal-ai";
}
static async start(runtime) {
const service = new _FalAiService(runtime);
if (!service.config.apiKey) {
logger3.warn("FAL_API_KEY not provided - FAL AI features will be disabled");
}
await service.initialize();
return service;
}
async initialize() {
logger3.info("FAL AI Service initialized successfully");
}
async stop() {
logger3.info("FAL AI Service stopped");
}
/**
* Generate images using FLUX Pro Kontext
*/
async generateImage(input) {
try {
logger3.info(`Generating image with prompt: ${input.prompt.substring(0, 100)}...`);
const validatedInput = ImageGenerationInputSchema.parse(input);
const result = await fal.subscribe(this.config.fluxModel, {
input: validatedInput,
logs: true,
onQueueUpdate: (update) => {
if (update.status === "IN_PROGRESS") {
update.logs?.map((log) => log.message).forEach(
(msg) => logger3.debug(`FAL Generation: ${msg}`)
);
}
}
});
this.generationHistory.push({
type: "image",
timestamp: /* @__PURE__ */ new Date(),
model: this.config.fluxModel,
success: true,
prompt: input.prompt,
resultUrl: result.data?.images?.[0]?.url
});
logger3.info("Image generated successfully");
return {
success: true,
images: result.data?.images,
requestId: result.requestId,
prompt: result.data?.prompt || input.prompt,
seed: result.data?.seed
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger3.error(`Image generation failed: ${errorMessage}`);
this.generationHistory.push({
type: "image",
timestamp: /* @__PURE__ */ new Date(),
model: this.config.fluxModel,
success: false,
prompt: input.prompt
});
return {
success: false,
error: errorMessage
};
}
}
/**
* Generate videos using Veo3
*/
async generateVideo(input) {
try {
logger3.info(`Generating video with prompt: ${input.prompt.substring(0, 100)}...`);
const validatedInput = VideoGenerationInputSchema.parse(input);
const result = await fal.subscribe(this.config.veoModel, {
input: validatedInput,
logs: true,
onQueueUpdate: (update) => {
if (update.status === "IN_PROGRESS") {
update.logs?.map((log) => log.message).forEach(
(msg) => logger3.debug(`FAL Video Generation: ${msg}`)
);
}
}
});
this.generationHistory.push({
type: "video",
timestamp: /* @__PURE__ */ new Date(),
model: this.config.veoModel,
success: true,
prompt: input.prompt,
resultUrl: result.data?.video?.url
});
logger3.info("Video generated successfully");
return {
success: true,
video: result.data?.video,
requestId: result.requestId
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger3.error(`Video generation failed: ${errorMessage}`);
this.generationHistory.push({
type: "video",
timestamp: /* @__PURE__ */ new Date(),
model: this.config.veoModel,
success: false,
prompt: input.prompt
});
return {
success: false,
error: errorMessage
};
}
}
/**
* Generate video from image using Veo3 Image-to-Video
*/
async generateImageToVideo(input) {
try {
logger3.info(`Converting image to video: ${input.prompt.substring(0, 100)}...`);
const validatedInput = ImageToVideoInputSchema.parse(input);
const result = await fal.subscribe(this.config.veoImageToVideoModel, {
input: validatedInput,
logs: true,
onQueueUpdate: (update) => {
if (update.status === "IN_PROGRESS") {
update.logs?.map((log) => log.message).forEach(
(msg) => logger3.debug(`FAL Image-to-Video: ${msg}`)
);
}
}
});
this.generationHistory.push({
type: "image-to-video",
timestamp: /* @__PURE__ */ new Date(),
model: this.config.veoImageToVideoModel,
success: true,
prompt: input.prompt,
resultUrl: result.data?.video?.url
});
logger3.info("Image-to-video conversion completed successfully");
return {
success: true,
video: result.data?.video,
requestId: result.requestId
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger3.error(`Image-to-video conversion failed: ${errorMessage}`);
this.generationHistory.push({
type: "image-to-video",
timestamp: /* @__PURE__ */ new Date(),
model: this.config.veoImageToVideoModel,
success: false,
prompt: input.prompt
});
return {
success: false,
error: errorMessage
};
}
}
/**
* Upload file to FAL storage
*/
async uploadFile(file) {
try {
const url = await fal.storage.upload(file);
logger3.info(`File uploaded to FAL storage: ${url}`);
return url;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger3.error(`File upload failed: ${errorMessage}`);
throw new Error(`File upload failed: ${errorMessage}`);
}
}
/**
* Get service analytics
*/
getAnalytics() {
const modelCounts = this.generationHistory.reduce((acc, item) => {
acc[item.model] = (acc[item.model] || 0) + 1;
return acc;
}, {});
const mostUsedModel = Object.entries(modelCounts).sort(([, a], [, b]) => b - a)[0]?.[0] || "none";
return {
totalImagesGenerated: this.generationHistory.filter((h) => h.type === "image" && h.success).length,
totalVideosGenerated: this.generationHistory.filter((h) => h.type === "video" && h.success).length,
totalImageToVideoConversions: this.generationHistory.filter((h) => h.type === "image-to-video" && h.success).length,
totalApiCalls: this.generationHistory.length,
lastGenerationTime: this.generationHistory.length > 0 ? this.generationHistory[this.generationHistory.length - 1].timestamp : null,
mostUsedModel
};
}
/**
* Get service state
*/
getState() {
return {
isConfigured: !!this.config.apiKey,
apiKeyPresent: !!this.config.apiKey,
lastError: null,
// Could track last error here
analytics: this.getAnalytics()
};
}
/**
* Get recent generation history
*/
getRecentGenerations(limit = 10) {
return this.generationHistory.slice(-limit).reverse();
}
};
// src/actions/claimFees.ts
import {
logger as logger4
} from "@elizaos/core";
var claimFeesAction = {
name: "CLAIM_BAGS_FEES",
similes: ["CLAIM_FEES", "COLLECT_EARNINGS", "CLAIM_REWARDS", "GET_FEES"],
description: "Claims available fees from Bags protocol tokens",
validate: async (runtime, message, state) => {
const text = message.content.text?.toLowerCase();
if (!text) return false;
const triggers = [
"claim fees",
"claim all fees",
"collect fees",
"collect earnings",
"claim rewards",
"get my fees",
"withdraw fees",
"claim from bags"
];
return triggers.some((trigger) => text.includes(trigger));
},
handler: async (runtime, message, state, options, callback) => {
try {
const bagsService = runtime.getService("bags");
if (!bagsService) {
await callback?.({
text: "Bags service is not available. Please check the configuration.",
error: true
});
return {
success: false,
text: "Bags service not found",
error: new Error("Bags service not available")
};
}
const text = message.content.text?.toLowerCase() || "";
let results;
if (text.includes("all") || !text.match(/[A-Za-z0-9]{32,}/)) {
await callback?.({
text: "\u{1F504} Checking for claimable fees across all tokens...",
action: "CLAIM_BAGS_FEES"
});
results = await bagsService.claimAllFees();
} else {
const tokenMintMatch = text.match(/([A-Za-z0-9]{32,})/);
if (tokenMintMatch) {
const tokenMint = tokenMintMatch[1];
await callback?.({
text: `\u{1F504} Claiming fees for token: ${tokenMint.slice(0, 8)}...${tokenMint.slice(-8)}`,
action: "CLAIM_BAGS_FEES"
});
const result = await bagsService.claimFeesForToken(tokenMint);
results = result ? [result] : [];
} else {
results = await bagsService.claimAllFees();
}
}
if (results.length === 0) {
await callback?.({
text: "\u{1F4AD} No claimable fees found at this time. Your tokens might not have generated fees yet, or fees may have already been claimed.",
action: "CLAIM_BAGS_FEES"
});
return {
success: true,
text: "No fees to claim",
values: {
claimedCount: 0,
totalClaimed: 0
},
data: {
actionName: "CLAIM_BAGS_FEES",
results: []
}
};
}
const totalClaimed = results.reduce((sum, result) => sum + result.claimedAmount, 0);
const successfulClaims = results.filter((r) => r.success).length;
let responseText = `\u2705 Fee claiming completed!
`;
responseText += `\u{1F4B0} Total claimed: ${totalClaimed.toFixed(6)} SOL
`;
responseText += `\u{1F4CA} Successful claims: ${successfulClaims}/${results.length}
`;
if (results.length <= 5) {
responseText += `Details:
`;
for (const result of results) {
const tokenShort = `${result.tokenMint.slice(0, 8)}...${result.tokenMint.slice(-8)}`;
responseText += `\u2022 ${tokenShort}: ${result.claimedAmount.toFixed(6)} SOL
`;
}
} else {
responseText += `View full details in the analytics dashboard.`;
}
await callback?.({
text: responseText,
action: "CLAIM_BAGS_FEES"
});
return {
success: true,
text: `Successfully claimed ${totalClaimed.toFixed(6)} SOL from ${successfulClaims} tokens`,
values: {
claimedCount: successfulClaims,
totalClaimed,
results
},
data: {
actionName: "CLAIM_BAGS_FEES",
totalClaimed,
successfulClaims,
results
}
};
} catch (error) {
logger4.error(`Error in claim fees action: ${error instanceof Error ? error.message : String(error)}`);
await callback?.({
text: "\u274C An error occurred while claiming fees. Please try again later.",
error: true
});
return {
success: false,
text: "Failed to claim fees",
error: error instanceof Error ? error : new Error(String(error)),
data: {
actionName: "CLAIM_BAGS_FEES",
errorMessage: error instanceof Error ? error.message : String(error)
}
};
}
},
examples: [
[
{
name: "{{userName}}",
content: {
text: "claim all my fees from bags",
actions: []
}
},
{
name: "{{agentName}}",
content: {
text: "\u{1F504} Checking for claimable fees across all tokens...\n\n\u2705 Fee claiming completed!\n\n\u{1F4B0} Total claimed: 0.125000 SOL\n\u{1F4CA} Successful claims: 3/3\n\nDetails:\n\u2022 4k3Dyjlb...8xMp1a2K: 0.045000 SOL\n\u2022 7wB5nCx9...2nFp4k7L: 0.032000 SOL\n\u2022 9mK8pLq2...5vGh3n8M: 0.048000 SOL",
actions: ["CLAIM_BAGS_FEES"]
}
}
],
[
{
name: "{{userName}}",
content: {
text: "collect my earnings",
actions: []
}
},
{
name: "{{agentName}}",
content: {
text: "\u{1F504} Checking for claimable fees across all tokens...\n\n\u{1F4AD} No claimable fees found at this time. Your tokens might not have generated fees yet, or fees may have already been claimed.",
actions: ["CLAIM_BAGS_FEES"]
}
}
],
[
{
name: "{{userName}}",
content: {
text: "claim fees for token 4k3Dyjlb8xMp1a2KcNvFp9L7wB5nCx92nFp4k7L5vGh3n8M",
actions: []
}
},
{
name: "{{agentName}}",
content: {
text: "\u{1F504} Claiming fees for token: 4k3Dyjlb...5vGh3n8M\n\n\u2705 Fee claiming completed!\n\n\u{1F4B0} Total claimed: 0.045000 SOL\n\u{1F4CA} Successful claims: 1/1\n\nDetails:\n\u2022 4k3Dyjlb...5vGh3n8M: 0.045000 SOL",
actions: ["CLAIM_BAGS_FEES"]
}
}
]
]
};
// src/actions/launchToken.ts
import {
logger as logger6
} from "@elizaos/core";
// src/utils/tokenParser.ts
import { logger as logger5 } from "@elizaos/core";
function parseTokenParameters(text) {
try {
const params = {};
const nameMatch = text.match(/(?:name|token name):\s*([^\n,]+)/i);
if (nameMatch) {
params.name = nameMatch[1].trim();
}
const symbolMatch = text.match(/(?:symbol|ticker):\s*\$?([A-Z0-9]{1,10})/i) || text.match(/\$([A-Z0-9]{1,10})/);
if (symbolMatch) {
params.symbol = symbolMatch[1].trim().toUpperCase();
}
const descMatch = text.match(/(?:description|desc|about):\s*([^\n,]+)/i);
if (descMatch) {
params.description = descMatch[1].trim();
}
const imageMatch = text.match(/(?:image|img|logo):\s*(https?:\/\/[^\s,]+)/i);
if (imageMatch) {
params.imageUrl = imageMatch[1].trim();
}
const websiteMatch = text.match(/(?:website|site|web):\s*(https?:\/\/[^\s,]+)/i);
if (websiteMatch) {
params.website = websiteMatch[1].trim();
}
const twitterMatch = text.match(/(?:twitter|x\.com):\s*(https?:\/\/[^\s,]+)/i);
if (twitterMatch) {
params.twitter = twitterMatch[1].trim();
}
const buyMatch = text.match(/(?:initial buy|buy amount):\s*(\d*\.?\d+)/i);
if (buyMatch) {
params.initialBuyAmountSOL = parseFloat(buyMatch[1]);
}
if (!params.name || !params.symbol || !params.description) {
return null;
}
return params;
} catch (error) {
logger5.error(`Error parsing token parameters: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
// src/actions/launchToken.ts
var launchTokenAction = {
name: "LAUNCH_BAGS_TOKEN",
similes: ["LAUNCH_TOKEN", "CREATE_TOKEN", "DEPLOY_TOKEN", "MINT_TOKEN"],
description: "Launches a new token on the Bags protocol with specified parameters",
validate: async (runtime, message, state) => {
const text = message.content.text?.toLowerCase();
if (!text) return false;
const triggers = [
"launch token",
"create token",
"deploy token",
"mint token",
"new token",
"launch on bags",
"create on bags"
];
return triggers.some((trigger) => text.includes(trigger));
},
handler: async (runtime, message, state, options, callback) => {
try {
const bagsService = runtime.getService("bags");
if (!bagsService) {
await callback?.({
text: "Bags service is not available. Please check the configuration.",
error: true
});
return {
success: false,
text: "Bags service not found",
error: new Error("Bags service not available")
};
}
const text = message.content.text || "";
const tokenParams = parseTokenParameters(text);
if (!tokenParams) {
await callback?.({
text: "\u274C Unable to parse token parameters. Please provide the token details in this format:\n\n**Launch token:**\n\u2022 Name: Your Token Name\n\u2022 Symbol: TOKEN\n\u2022 Description: A brief description of your token\n\u2022 Image: https://example.com/image.png (optional)\n\u2022 Website: https://yourwebsite.com (optional)\n\u2022 Twitter: https://twitter.com/yourtoken (optional)\n\u2022 Initial buy: 0.01 SOL (optional, defaults to 0.01)",
error: true
});
return {
success: false,
text: "Invalid token parameters",
error: new Error("Could not parse token parameters from message")
};
}
if (!tokenParams.name || !tokenParams.symbol || !tokenParams.description) {
await callback?.({
text: "\u274C Missing required fields. Please provide at least:\n\u2022 **Name**: Token name\n\u2022 **Symbol**: Token symbol (2-10 characters)\n\u2022 **Description**: Token description",
error: true
});
return {
success: false,
text: "Missing required token parameters",
error: new Error("Name, symbol, and description are required")
};
}
await callback?.({
text: `\u{1F504} Launching token with the following parameters:
\u{1F4DB} **Name**: ${tokenParams.name}
\u{1F3F7}\uFE0F **Symbol**: $${tokenP