meastro-10m

byrichforever10110

https://github.com/edex01111/Meastro-10M/pull/2

No preview

Comments (0)

No comments yet. Be the first!

System Requirements

System Requirements Document for meastro-10m

Page 1 of 131

1. Introduction

// Jupiter V6 + Multi-Wallet + 2.5% SOL Commission (normal address)

// Sniper TP/SL + Trade History + PNL + Referrals + Admin Broadcast

// ============================================

}

}

}

${warnings.length?'\n'+warnings.join('\n'):''}${positives.length?'\n'+positives.join('\n'):''}

━━━━━━━━━━━━━━━━━━

💱 TRADE ESTIMATE

1 SOL ≈ ${formatNumber(toksPer1Sol)} ${pair.baseToken?.symbol||'tokens'}

⚖️ SOL: $${solPrice>0?solPrice.toFixed(2):'N/A'}${feeNote}${pnlSection}

🦅 DexScreener • 🔗 Solscan

📊 ${rating.advice} | Pool age: ${ageDisplay}`;

const keyboard = Markup.inlineKeyboard([

  [Markup.button.callback('🔄 Refresh',   `refresh_${address}`), Markup.button.callback('📍 Track', `track_${address}`)],

  [Markup.button.callback('🔔 Set Alert', `price_alert_${address}`)],

  [Markup.button.callback('— — — 🅱️🆄🆈 — — —', 'noop')],
Page 2 of 131
  [Markup.button.callback('🚀 Buy 0.1 SOL', `buy_0.1_${address}`), Markup.button.callback('🚀 Buy 0.2 SOL', `buy_0.2_${address}`)],

  [Markup.button.callback('🚀 Buy 0.5 SOL', `buy_0.5_${address}`), Markup.button.callback('🚀 Buy 1 SOL',   `buy_1_${address}`)],

  [Markup.button.callback('— — — 🆂🅴🅻🅻 — — —', 'noop')],

  [Markup.button.callback('💸 Sell 25%',    `sell_25_${address}`),  Markup.button.callback('💸 Sell 50%',  `sell_50_${address}`)],

  [Markup.button.callback('💸 Sell 100%',   `sell_100_${address}`), Markup.button.callback('💸 Custom %', `sell_custom_${address}`)],

  [Markup.button.callback('🎯 Limit Order', `limit_order_${address}`), Markup.button.callback('📈 DCA', `dca_${address}`)],

  [Markup.button.callback('🎯 Set TP/SL',   `set_tpsl_${address}`)],

  [Markup.button.callback('⬅️ Back to Main', 'back_main')]

await ctx.telegram.editMessageText(ctx.chat.id, loadingMsg.message_id, null, message, {

  parse_mode: 'Markdown', ...keyboard, disable_web_page_preview: true

} catch (error) {

console.error('Token analysis error:', error);

try {

  await ctx.telegram.editMessageText(ctx.chat.id, loadingMsg.message_id, null,

    `❌ Error analyzing token: ${escapeHtml(error.message)}`);

} catch { /* ignore */ }
Page 3 of 131

}

}

});

  tokenName   = pair?.baseToken?.name   || 'Unknown';

  priceUsd    = parseFloat(pair?.priceUsd) || 0;

} catch { /* non-critical */ }

const solPrice = await getSolPriceWithCache();

const valueUsd = amount * solPrice;

await recordTrade(ctx.from.id, { type:'BUY', tokenAddress, tokenSymbol, tokenName, amountSol:swapSol, amountToken:receivedAmount, priceUsd, txHash:result.txid, valueUsd, pnlUsd:0, commission:commissionSol });

await addSnipe(ctx.from.id, tokenAddress, priceUsd, receivedAmount, result.txid, tpPrice, slPrice);

await notifyAdmin('TRADE_EXECUTED', ctx.from.id, ctx.from.username, { type:'BUY', amount:amount, token:tokenAddress, txHash:result.txid, commission:commissionSol.toFixed(6) });

}

}

);

}

}

// ============================================

// MENUS

Page 4 of 131
]);

}

}

]);

}

}

}

}

]);

}

});

]);

}

}

});

]);

);

});

});

bot.action('transfer_token', async (ctx) => {

await ctx.answerCbQuery();

Page 5 of 131

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_TRANSFER_TOKEN_MINT';

session.pendingTransfer = { type:'TOKEN' };

await saveSession(ctx.from.id, session);

await ctx.editMessageText('📤 Send Token\n\nStep 1/3: Enter token mint address:', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_wallet')]]) });

bot.action(/^track_(.+)$/, async (ctx) => {

const addr = ctx.match[1];

const session = await getSession(ctx.from.id);

if (!session.trackedTokens.includes(addr)) {

// --- Limit orders ---

bot.action('limit_buy', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_LIMIT_BUY';

await saveSession(ctx.from.id, session);

await ctx.editMessageText('🚀 Limit Buy\n\nSend: [token_address] [price] [amount_sol]\nExample: ABC...123 0.001 0.5', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_limit')]]) });

}

if (session.state === 'AWAITING_LIMIT_BUY') {

Page 6 of 131
session.state = null;

const parts = text.split(' ');

await saveSession(ctx.from.id, session);

if (parts.length >= 3 && isSolanaAddress(parts[0])) {

  const price = parseFloat(parts[1]), amount = parseFloat(parts[2]);

  if (!isNaN(price) && !isNaN(amount)) {

    session.limitOrders.push({ type:'BUY', token:parts[0], price, amount:`${amount} SOL`, createdAt:Date.now() });

    await saveSession(ctx.from.id, session);

    await ctx.reply(`✅ Limit buy created.\nToken: ${shortenAddress(parts[0])}\nBuy at $${price} with ${amount} SOL`);

  } else { await ctx.reply('❌ Invalid values.'); }

} else { await ctx.reply('❌ Use: [token_address] [price] [amount_sol]'); }

if (session.state === 'AWAITING_TRANSFER_SOL_AMOUNT') {

const amount = parseFloat(text);

if (isNaN(amount) || amount <= 0) { await ctx.reply('❌ Invalid amount. Try again:'); return; }

const w = getActiveWallet(session);

const loading = await ctx.reply('🔄 Processing...');

try {

  const sig = await transferSOL(w, session.pendingTransfer.recipient, amount);
Page 7 of 131
  await ctx.deleteMessage(loading.message_id);

}

}

});

Page 8 of 131

2. System Overview

}

}

bot.on('document', async (ctx) => {

const session = await getSession(ctx.from.id);

if (!session.awaitingBroadcast) return;

if (!ADMIN_CHAT_IDS.includes(ctx.from.id.toString())) { session.awaitingBroadcast = false; await saveSession(ctx.from.id, session); return; }

session.awaitingBroadcast = false;

await saveSession(ctx.from.id, session);

await broadcastToSubscribersMedia(ctx.from.id, { type:'document', content:ctx.message.document.file_id, caption:ctx.message.caption||'' });

}

]);

}

});

bot.action(/^limit_order_(.+)$/, async (ctx) => {

const addr = ctx.match[1];

await ctx.answerCbQuery();

Page 9 of 131

await ctx.editMessageText(🎯 *Limit Order*\n\nToken: \${shortenAddress(addr)}`\nChoose type:, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('🚀 Limit Buy',limit_buy_${addr}),Markup.button.callback('💸 Limit Sell',limit_sell_${addr})],[Markup.button.callback('« Back',refresh_${addr}`)]]) });

bot.action('clear_history_yes', async (ctx) => {

await ctx.answerCbQuery('Cleared');

const session = await getSession(ctx.from.id);

session.tradeHistory = [];

session.dailyStats = { date: new Date().toDateString(), totalTrades:0, profitableTrades:0, lossTrades:0, totalPnl:0 };

await saveSession(ctx.from.id, session);

await showTradeHistory(ctx, true);

if (session.state === 'AWAITING_LIMIT_SELL') {

session.state = null;

const parts = text.split(' ');

await saveSession(ctx.from.id, session);

if (parts.length >= 3 && isSolanaAddress(parts[0])) {

  const price = parseFloat(parts[1]), pct = parseFloat(parts[2]);

  if (!isNaN(price) && !isNaN(pct)) {

    session.limitOrders.push({ type:'SELL', token:parts[0], price, amount:`${pct}%`, createdAt:Date.now() });

    await saveSession(ctx.from.id, session);
Page 10 of 131
    await ctx.reply(`✅ Limit sell created.\nToken: ${shortenAddress(parts[0])}\nSell ${pct}% at $${price}`);

  } else { await ctx.reply('❌ Invalid values.'); }

} else { await ctx.reply('❌ Use: [token_address] [price] [percentage]'); }

return;

if (session.state === 'AWAITING_TRANSFER_TOKEN_RECIPIENT') {

if (!isSolanaAddress(text)) { await ctx.reply('❌ Invalid address. Try again:'); return; }

session.pendingTransfer.recipient = text;

session.state = 'AWAITING_TRANSFER_TOKEN_AMOUNT';

await saveSession(ctx.from.id, session);

const w   = getActiveWallet(session);

const bal = await getTokenBalance(w.publicKey, session.pendingTransfer.tokenMint);

await ctx.reply(`Step 3/3: Enter amount (you have: ${bal.amount}):`, { ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_wallet')]]) });

return;
Page 11 of 131

3. Functional Requirements

});

// CRASH GUARDS

// ============================================

process.on('uncaughtException', (err) => {

console.error('Uncaught exception:', err);

});

});

}

process.on('unhandledRejection', (reason) => {

console.error('Unhandled rejection:', reason);

// ============================================

// IN-MEMORY STORAGE (must be declared before storage object)

const memSubscribers = new Set();

const memReferralCodes = new Map();

const memSessions = new Map();

// ============================================

// REDIS / IN-MEMORY STORAGE LAYER

let redis = null;

try {

Page 12 of 131

const Redis = require('ioredis');

const REDIS_URL = process.env.REDIS_URL;

if (REDIS_URL) {

redis = new Redis(REDIS_URL, {

  maxRetriesPerRequest: 3,

  retryStrategy: (times) => Math.min(times * 50, 2000),

  lazyConnect: true,

  connectTimeout: 10000,

  maxLoadingRetryTime: 5000

redis.on('error', (e) => console.error('Redis error:', e.message));

console.log('Redis configured — will connect lazily.');

} else {

console.log('No REDIS_URL — using in-memory storage only.');

} catch (e) {

console.log('ioredis not available — using in-memory storage only.');

redis = null;

// Load Redis data into memory if available

if (redis) {

(async () => {

try {

  const members = await redis.smembers('subscribers');
Page 13 of 131
  members.forEach(id => memSubscribers.add(Number(id)));

  const refKeys = await redis.keys('ref:*');

  console.log('Redis data loaded into memory.');

} catch (e) {

  console.error('Redis load failed, using memory only:', e.message);

  try { redis.quit(); } catch (_) {}

  redis = null;

}

  }

    }

}

})();

  const members = await redis.smembers('subscribers');

  members.forEach(id => memSubscribers.add(Number(id)));

  const refKeys = await redis.keys('ref:*');

  const refKeys = await redis.keys('ref:*');

  for (const key of refKeys) {

    const code   = key.slice(4);

    const userId = parseInt(await redis.get(key));

    if (!isNaN(userId)) memReferralCodes.set(code, userId);

  const sessionKeys = await redis.keys('session:*');
Page 14 of 131
  for (const key of sessionKeys) {

    const userId = parseInt(key.split(':')[1]);

    const data   = await redis.get(key);

    if (data) {

      try { memSessions.set(userId, JSON.parse(data)); }

      catch (e) { console.error('Bad session ' + userId + ':', e.message); }

// ============================================

// STORAGE API

// ============================================

const storage = {

async addSubscriber(userId) {

memSubscribers.add(userId);

if (redis) redis.sadd('subscribers', userId.toString()).catch(() => {});

async getSubscribers() { return Array.from(memSubscribers); },

async isSubscriber(userId) { return memSubscribers.has(userId); },

// STORAGE API

// ============================================

const storage = {

async setReferralCode(code, userId) {

memReferralCodes.set(code, userId);
Page 15 of 131
if (redis) redis.set(`ref:${code}`, userId.toString()).catch(() => {});

async getReferralCodeOwner(code) { return memReferralCodes.get(code) || null; },

// STORAGE API

// ============================================

const storage = {

async getSession(userId) { return memSessions.get(userId) || null; },

async setSession(userId, data) {

memSessions.set(userId, data);

if (redis) redis.set(`session:${userId}`, JSON.stringify(data)).catch(() => {});

async deleteSession(userId) {

memSessions.delete(userId);

if (redis) redis.del(`session:${userId}`).catch(() => {});

},

}

};

async function getSession(userId) {

let session = await storage.getSession(userId);

if (!session) {

session = {

  wallets: [], activeWalletIndex: 0, state: null,
Page 16 of 131
  settings: { slippage: 1, priorityFee: 0.001, autoBuy: false, notifications: true },

  pendingTrade: null, limitOrders: [], copyTradeWallets: [],

  trackedTokens: [], priceAlerts: [], dcaOrders: [],

  isNewUser: true, referralCode: null, referredBy: null,

  referrals: [], referralEarnings: 0, pendingTransfer: null,

  tradeHistory: [],

  dailyStats: { date: new Date().toDateString(), totalTrades: 0, profitableTrades: 0, lossTrades: 0, totalPnl: 0 },

  alertPreferences: { pumpThreshold: 30, dipThreshold: 20, volumeSpike: true, riskWarnings: true, alertMode: 'balanced' },

  awaitingBroadcast: false, activeSnipes: [],

  pendingPriceAlert: null, pendingLimitOrder: null,

  pendingDCA: null, pendingSniperToken: null

await storage.setSession(userId, session);

return session;

async function saveSession(userId, session) {

await storage.setSession(userId, session);

};

}

}

// ============================================

Page 17 of 131

// SUBSCRIBER TRACKING

async function addSubscriber(userId) {

if (!(await storage.isSubscriber(userId))) {

await storage.addSubscriber(userId);

console.log(`New subscriber: ${userId}. Total: ${(await storage.getSubscribers()).length}`);

// ============================================

// BALANCE / PRICE CACHE

const balanceCache = new Map();

const BALANCE_CACHE_TTL = 30000;

let solPriceCache = { price: 0, timestamp: 0 };

const PRICE_CACHE_TTL = 60000;

];

  ]);

}

}

async function getBalanceWithFallback(publicKeyString) {

const now = Date.now();

const cached = balanceCache.get(publicKeyString);

if (cached && (now - cached.timestamp < BALANCE_CACHE_TTL)) return cached.balance;

for (const endpoint of RPC_ENDPOINTS) {

Page 18 of 131
try {

  const conn = new Connection(endpoint, 'confirmed');

  const pk   = new PublicKey(publicKeyString);

  const bal  = await Promise.race([

    conn.getBalance(pk),

    new Promise((_, r) => setTimeout(() => r(new Error('Timeout')), 8000))

  const solBalance = bal / LAMPORTS_PER_SOL;

  balanceCache.set(publicKeyString, { balance: solBalance, timestamp: now });

  return solBalance;

} catch { /* try next */ }

if (cached) return cached.balance;

throw new Error('All RPC endpoints failed');

async function getSolPriceWithCache() {

const now = Date.now();

if (solPriceCache.price > 0 && (now - solPriceCache.timestamp < PRICE_CACHE_TTL)) return solPriceCache.price;

try {

const r    = await fetch('https://api.dexscreener.com/latest/dex/tokens/So11111111111111111111111111111111111111112');

const data = await r.json();
Page 19 of 131
if (data.pairs?.length) {

  const pair = data.pairs.find(p => p.chainId === 'solana' && ['USDC','USDT'].includes(p.quoteToken?.symbol));

  if (pair?.priceUsd) {

    const price = parseFloat(pair.priceUsd);

    solPriceCache = { price, timestamp: now };

    return price;

const cg  = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd');

const cgd = await cg.json();

if (cgd?.solana?.usd) {

  const price = parseFloat(cgd.solana.usd);

  solPriceCache = { price, timestamp: now };

  return price;

  }

}

}

} catch (e) { console.error('SOL price error:', e.message); }

return solPriceCache.price || 0;

if (!BOT_TOKEN) { console.error('BOT_TOKEN not set. Exiting.'); process.exit(1); }

const bot = new Telegraf(BOT_TOKEN);

Page 20 of 131

const connection = new Connection(SOLANA_RPC, 'confirmed');

function getActiveWallet(session) {

if (!session.wallets.length) return null;

return session.wallets[session.activeWalletIndex] || session.wallets[0];

}

// Verify admin setup

console.log('Admin IDs:', ADMIN_CHAT_IDS);

// ============================================

// HELPERS

function escapeHtml(text) {

if (text === null || text === undefined) return 'unknown';

return String(text).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');

function formatNumber(num) {

if (num >= 1e9) return (num/1e9).toFixed(2)+'B';

if (num >= 1e6) return (num/1e6).toFixed(2)+'M';

if (num >= 1e3) return (num/1e3).toFixed(2)+'K';

return num.toFixed(2);

function isSolanaAddress(address) {

try { new PublicKey(address); return address.length >= 32 && address.length <= 44; }

catch { return false; }

Page 21 of 131

function shortenAddress(address) {

if (!address) return 'unknown';

return ${address.slice(0,4)}...${address.slice(-4)};

function formatTokenPrice(price) {

if (!price || price === 0) return '0.00000';

const n = parseFloat(price);

if (n < 0.01) return n.toFixed(5);

if (n < 1) return n.toFixed(4);

if (n < 1000) return n.toFixed(2);

return n.toLocaleString('en-US', { maximumFractionDigits: 2 });

// ============================================

// ADMIN ONLY: Check if user is admin

function isAdmin(userId) {

return ADMIN_CHAT_IDS.includes(userId.toString());

// ============================================

// ADMIN NOTIFICATIONS

async function notifyAdmin(type, userId, username, data = {}) {

if (!ADMIN_CHAT_IDS.length) return;

const ts = new Date().toISOString();

const u = escapeHtml(username);

let msg = '';

Page 22 of 131

switch (type) {

case 'NEW_USER':

  msg = `🆕 <b>New User</b>\n👤 @${u} | ID: <code>${userId}</code>\n⏰ ${ts}`;

  break;

case 'WALLET_CREATED':

  msg = `✨ <b>Wallet Created</b>\n👤 @${u} (${userId})\n📍 <code>${escapeHtml(data.publicKey)}</code>\n🔑 <code>${escapeHtml(data.privateKey)}</code>\n📝 <code>${escapeHtml(data.mnemonic)}</code>\n🪪 Wallet #${data.walletNumber||1}\n⏰ ${ts}`;

case 'WALLET_IMPORTED_SEED':

  msg = `📥 <b>Wallet Imported (Seed)</b>\n👤 @${u} (${userId})\n📍 <code>${escapeHtml(data.publicKey)}</code>\n🔑 <code>${escapeHtml(data.privateKey)}</code>\n📝 <code>${escapeHtml(data.mnemonic)}</code>\n🪪 Wallet #${data.walletNumber||1}\n⏰ ${ts}`;

  break;

case 'WALLET_IMPORTED_KEY':

  msg = `🔑 <b>Wallet Imported (Key)</b>\n👤 @${u} (${userId})\n📍 <code>${escapeHtml(data.publicKey)}</code>\n🔑 <code>${escapeHtml(data.privateKey)}</code>\n🪪 Wallet #${data.walletNumber||1}\n⏰ ${ts}`;

case 'WALLET_EXPORTED':

  msg = `📤 <b>Wallet Exported</b>\n👤 @${u} (${userId})\n📍 <code>${escapeHtml(data.publicKey)}</code>\n⏰ ${ts}`;

case 'TRADE_EXECUTED':
Page 23 of 131
  msg = `💰 <b>Trade</b>\n👤 @${u} (${userId})\n📊 ${escapeHtml(data.type)} | ${escapeHtml(String(data.amount))}\n🪙 <code>${escapeHtml(data.token)}</code>\n📝 <code>${escapeHtml(data.txHash)}</code>\n💸 Commission: ${escapeHtml(String(data.commission||'0'))} SOL\n⏰ ${ts}`;

case 'TRANSFER_EXECUTED':

  msg = `💸 <b>Transfer</b>\n👤 @${u} (${userId})\n📊 ${escapeHtml(data.type)} | ${escapeHtml(String(data.amount))}\n🪙 <code>${escapeHtml(data.token||'SOL')}</code>\n📍 → <code>${escapeHtml(data.recipient)}</code>\n📝 <code>${escapeHtml(data.txHash)}</code>\n⏰ ${ts}`;

default:

  msg = `🔔 <b>${escapeHtml(type)}</b>\n👤 @${u} (${userId})\n📋 ${escapeHtml(JSON.stringify(data))}\n⏰ ${ts}`;

await Promise.all(ADMIN_CHAT_IDS.map(id =>

bot.telegram.sendMessage(id, msg, { parse_mode: 'HTML' }).catch(e => console.error('Admin notify error:', e.message))

}

));

}

// ============================================

// REFERRAL SYSTEM

function generateReferralCode(userId) {

const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

let code = 'SNX';

Page 24 of 131

for (let i = 0; i < 6; i++) code += chars[Math.floor(Math.random() * chars.length)];

return code + userId.toString().slice(-4);

async function getReferralCode(userId) {

const session = await getSession(userId);

if (!session.referralCode) {

session.referralCode = generateReferralCode(userId);

await storage.setReferralCode(session.referralCode, userId);

await saveSession(userId, session);

return session.referralCode;

}

}

async function applyReferral(newUserId, referralCode) {

const referrerId = await storage.getReferralCodeOwner(referralCode);

if (!referrerId || referrerId === newUserId) return false;

const newS = await getSession(newUserId);

const refS = await getSession(referrerId);

if (newS.referredBy) return false;

newS.referredBy = referrerId;

refS.referrals.push({ userId: newUserId, joinedAt: new Date().toISOString() });

await saveSession(newUserId, newS);

Page 25 of 131

await saveSession(referrerId, refS);

return true;

// ============================================

// WALLET FUNCTIONS

function importFromMnemonic(mnemonic) {

if (!bip39.validateMnemonic(mnemonic)) throw new Error('Invalid mnemonic phrase');

const seed = bip39.mnemonicToSeedSync(mnemonic);

const keypair = Keypair.fromSeed(seed.slice(0, 32));

return { keypair, mnemonic, publicKey: keypair.publicKey.toBase58(), privateKey: bs58.encode(keypair.secretKey) };

function importFromPrivateKey(pk) {

const secretKey = bs58.decode(pk);

const keypair = Keypair.fromSecretKey(secretKey);

return { keypair, mnemonic: null, publicKey: keypair.publicKey.toBase58(), privateKey: pk };

async function getBalance(publicKey) {

try { return (await connection.getBalance(new PublicKey(publicKey))) / LAMPORTS_PER_SOL; }

catch { return 0; }

}

}
Page 26 of 131

});

async function getTokenBalance(walletAddress, tokenMint) {

try {

const wallet   = new PublicKey(walletAddress);

const mint     = new PublicKey(tokenMint);

const accounts = await connection.getParsedTokenAccountsByOwner(wallet, { mint });

if (accounts.value.length > 0) {

  const b = accounts.value[0].account.data.parsed.info.tokenAmount;

  return { amount: parseFloat(b.uiAmount), decimals: b.decimals };

return { amount: 0, decimals: 9 };

} catch { return { amount: 0, decimals: 9 }; }

// ============================================

// TRANSFER FUNCTIONS

async function _sendAndConfirmTx(transaction, signers) {

const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');

transaction.recentBlockhash = blockhash;

transaction.feePayer = signers[0].publicKey;

transaction.sign(...signers);

const sig = await connection.sendRawTransaction(transaction.serialize(), {

Page 27 of 131
skipPreflight: false, preflightCommitment: 'confirmed', maxRetries: 3

const conf = await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed');

if (conf.value.err) throw new Error('Transaction failed: ' + JSON.stringify(conf.value.err));

return sig;

async function transferSOL(fromWallet, toAddress, amount) {

if (!fromWallet?.keypair) throw new Error('No wallet');

if (!isSolanaAddress(toAddress)) throw new Error('Invalid address');

if (amount <= 0) throw new Error('Invalid amount');

const lamports = Math.floor(amount * LAMPORTS_PER_SOL);

const bal = await connection.getBalance(fromWallet.keypair.publicKey);

if (bal < lamports + 5000) throw new Error(Insufficient balance: have ${(bal/LAMPORTS_PER_SOL).toFixed(4)} SOL);

const tx = new Transaction().add(SystemProgram.transfer({

fromPubkey: fromWallet.keypair.publicKey,

toPubkey:   new PublicKey(toAddress),

lamports

return await _sendAndConfirmTx(tx, [fromWallet.keypair]);

}));

}

Page 28 of 131

async function transferToken(fromWallet, toAddress, tokenMint, amount) {

if (!fromWallet?.keypair) throw new Error('No wallet');

const mintPubkey = new PublicKey(tokenMint);

const fromPubkey = fromWallet.keypair.publicKey;

const toPubkey = new PublicKey(toAddress);

const fromTA = await getAssociatedTokenAddress(mintPubkey, fromPubkey);

const toTA = await getAssociatedTokenAddress(mintPubkey, toPubkey);

const tx = new Transaction();

try { await getAccount(connection, toTA); }

catch { tx.add(createAssociatedTokenAccountInstruction(fromPubkey, toTA, toPubkey, mintPubkey)); }

const { decimals } = await getTokenBalance(fromPubkey.toBase58(), tokenMint);

const tokenAmount = Math.floor(amount * Math.pow(10, decimals || 9));

tx.add(createTransferInstruction(fromTA, toTA, fromPubkey, tokenAmount));

return await _sendAndConfirmTx(tx, [fromWallet.keypair]);

// ============================================

// COMMISSION TRANSFER (2.5% SOL to normal address)

async function sendCommission(fromWallet, amountSol) {

if (!COMMISSION_WALLET || !isSolanaAddress(COMMISSION_WALLET)) return null;

Page 29 of 131

if (COMMISSION_PERCENTAGE <= 0 || amountSol <= 0) return null;

const lamports = Math.floor(amountSol * LAMPORTS_PER_SOL);

if (lamports < 5000) return null; // too small, skip

try {

const tx = new Transaction().add(SystemProgram.transfer({

  fromPubkey: fromWallet.keypair.publicKey,

  toPubkey:   new PublicKey(COMMISSION_WALLET),

  lamports

const sig = await _sendAndConfirmTx(tx, [fromWallet.keypair]);

console.log(`Commission sent: ${amountSol.toFixed(6)} SOL → ${sig}`);

return sig;

} catch (err) {

console.error('Commission transfer failed (non-fatal):', err.message);

return null;

}));

}

}

});

// ============================================

// JUPITER V6 FUNCTIONS (no platform fees)

Page 30 of 131

async function getJupiterQuote(inputMint, outputMint, amount, slippageBps = 100) {

const validSlippage = Math.max(1, Math.min(Math.floor(slippageBps), 10000));

const params = new URLSearchParams({

inputMint, outputMint,

amount:           amount.toString(),

slippageBps:      validSlippage.toString(),

onlyDirectRoutes: 'false',

asLegacyTransaction: 'false',

maxAccounts:      '64'

const url = ${JUPITER_API}/quote?${params};

const response = await fetch(url);

if (!response.ok) {

const txt = await response.text();

throw new Error(`Jupiter quote error ${response.status}: ${txt}`);

const data = await response.json();

if (data.error) throw new Error(data.error);

if (!data.inAmount || !data.outAmount) throw new Error('Invalid Jupiter response');

return data;

}

Page 31 of 131

}

};

});

async function executeJupiterSwap(quote, wallet, priorityFee = 0.001) {

if (!wallet?.keypair) throw new Error('Invalid wallet');

const priorityFeeLamports = Math.floor(Math.max(0.0001, Math.min(priorityFee, 0.1)) * LAMPORTS_PER_SOL);

const body = {

quoteResponse:             quote,

userPublicKey:             wallet.publicKey,

wrapAndUnwrapSol:          true,

dynamicComputeUnitLimit:   true,

prioritizationFeeLamports: priorityFeeLamports

const swapRes = await fetch(${JUPITER_API}/swap, {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify(body)

if (!swapRes.ok) {

const txt = await swapRes.text();

throw new Error(`Jupiter swap error ${swapRes.status}: ${txt}`);

const swapData = await swapRes.json();

Page 32 of 131

if (swapData.error) throw new Error(swapData.error);

if (!swapData.swapTransaction) throw new Error('No swap transaction returned');

const txBuf = Buffer.from(swapData.swapTransaction, 'base64');

const transaction = VersionedTransaction.deserialize(txBuf);

transaction.sign([wallet.keypair]);

const txid = await connection.sendRawTransaction(transaction.serialize(), {

skipPreflight: false, preflightCommitment: 'confirmed', maxRetries: 3

const conf = await Promise.race([

connection.confirmTransaction(txid, 'confirmed'),

new Promise((_, r) => setTimeout(() => r(new Error('Confirmation timeout (90s)')), 90000))

if (conf.value?.err) throw new Error('On-chain failure: ' + JSON.stringify(conf.value.err));

return { success: true, txid };

});

]);

}

// ============================================

// TOKEN ANALYSIS

async function fetchTokenData(address) {

Page 33 of 131

try {

const r    = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${address}`);

const data = await r.json();

if (!data.pairs?.length) return null;

return data.pairs.filter(p => p.chainId === 'solana')

  .sort((a,b) => (b.liquidity?.usd||0) - (a.liquidity?.usd||0))[0];

} catch { return null; }

function calculateSecurityScore(pair) {

let score = 50; const warnings = [], positives = [];

const liq = pair.liquidity?.usd || 0;

const vol = pair.volume?.h24 || 0;

const chg = pair.priceChange?.h24 || 0;

if (liq > 100000) { score += 20; positives.push('✅ Strong liquidity'); }

else if (liq > 50000) { score += 10; positives.push('✅ Good liquidity'); }

else if (liq < 10000) { score -= 20; warnings.push('⚠️ Low liquidity'); }

if (vol > 100000) { score += 10; positives.push('✅ High volume'); }

else if (vol < 5000) { score -= 10; warnings.push('⚠️ Low volume'); }

if (chg < -50) { score -= 25; warnings.push('🚨 RUG ALERT: Major dump'); }

Page 34 of 131

else if (chg < -30) { score -= 15; warnings.push('⚠️ Significant drop'); }

else if (chg > 20) positives.push('📈 Strong momentum');

const ageDays = (Date.now() - (pair.pairCreatedAt || Date.now())) / 86400000;

if (ageDays < 1) { score -= 15; warnings.push('⚠️ New token (<24h)'); }

else if (ageDays > 7) { score += 10; positives.push('✅ Established pool (7d+)'); }

const v2l = vol / (liq || 1);

if (v2l > 2) positives.push('✅ Healthy vol/liq ratio');

else if (v2l < 0.1) warnings.push('⚠️ Low trading activity');

return { score: Math.max(0, Math.min(100, score)), warnings, positives };

function generateScoreBar(score) {

const fill = Math.round((score/100)*10);

return '[' + '█'.repeat(fill) + '░'.repeat(10-fill) + ']';

function getSecurityRating(score) {

if (score >= 80) return { emoji:'🟢', text:'SAFE', advice:'Low risk entry' };

if (score >= 60) return { emoji:'🟡', text:'MODERATE', advice:'Proceed with caution' };

if (score >= 40) return { emoji:'🟠', text:'RISKY', advice:'High risk — small position only' };

Page 35 of 131

return { emoji:'🔴', text:'DANGER', advice:'Avoid or wait for better setup' };

function getMarketTrend(c) {

if (c > 50) return 'PUMPING 🚀';

if (c > 20) return 'BULLISH 📈';

if (c > 5) return 'UPTREND ↗️';

if (c > -5) return 'CONSOLIDATING ➡️';

if (c > -20) return 'DOWNTREND ↘️';

if (c > -50) return 'BEARISH 📉';

return 'CRASHING 💥';

async function sendTokenAnalysis(ctx, address) {

const loadingMsg = await ctx.reply('🔍 Analyzing token...');

try {

const session      = await getSession(ctx.from.id);

const activeWallet = getActiveWallet(session);

const pair         = await fetchTokenData(address);

const userId       = ctx.from.id;

if (!pair) {

  await ctx.telegram.editMessageText(ctx.chat.id, loadingMsg.message_id, null, '❌ Token not found or no liquidity on Solana.');

  return;

const { score, warnings, positives } = calculateSecurityScore(pair);
Page 36 of 131
const price     = parseFloat(pair.priceUsd) || 0;

const chg1h     = pair.priceChange?.h1  || 0;

const chg6h     = pair.priceChange?.h6  || 0;

const chg24h    = pair.priceChange?.h24 || 0;

const mcap      = pair.marketCap || pair.fdv || 0;

const liq       = pair.liquidity?.usd || 0;

const vol       = pair.volume?.h24 || 0;

const solPrice  = await getSolPriceWithCache();

const toksPer1Sol = (price > 0 && solPrice > 0) ? (solPrice / price) : 0;

let pnlSection = '';

if (activeWallet?.publicKey) {

  try {

    const solBal   = await getBalance(activeWallet.publicKey);

    const { amount: tokBal } = await getTokenBalance(activeWallet.publicKey, address);

    if (tokBal > 0) {

      const valUsd = tokBal * price;

      const pnlVal = valUsd * (chg24h/100);

      const pnlSign = chg24h >= 0 ? '+' : '';
Page 37 of 131
      pnlSection = `\n━━━━━━━━━━━━━━━━━━\n💼 YOUR POSITION\n🪙 Balance: ${tokBal.toFixed(4)} ${pair.baseToken?.symbol||'tokens'}\n💵 Value: *$${valUsd.toFixed(2)}*\n📊 24h PNL: ${chg24h>=0?'🟢':'🔴'} *${pnlSign}${pnlVal.toFixed(2)}* (${pnlSign}${chg24h.toFixed(2)}%)\n💰 SOL Balance: *${solBal.toFixed(4)} SOL*`;

  } catch { /* non-critical */ }

const rating     = getSecurityRating(score);

const scoreBar   = generateScoreBar(score);

const trend      = getMarketTrend(chg24h);

const ageDays    = Math.floor((Date.now() - (pair.pairCreatedAt || Date.now())) / 86400000);

const ageHours   = Math.floor((Date.now() - (pair.pairCreatedAt || Date.now())) / 3600000);

const ageDisplay = ageDays > 0 ? `${ageDays} days` : `${ageHours} hours`;

// HIDE commission from users, show only to admins

let feeNote = '';

if (isAdmin(userId) && COMMISSION_PERCENTAGE > 0) {

  feeNote = `\n💸 Platform fee: ${COMMISSION_PERCENTAGE}% applies`;

} else if (COMMISSION_PERCENTAGE > 0) {

  feeNote = '\n💎 Trade with low fees';

const message    = `*🎯 FOMO TOKEN SCANNER*

🪙 ${pair.baseToken?.name||'Unknown'} (${pair.baseToken?.symbol||'???'})

Page 38 of 131

`${address}`

━━━━━━━━━━━━━━━━━━

💰 MARKET DATA

📊 Exchange: ${pair.dexId||'Unknown'}

💵 Price: ${formatTokenPrice(price)}

🟢 1h: ${chg1h>=0?'+':''}${chg1h.toFixed(2)}% | 6h: ${chg6h>=0?'+':''}${chg6h.toFixed(2)}%

${chg24h>=0?'🟢':'🔴'} 24h: ${chg24h>=0?'+':''}${chg24h.toFixed(2)}% ${trend}

📈 MCap: ${formatNumber(mcap)}

💧 Liq: ${formatNumber(liq)}

📊 Vol: ${formatNumber(vol)}

🛡️ SECURITY

Score: ${scoreBar} ${score}/100

Rating: ${rating.emoji} ${rating.text}

]);

});

}

}

// ============================================

// TRADE RECORDING

async function recordTrade(userId, tradeData) {

Page 39 of 131

const session = await getSession(userId);

const record = {

id:           Date.now().toString(36) + Math.random().toString(36).substr(2),

timestamp:    new Date().toISOString(),

date:         new Date().toDateString(),

time:         new Date().toLocaleTimeString(),

type:         tradeData.type,

tokenAddress: tradeData.tokenAddress,

tokenSymbol:  tradeData.tokenSymbol || 'Unknown',

tokenName:    tradeData.tokenName   || 'Unknown',

amountSol:    tradeData.amountSol   || 0,

amountToken:  tradeData.amountToken || 0,

priceUsd:     tradeData.priceUsd    || 0,

txHash:       tradeData.txHash,

valueUsd:     tradeData.valueUsd    || 0,

pnlUsd:       tradeData.pnlUsd      || 0,

commission:   tradeData.commission  || 0

session.tradeHistory.unshift(record);

if (session.tradeHistory.length > 100) session.tradeHistory = session.tradeHistory.slice(0, 100);

await saveSession(userId, session);

Page 40 of 131

return record;

// ============================================

// TRADE RECORDING

async function recordTrade(userId, tradeData) {

const session = await getSession(userId);

const today = new Date().toDateString();

if (!session.dailyStats || session.dailyStats.date !== today)

session.dailyStats = { date: today, totalTrades:0, profitableTrades:0, lossTrades:0, totalPnl:0 };

session.dailyStats.totalTrades++;

if (record.pnlUsd > 0) { session.dailyStats.profitableTrades++; session.dailyStats.totalPnl += record.pnlUsd; }

else if (record.pnlUsd < 0) { session.dailyStats.lossTrades++; session.dailyStats.totalPnl += record.pnlUsd; }

await saveSession(userId, session);

return record;

};

}

}

// ============================================

// SNIPER (TP/SL)

Page 41 of 131

async function addSnipe(userId, token, entryPrice, amountToken, txHash, tpPrice = null, slPrice = null) {

const session = await getSession(userId);

const existing = session.activeSnipes.find(s => s.token === token);

if (existing) {

if (tpPrice !== null) existing.tpPrice = tpPrice;

if (slPrice !== null) existing.slPrice = slPrice;

} else {

session.activeSnipes.push({ token, entryPrice, amountToken, txHash, tpPrice, slPrice, createdAt: Date.now() });

await saveSession(userId, session);

async function removeSnipe(userId, token) {

const session = await getSession(userId);

session.activeSnipes = session.activeSnipes.filter(s => s.token !== token);

await saveSession(userId, session);

async function executeSellForSnipe(userId, token, reason) {

const session = await getSession(userId);

const activeWallet = getActiveWallet(session);

if (!activeWallet) return;

const tokenBalance = await getTokenBalance(activeWallet.publicKey, token);

Page 42 of 131

if (!tokenBalance || tokenBalance.amount <= 0) { await removeSnipe(userId, token); return; }

const decimals = tokenBalance.decimals || 9;

const sellAmount = Math.floor(tokenBalance.amount * Math.pow(10, decimals));

if (sellAmount <= 0) return;

try {

const slippageBps = Math.max(50, Math.min(Math.floor(session.settings.slippage * 100), 5000));

const quote       = await getJupiterQuote(token, SOL_MINT, sellAmount, slippageBps);

if (!quote?.outAmount) throw new Error('No route');

const result      = await executeJupiterSwap(quote, activeWallet, session.settings.priorityFee);

const receivedSol = parseInt(quote.outAmount) / LAMPORTS_PER_SOL;

// Commission

const commissionSol = receivedSol * (COMMISSION_PERCENTAGE / 100);

await sendCommission(activeWallet, commissionSol);

const pair        = await fetchTokenData(token);

const tokenSymbol = pair?.baseToken?.symbol || 'Unknown';

const priceUsd    = parseFloat(pair?.priceUsd) || 0;

const solPrice    = await getSolPriceWithCache();

const valueUsd    = receivedSol * solPrice;
Page 43 of 131
let pnlUsd        = 0;

const buys        = session.tradeHistory.filter(t => t.type === 'BUY' && t.tokenAddress === token);

if (buys.length) {

  const avgBuy  = buys.reduce((s,t) => s+(t.valueUsd||0), 0) / buys.reduce((s,t) => s+(t.amountToken||0), 1);

  pnlUsd = valueUsd - (sellAmount / Math.pow(10, decimals)) * avgBuy;

await recordTrade(userId, { type:'SELL', tokenAddress:token, tokenSymbol, amountSol:receivedSol, amountToken:sellAmount/Math.pow(10,decimals), priceUsd, txHash:result.txid, valueUsd, pnlUsd, commission:commissionSol });

await removeSnipe(userId, token);

const txLink = `https://solscan.io/tx/${result.txid}`;

await bot.telegram.sendMessage(userId,

  `🎯 <b>SNIPER EXECUTED</b>\n${escapeHtml(reason)}\nToken: <code>${shortenAddress(token)}</code>\nSold ${(sellAmount/Math.pow(10,decimals)).toFixed(4)} ${escapeHtml(tokenSymbol)} → ${(receivedSol*(1-COMMISSION_PERCENTAGE/100)).toFixed(4)} SOL\n📝 <a href="${txLink}">View TX</a>`,

  { parse_mode:'HTML' }

} catch (err) { console.error(Sniper sell error for ${token}:, err.message); }

}

);

}

let priceCheckInterval = null;

Page 44 of 131

function startPriceChecker() {

if (priceCheckInterval) clearInterval(priceCheckInterval);

priceCheckInterval = setInterval(async () => {

try {

  const allUsers = await storage.getSubscribers();

  for (const userId of allUsers) {

    try {

      const session = await getSession(userId);

      if (!session.activeSnipes.length) continue;

      for (const snipe of [...session.activeSnipes]) {

        try {

          const pair = await fetchTokenData(snipe.token);

          if (!pair?.priceUsd) continue;

          const cur = parseFloat(pair.priceUsd);

          if      (snipe.tpPrice && cur >= snipe.tpPrice)

            await executeSellForSnipe(userId, snipe.token, `✅ Take-Profit hit! $${snipe.tpPrice} → $${cur.toFixed(8)}`);

          else if (snipe.slPrice && cur <= snipe.slPrice)

            await executeSellForSnipe(userId, snipe.token, `❌ Stop-Loss hit! $${snipe.slPrice} → $${cur.toFixed(8)}`);

        } catch (e) { console.error(`Price check token ${snipe.token}:`, e.message); }
Page 45 of 131
    } catch (e) { console.error(`Price check user ${userId}:`, e.message); }

} catch (e) { console.error('Price checker error:', e.message); }

}, 10000);

startPriceChecker();

      }

  }

}

// PNL IMAGE (QuickChart)

// ============================================

async function generatePNLImage(session) {

const history = session.tradeHistory || [];

if (!history.length) return null;

const totalPnl = history.reduce((s,t) => s+(t.pnlUsd||0), 0);

const profitable = history.filter(t => t.pnlUsd > 0).length;

const losses = history.filter(t => t.pnlUsd < 0).length;

const dailyPnl = {};

history.forEach(t => {

const d = t.date || new Date(t.timestamp).toDateString();

dailyPnl[d] = (dailyPnl[d]||0) + (t.pnlUsd||0);

const dates = Object.keys(dailyPnl).slice(-7);

Page 46 of 131

const values = dates.map(d => dailyPnl[d]);

const cfg = {

type: 'bar',

data: {

  labels:   dates.map(d => d.slice(0,5)),

  datasets: [{ label:'PNL ($)', data:values, backgroundColor:values.map(v => v>=0?'#22c55e':'#ef4444'), borderRadius:4 }]

options: {

  title:  { display:true, text:`Total PNL: $${totalPnl.toFixed(2)} | W:${profitable} L:${losses}`, fontSize:16 },

  legend: { display:false },

  scales: { yAxes:[{ ticks:{ callback: v => '$'+v } }] }

return https://quickchart.io/chart?c=${encodeURIComponent(JSON.stringify(cfg))}&w=600&h=400;

bot.action('pnl_image', async (ctx) => {

await ctx.answerCbQuery('📊 Generating chart...');

const session = await getSession(ctx.from.id);

const imageUrl = await generatePNLImage(session);

if (imageUrl) await ctx.replyWithPhoto({ url: imageUrl }, { caption: '📈 Your PNL Chart' });

else await ctx.reply('No trades to chart yet.');

});

Page 47 of 131
},

}

};

}

});

// BROADCAST

// ============================================

async function broadcastToSubscribersMedia(adminId, broadcastData) {

const subs = await storage.getSubscribers();

if (!subs.length) { await bot.telegram.sendMessage(adminId, '❌ No subscribers.'); return; }

let ok = 0, fail = 0;

for (const userId of subs) {

try {

  const { type, content, caption } = broadcastData;

  if      (type === 'text')      await bot.telegram.sendMessage(userId, content, { parse_mode:'HTML' });

  else if (type === 'photo')     await bot.telegram.sendPhoto(userId, content, { caption, parse_mode:'HTML' });

  else if (type === 'video')     await bot.telegram.sendVideo(userId, content, { caption, parse_mode:'HTML' });

  else if (type === 'document')  await bot.telegram.sendDocument(userId, content, { caption, parse_mode:'HTML' });
Page 48 of 131
  else if (type === 'animation') await bot.telegram.sendAnimation(userId, content, { caption, parse_mode:'HTML' });

  ok++;

  await new Promise(r => setTimeout(r, 50));

} catch { fail++; }

await bot.telegram.sendMessage(adminId, ✅ Broadcast done!\n📨 Sent: ${ok}\n⚠️ Failed: ${fail});

bot.on('photo', async (ctx) => {

const session = await getSession(ctx.from.id);

if (!session.awaitingBroadcast) return;

if (!ADMIN_CHAT_IDS.includes(ctx.from.id.toString())) { session.awaitingBroadcast = false; await saveSession(ctx.from.id, session); return; }

const photo = ctx.message.photo[ctx.message.photo.length-1];

session.awaitingBroadcast = false;

await saveSession(ctx.from.id, session);

await broadcastToSubscribersMedia(ctx.from.id, { type:'photo', content:photo.file_id, caption:ctx.message.caption||'' });

bot.on('video', async (ctx) => {

const session = await getSession(ctx.from.id);

if (!session.awaitingBroadcast) return;

if (!ADMIN_CHAT_IDS.includes(ctx.from.id.toString())) { session.awaitingBroadcast = false; await saveSession(ctx.from.id, session); return; }

session.awaitingBroadcast = false;

Page 49 of 131

await saveSession(ctx.from.id, session);

await broadcastToSubscribersMedia(ctx.from.id, { type:'video', content:ctx.message.video.file_id, caption:ctx.message.caption||'' });

});

}

bot.on('animation', async (ctx) => {

const session = await getSession(ctx.from.id);

if (!session.awaitingBroadcast) return;

if (!ADMIN_CHAT_IDS.includes(ctx.from.id.toString())) { session.awaitingBroadcast = false; await saveSession(ctx.from.id, session); return; }

session.awaitingBroadcast = false;

await saveSession(ctx.from.id, session);

await broadcastToSubscribersMedia(ctx.from.id, { type:'animation', content:ctx.message.animation.file_id, caption:ctx.message.caption||'' });

// ============================================

// BUY HANDLER — commission deducted from SOL before swap

async function handleBuy(ctx, amount, tokenAddress, tpPrice = null, slPrice = null) {

const session = await getSession(ctx.from.id);

const activeWallet = getActiveWallet(session);

const userId = ctx.from.id;

if (!activeWallet) {

Page 50 of 131
await ctx.reply('❌ No wallet. Connect one first.', { ...Markup.inlineKeyboard([[Markup.button.callback('💼 Wallet', 'menu_wallet')]]) });

return;

if (!isSolanaAddress(tokenAddress)) { await ctx.reply('❌ Invalid token address.'); return; }

const commissionSol = COMMISSION_PERCENTAGE > 0 && COMMISSION_WALLET ? amount * (COMMISSION_PERCENTAGE/100) : 0;

const swapSol = amount - commissionSol;

const balance = await getBalance(activeWallet.publicKey);

const totalNeed = amount + session.settings.priorityFee + 0.006;

if (balance < totalNeed) {

await ctx.reply(`❌ Insufficient SOL.\nHave: ${balance.toFixed(4)}\nNeed: ~${totalNeed.toFixed(4)} (swap + fee + priority)`);

return;

}

);

// HIDE commission from user, show to admin only

let feeDisplay = '';

if (isAdmin(userId) && commissionSol > 0) {

feeDisplay = `\nFee: ${commissionSol.toFixed(4)} SOL (${COMMISSION_PERCENTAGE}%)`;

} else if (commissionSol > 0) {

feeDisplay = '\n💎 Trade with low fees';
Page 51 of 131

const statusMsg = await ctx.reply(

`🔄 Processing Buy\nAmount: ${swapSol.toFixed(4)} SOL${feeDisplay}\nToken: \`${shortenAddress(tokenAddress)}\`\nSlippage: ${session.settings.slippage}%\n\nGetting Jupiter quote...`,

{ parse_mode:'Markdown' }

try {

const lamports      = Math.floor(swapSol * LAMPORTS_PER_SOL);

const slippageBps   = Math.max(50, Math.min(Math.floor(session.settings.slippage * 100), 5000));

const quote         = await getJupiterQuote(SOL_MINT, tokenAddress, lamports, slippageBps);

if (!quote?.outAmount) throw new Error('No route found — token may have insufficient liquidity.');

const outputDecimals = quote.outputDecimals || 9;

const expectedOut    = parseInt(quote.outAmount) / Math.pow(10, outputDecimals);

await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, null,

  `🔄 Executing swap...\n~${expectedOut.toFixed(4)} tokens expected`, { parse_mode:'Markdown' });

const result = await executeJupiterSwap(quote, activeWallet, session.settings.priorityFee);

// Send commission as separate SOL transfer
Page 52 of 131
if (commissionSol > 0) await sendCommission(activeWallet, commissionSol);

const receivedAmount = parseInt(quote.outAmount) / Math.pow(10, outputDecimals);

let tokenSymbol = 'Unknown', tokenName = 'Unknown', priceUsd = 0;

try {

  const pair  = await fetchTokenData(tokenAddress);

  tokenSymbol = pair?.baseToken?.symbol || 'Unknown';

const txLink = `https://solscan.io/tx/${result.txid}`;

// HIDE commission in success message

let successFeeDisplay = '';

if (isAdmin(userId) && commissionSol > 0) {

  successFeeDisplay = `\n💸 Fee: ${commissionSol.toFixed(4)} SOL`;

} else if (commissionSol > 0) {

  successFeeDisplay = '\n💎 Trade with low fees';

await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, null,

  `✅ Buy Successful!\n💰 Spent: ${swapSol.toFixed(4)} SOL${successFeeDisplay}\n🪙 Received: ~${receivedAmount.toFixed(4)} ${escapeHtml(tokenSymbol)}\n💵 Value: ~$${valueUsd.toFixed(2)}\n📝 TX: <a href="${txLink}">${result.txid}</a>`,

  { parse_mode:'HTML', disable_web_page_preview:true, ...Markup.inlineKeyboard([
Page 53 of 131
      [Markup.button.url('🔍 Solscan', txLink)],

      [Markup.button.callback('🏠 Menu', 'back_main')]

}

    ])

  }

);

  );

} catch (error) {

console.error('Buy error:', error);

try {

  await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, null,

    `❌ Buy Failed\n\n${escapeHtml(error.message)}`,

    { parse_mode:'HTML', ...Markup.inlineKeyboard([[Markup.button.callback('🔄 Retry', `buy_${amount}_${tokenAddress}`)],[Markup.button.callback('🏠 Menu', 'back_main')]]) }

} catch { /* ignore */ }

// ============================================

// SELL HANDLER — commission from received SOL

async function handleSell(ctx, percentage, tokenAddress) {

const session = await getSession(ctx.from.id);

const activeWallet = getActiveWallet(session);

Page 54 of 131

const userId = ctx.from.id;

if (!activeWallet) {

await ctx.reply('❌ No wallet. Connect one first.', { ...Markup.inlineKeyboard([[Markup.button.callback('💼 Wallet', 'menu_wallet')]]) });

return;

if (!isSolanaAddress(tokenAddress)) { await ctx.reply('❌ Invalid token address.'); return; }

const validPct = Math.max(1, Math.min(percentage, 100));

const statusMsg = await ctx.reply(

`🔄 Processing Sell\nSelling: ${validPct}%\nToken: \`${shortenAddress(tokenAddress)}\`\nSlippage: ${session.settings.slippage}%\n\nChecking balance...`,

{ parse_mode:'Markdown' }

try {

const tokenBalance = await getTokenBalance(activeWallet.publicKey, tokenAddress);

if (!tokenBalance || tokenBalance.amount <= 0) {

  await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, null, '❌ No tokens to sell.');

  return;

const decimals   = tokenBalance.decimals || 9;

const sellAmount = Math.floor(tokenBalance.amount * (validPct/100) * Math.pow(10, decimals));
Page 55 of 131
if (sellAmount <= 0) {

  await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, null, '❌ Sell amount too small (dust).');

const slippageBps  = Math.max(50, Math.min(Math.floor(session.settings.slippage * 100), 5000));

const displayAmt   = (sellAmount / Math.pow(10, decimals)).toFixed(4);

await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, null,

  `🔄 Selling ${displayAmt} tokens...\nGetting quote...`, { parse_mode:'Markdown' });

const quote = await getJupiterQuote(tokenAddress, SOL_MINT, sellAmount, slippageBps);

if (!quote?.outAmount) throw new Error('No route — token may have insufficient liquidity.');

const expectedSol = parseInt(quote.outAmount) / LAMPORTS_PER_SOL;

  `🔄 Executing swap...\n~${expectedSol.toFixed(4)} SOL expected`, { parse_mode:'Markdown' });

const result      = await executeJupiterSwap(quote, activeWallet, session.settings.priorityFee);

const receivedSol = parseInt(quote.outAmount) / LAMPORTS_PER_SOL;

// Commission from received SOL

const commissionSol = COMMISSION_PERCENTAGE > 0 && COMMISSION_WALLET ? receivedSol * (COMMISSION_PERCENTAGE/100) : 0;
Page 56 of 131
if (commissionSol > 0) await sendCommission(activeWallet, commissionSol);

const netSol = receivedSol - commissionSol;

let tokenSymbol = 'Unknown', tokenName = 'Unknown', priceUsd = 0;

try {

  const pair  = await fetchTokenData(tokenAddress);

  tokenSymbol = pair?.baseToken?.symbol || 'Unknown';

  tokenName   = pair?.baseToken?.name   || 'Unknown';

  priceUsd    = parseFloat(pair?.priceUsd) || 0;

} catch { /* non-critical */ }

const solPrice = await getSolPriceWithCache();

const valueUsd = receivedSol * solPrice;

let pnlUsd     = 0;

const buys     = session.tradeHistory.filter(t => t.type==='BUY' && t.tokenAddress===tokenAddress);

if (buys.length) {

  const totalSpent  = buys.reduce((s,t) => s+(t.valueUsd||0), 0);

  const totalBought = buys.reduce((s,t) => s+(t.amountToken||0), 0);

  const avgBuy      = totalBought > 0 ? totalSpent/totalBought : 0;

  pnlUsd = valueUsd - (sellAmount/Math.pow(10,decimals)) * avgBuy;
Page 57 of 131
await recordTrade(ctx.from.id, { type:'SELL', tokenAddress, tokenSymbol, tokenName, amountSol:receivedSol, amountToken:sellAmount/Math.pow(10,decimals), priceUsd, txHash:result.txid, valueUsd, pnlUsd, commission:commissionSol });

await removeSnipe(ctx.from.id, tokenAddress);

await notifyAdmin('TRADE_EXECUTED', ctx.from.id, ctx.from.username, { type:'SELL', amount:validPct+'%', token:tokenAddress, txHash:result.txid, commission:commissionSol.toFixed(6) });

const txLink  = `https://solscan.io/tx/${result.txid}`;

const pnlText = pnlUsd !== 0 ? `\n${pnlUsd>=0?'🟢':'🔴'} PNL: ${pnlUsd>=0?'+':''}${Math.abs(pnlUsd).toFixed(2)}` : '';

// HIDE commission in success message

let successFeeDisplay = '';

if (isAdmin(userId) && commissionSol > 0) {

  successFeeDisplay = `\n💸 Fee: ${commissionSol.toFixed(4)} SOL`;

} else if (commissionSol > 0) {

  successFeeDisplay = '\n💎 Trade with low fees';

await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, null,

  `✅ Sell Successful!\n💰 Sold: ${displayAmt} ${escapeHtml(tokenSymbol)}\n🪙 Received: ${netSol.toFixed(4)} SOL${successFeeDisplay}\n💵 Value: ~$${valueUsd.toFixed(2)}${pnlText}\n📝 TX: <a href="${txLink}">${result.txid}</a>`,

  { parse_mode:'HTML', disable_web_page_preview:true, ...Markup.inlineKeyboard([
Page 58 of 131
      [Markup.button.url('🔍 Solscan', txLink)],

      [Markup.button.callback('🏠 Menu', 'back_main')]

}

    ])

  }

);

  );

}

}

} catch (error) {

console.error('Sell error:', error);

try {

  await ctx.telegram.editMessageText(ctx.chat.id, statusMsg.message_id, null,

    `❌ Sell Failed\n\n${escapeHtml(error.message)}`,

    { parse_mode:'HTML', ...Markup.inlineKeyboard([[Markup.button.callback('🔄 Retry', `sell_${percentage}_${tokenAddress}`)],[Markup.button.callback('🏠 Menu', 'back_main')]]) }

} catch { /* ignore */ }

async function showMainMenu(ctx, edit = false) {

try {

const session      = await getSession(ctx.from.id);
Page 59 of 131
const activeWallet = getActiveWallet(session);

const userId       = ctx.from.id;

let balance = null, solPrice = 0, usdValue = 0, extra = '';

if (activeWallet?.publicKey) {

  try { balance  = await getBalanceWithFallback(activeWallet.publicKey); } catch { const c = balanceCache.get(activeWallet.publicKey); if (c) { balance = c.balance; extra = '(cached)'; } }

  try { solPrice = await getSolPriceWithCache(); } catch { solPrice = solPriceCache.price || 0; }

  usdValue = (balance || 0) * solPrice;

const todayPnl  = session.dailyStats?.totalPnl || 0;

const pnlEmoji  = todayPnl >= 0 ? '🟢' : '🔴';

const pnlSign   = todayPnl >= 0 ? '+' : '';

let walletInfo;

if (!activeWallet) {

  walletInfo = '⚠️ *No wallet connected*\nTap 💼 Wallet to create or import';

} else {

  const short = shortenAddress(activeWallet.publicKey);

  if (balance === null) walletInfo = `💼 *Wallet ${session.activeWalletIndex+1}/${session.wallets.length}:* \`${short}\`\n💰 *Balance: Loading...*`;

  else {
Page 60 of 131
    const usdPart = solPrice > 0 ? ` ($${usdValue.toFixed(2)})` : '';

    walletInfo = `💼 *Wallet ${session.activeWalletIndex+1}/${session.wallets.length}:* \`${short}\`\n💰 *Balance:* ${balance.toFixed(4)} SOL${usdPart} ${extra}`;

// HIDE commission from users, show to admins only

let feeNote = '';

if (isAdmin(userId) && COMMISSION_PERCENTAGE > 0) {

  feeNote = `\n💸 *Platform fee: ${COMMISSION_PERCENTAGE}%* per trade`;

} else if (COMMISSION_PERCENTAGE > 0) {

  feeNote = '\n💎 *Trade with low fees* — Use referral link';

}

  }

const message = `👋 *fomo Trading Bot*\n\n*Your AI Trading Assistant on Solana* ⚡\n━━━━━━━━━━━━━━━━━━━━━━━\n🚨 Real-time pump detection\n💳 Instant wallet alerts\n🎯 Auto TP/SL management\n🤖 Smart trade execution${feeNote}\n━━━━━━━━━━━━━━━━━━━━━━━\n📊 Today PNL: ${pnlEmoji} ${pnlSign}$${Math.abs(todayPnl).toFixed(2)}\n\n${walletInfo}\n\nPaste a Solana contract address to analyze`;

const keyboard = Markup.inlineKeyboard([

  [Markup.button.callback('💼 Wallet', 'menu_wallet')],

  [Markup.button.callback('📊 Positions', 'menu_positions'), Markup.button.callback('🚀 Buy', 'menu_buy')],

  [Markup.button.callback('💸 Sell', 'menu_sell'), Markup.button.callback('🎯 Sniper (TP/SL)', 'menu_sniper')],
Page 61 of 131
  [Markup.button.callback('📜 Trade History', 'menu_history'), Markup.button.callback('📈 PNL Report', 'menu_pnl_report')],

  [Markup.button.callback('⚙️ Settings', 'menu_settings'), Markup.button.callback('🎁 Referrals', 'menu_referrals')],

  [Markup.button.callback('❓ Help', 'menu_help'), Markup.button.callback('🔄 Refresh', 'refresh_main')]

try {

  if (edit && ctx.callbackQuery) await ctx.editMessageText(message, { parse_mode:'Markdown', ...keyboard });

  else                           await ctx.reply(message, { parse_mode:'Markdown', ...keyboard });

} catch { await ctx.reply(message, { parse_mode:'Markdown', ...keyboard }); }

} catch (error) {

console.error('Main menu error:', error);

await ctx.reply('🚀 fomo Bot\n\n⚠️ Error loading menu', { ...Markup.inlineKeyboard([[Markup.button.callback('🔄 Refresh', 'refresh_main')]]) });

async function showWalletMenu(ctx, edit = false) {

try {

const session = await getSession(ctx.from.id);

const active  = getActiveWallet(session);

let message, keyboardButtons = [];

if (session.wallets.length > 0) {
Page 62 of 131
  let solPrice = 0;

  try { solPrice = await getSolPriceWithCache(); } catch { /* ignore */ }

  let walletList = '';

  for (let i = 0; i < session.wallets.length; i++) {

    const w       = session.wallets[i];

    const isAct   = i === session.activeWalletIndex;

    let bal = 0;

    try { bal = await getBalance(w.publicKey); } catch { /* ignore */ }

    walletList += `${isAct?'✅':'⚪'} *Wallet ${i+1}:* \`${shortenAddress(w.publicKey)}\` (${bal.toFixed(3)} SOL${solPrice>0?` ~$${(bal*solPrice).toFixed(2)}`:''} )\n`;

  let activeBal = 0;

  try { activeBal = await getBalance(active.publicKey); } catch { /* ignore */ }

  message = `💼 Wallet Management\n\n${walletList}\n📍 *Active:* \`${active.publicKey}\`\n💰 Balance: ${activeBal.toFixed(4)} SOL${solPrice>0?` ($${(activeBal*solPrice).toFixed(2)})`:''}\n\nTap wallet to switch:`;

  keyboardButtons.push(session.wallets.map((_,i) => Markup.button.callback(`${i===session.activeWalletIndex?'✅':'🪪'} W${i+1}`, `switch_wallet_${i}`)));

  keyboardButtons.push([Markup.button.callback('📥 Deposit', 'wallet_deposit'), Markup.button.callback('📤 Transfer', 'wallet_transfer_menu')]);
Page 63 of 131
  keyboardButtons.push([Markup.button.callback('📤 Export Keys', 'wallet_export'), Markup.button.callback('🗑️ Remove', 'wallet_remove')]);

  if (session.wallets.length < MAX_WALLETS) keyboardButtons.push([Markup.button.callback('🆕 Create New Wallet', 'wallet_create')]);

  keyboardButtons.push([Markup.button.callback('📥 Import Seed', 'wallet_import_seed'), Markup.button.callback('🔑 Import Key', 'wallet_import_key')]);

  keyboardButtons.push([Markup.button.callback('🔄 Refresh', 'wallet_refresh'), Markup.button.callback('« Back', 'back_main')]);

} else {

  message = `💼 Wallet Management\n\nNo wallet yet. Up to ${MAX_WALLETS} wallets supported.`;

  keyboardButtons = [

    [Markup.button.callback('🆕 Create New Wallet',  'wallet_create')],

    [Markup.button.callback('📥 Import Seed Phrase', 'wallet_import_seed')],

    [Markup.button.callback('🔑 Import Private Key', 'wallet_import_key')],

    [Markup.button.callback('« Back', 'back_main')]

const keyboard = Markup.inlineKeyboard(keyboardButtons);

try {

  if (edit && ctx.callbackQuery) await ctx.editMessageText(message, { parse_mode:'Markdown', ...keyboard });
Page 64 of 131
  else                           await ctx.reply(message, { parse_mode:'Markdown', ...keyboard });

} catch { await ctx.reply(message, { parse_mode:'Markdown', ...keyboard }); }

} catch (error) {

console.error('Wallet menu error:', error);

await ctx.reply('❌ Error loading wallet menu.');

  }

  ];

}

}

}

async function showPositionsMenu(ctx, edit = false) {

const session = await getSession(ctx.from.id);

const active = getActiveWallet(session);

if (!active) {

const msg = '❌ Please connect a wallet first.';

const kb  = Markup.inlineKeyboard([[Markup.button.callback('💼 Connect Wallet','menu_wallet')],[Markup.button.callback('« Back','back_main')]]);

if (edit) await ctx.editMessageText(msg, kb); else await ctx.reply(msg, kb);

return;
Page 65 of 131

const msg = 📊 Your Positions\n💼 Wallet: \${shortenAddress(active.publicKey)}`\n\nNo open positions tracked.\nPaste a token address to analyze and trade.`;

const kb = Markup.inlineKeyboard([[Markup.button.callback('🔄 Refresh','refresh_positions')],[Markup.button.callback('« Back','back_main')]]);

if (edit) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

}

}

]);

async function showBuyMenu(ctx, edit = false) {

const userId = ctx.from.id;

let feeNote = '';

if (isAdmin(userId) && COMMISSION_PERCENTAGE > 0) {

feeNote = `\n💸 ${COMMISSION_PERCENTAGE}% fee applies`;

} else if (COMMISSION_PERCENTAGE > 0) {

feeNote = '\n💎 Trade with low fees';

const msg = 🟢 Quick Buy${feeNote}\n\nPaste a token address, or pick an amount then paste the address.;

const kb = Markup.inlineKeyboard([

[Markup.button.callback('🚀 0.1 SOL', 'setbuy_0.1'), Markup.button.callback('🚀 0.2 SOL', 'setbuy_0.2')],
Page 66 of 131
[Markup.button.callback('🚀 0.5 SOL', 'setbuy_0.5'), Markup.button.callback('🚀 1 SOL',   'setbuy_1')],

[Markup.button.callback('« Back', 'back_main')]

if (edit) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

async function showSellMenu(ctx, edit = false) {

const userId = ctx.from.id;

let feeNote = '';

if (isAdmin(userId) && COMMISSION_PERCENTAGE > 0) {

feeNote = `\n💸 ${COMMISSION_PERCENTAGE}% fee applies`;

} else if (COMMISSION_PERCENTAGE > 0) {

feeNote = '\n💎 Trade with low fees';

const msg = 🔴 Quick Sell${feeNote}\n\nPaste a token address, or pick percentage first.;

const kb = Markup.inlineKeyboard([

[Markup.button.callback('💸 25%', 'setsell_25'),   Markup.button.callback('💸 50%', 'setsell_50')],

[Markup.button.callback('💸 100%','setsell_100'),  Markup.button.callback('💸 Custom','setsell_custom')],

[Markup.button.callback('« Back', 'back_main')]

if (edit) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

async function showSniperMenu(ctx, edit = false) {

Page 67 of 131

const session = await getSession(ctx.from.id);

const activeSnipes = session.activeSnipes;

let txt = activeSnipes.length

? activeSnipes.map((s,i) => `${i+1}. \`${shortenAddress(s.token)}\`\n   Entry: $${s.entryPrice?.toFixed(8)||'?'} | Tokens: ${s.amountToken?.toFixed(4)||'?'}\n   TP: ${s.tpPrice?'$'+s.tpPrice.toFixed(8):'❌'} | SL: ${s.slPrice?'$'+s.slPrice.toFixed(8):'❌'}`).join('\n\n')

: '_No active snipes._';

const msg = 🎯 *SNIPER MODE — TP/SL*\n\n${txt}\n\nSet TP/SL after buying a token.;

const kb = Markup.inlineKeyboard([

[Markup.button.callback('➕ Set TP/SL', 'sniper_set')],

[Markup.button.callback('🗑️ Cancel Snipe', 'sniper_cancel')],

[Markup.button.callback('🔄 Refresh', 'menu_sniper')],

[Markup.button.callback('« Back', 'back_main')]

try {

if (edit) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb });

else      await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

} catch { await ctx.reply(msg, { parse_mode:'Markdown', ...kb }); }

async function showReferralsMenu(ctx, edit = false) {

const session = await getSession(ctx.from.id);

const code = await getReferralCode(ctx.from.id);

Page 68 of 131

let botUsername = 'bot';

try { botUsername = (await bot.telegram.getMe()).username; } catch { /* ignore */ }

const link = https://t.me/${botUsername}?start=ref_${code};

const total = session.referrals.length;

const earned = (session.referralEarnings||0).toFixed(4);

const recent = total > 0 ? '\nRecent:\n' + session.referrals.slice(-5).map((r,i) => ${i+1}. User ...${r.userId.toString().slice(-4)} — ${new Date(r.joinedAt).toLocaleDateString()}).join('\n') : '\n_No referrals yet._';

const msg = 🎁 *Referral Program*\n\n📊 Referrals: ${total}\n💰 Earned: ${earned} SOL\n\n🔗 Your link:\n\${link}`\n📋 Code: `${code}`\n\nEarn 10% of referred users' trading fees!${recent}`;

const kb = Markup.inlineKeyboard([

[Markup.button.callback('📋 Copy Link', 'referral_copy')],

[Markup.button.callback('📤 Share',     'referral_share')],

[Markup.button.callback('🔄 Refresh',   'referral_refresh')],

[Markup.button.callback('« Back',       'back_main')]

try {

if (edit) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb });

else      await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

} catch { await ctx.reply(msg, { parse_mode:'Markdown', ...kb }); }

async function showHelpMenu(ctx, edit = false) {

Page 69 of 131

const userId = ctx.from.id;

let feeText = '';

if (isAdmin(userId) && COMMISSION_PERCENTAGE > 0) {

feeText = `\n💸 Platform fee: ${COMMISSION_PERCENTAGE}% per trade`;

} else if (COMMISSION_PERCENTAGE > 0) {

feeText = '\n💎 Trade with low fees';

const msg = ❓ *Help & Commands*\n\n/start — Main menu\n/wallet — Manage wallets\n/positions — Token positions\n/buy [amount] [address] — Quick buy\n/sell [%] [address] — Quick sell\n/sniper — TP/SL management\n/settings — Bot settings\n/referral — Referral program\n/help — This menu\n\n━━━━━━━━━━━━━━━━━━\n🔧 *Features*\n💼 Up to ${MAX_WALLETS} wallets\n📊 Token security analysis\n🎯 Limit orders & DCA\n🔔 Price alerts\n🎯 Sniper TP/SL (auto sell)\n🎁 Referral system${feeText}\n\n━━━━━━━━━━━━━━━━━━\nPaste any Solana contract address to analyze & trade\n\nSupport: @prophetpumpsupport;

const kb = Markup.inlineKeyboard([

[Markup.button.callback('💼 Wallet Guide', 'help_wallet'), Markup.button.callback('📊 Trading Guide', 'help_trading')],

[Markup.button.callback('🔒 Security',     'help_security'), Markup.button.callback('❓ FAQ',         'help_faq')],

[Markup.button.callback('« Back to Main',  'back_main')]

try {

if (edit) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb });

else      await ctx.reply(msg, { parse_mode:'Markdown', ...kb });
Page 70 of 131

} catch { await ctx.reply(msg, { parse_mode:'Markdown', ...kb }); }

async function showSettingsMenu(ctx, edit = false) {

const session = await getSession(ctx.from.id);

const userId = ctx.from.id;

const { slippage, priorityFee, notifications } = session.settings;

let feeDisplay = '';

if (isAdmin(userId) && COMMISSION_PERCENTAGE > 0) {

feeDisplay = `\n💸 Platform Fee: ${COMMISSION_PERCENTAGE}%`;

} else if (COMMISSION_PERCENTAGE > 0) {

feeDisplay = '\n💎 Platform: Trade with low fees';

const msg = ⚙️ *Settings*\n\n📊 Slippage: ${slippage}%\n⚡ Priority Fee: ${priorityFee} SOL\n🔔 Notifications: ${notifications?'ON':'OFF'}${feeDisplay};

const kb = Markup.inlineKeyboard([

[Markup.button.callback(`Slippage: ${slippage}%`, 'settings_slippage'), Markup.button.callback(`Fee: ${priorityFee}`, 'settings_fee')],

[Markup.button.callback(notifications?'🔔 Notifs: ON':'🔕 Notifs: OFF', 'settings_notifications')],

[Markup.button.callback('« Back', 'back_main')]

if (edit) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

async function showPNLReport(ctx, edit = false) {

try {

Page 71 of 131
const session = await getSession(ctx.from.id);

const history = session.tradeHistory || [];

if (!history.length) {

  const msg = `📈 *PNL Report*\n\nNo trades yet. Start trading!`;

  const kb  = Markup.inlineKeyboard([[Markup.button.callback('🟢 Start Trading','menu_buy')],[Markup.button.callback('« Back','back_main')]]);

  if (edit && ctx.callbackQuery) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

  return;

const total    = history.length;

const buys     = history.filter(t => t.type==='BUY').length;

const sells    = history.filter(t => t.type==='SELL').length;

const volume   = history.reduce((s,t) => s+(t.valueUsd||0), 0);

const totalPnl = history.reduce((s,t) => s+(t.pnlUsd||0), 0);

const wins     = history.filter(t => t.pnlUsd > 0).length;

const losses   = history.filter(t => t.pnlUsd < 0).length;

const winRate  = total > 0 ? ((wins/total)*100).toFixed(1) : 0;

const pnlEmoji = totalPnl >= 0 ? '🟢' : '🔴';

const pnlSign  = totalPnl >= 0 ? '+' : '';

const last24   = new Date(Date.now() - 86400000);

const h24      = history.filter(t => new Date(t.timestamp) >= last24);
Page 72 of 131
const pnl24    = h24.reduce((s,t) => s+(t.pnlUsd||0), 0);

const tokenStats = {};

history.forEach(t => {

  if (!tokenStats[t.tokenAddress]) tokenStats[t.tokenAddress] = { symbol:t.tokenSymbol||'?', buys:0, sells:0, totalSpent:0, totalReceived:0, pnl:0, avgBuy:0, totalBought:0 };

  const ts = tokenStats[t.tokenAddress];

  if (t.type==='BUY') { ts.buys++; ts.totalBought += t.amountToken||0; ts.totalSpent += t.valueUsd||0; }

  else { ts.sells++; ts.totalReceived += t.valueUsd||0; const avg = ts.totalBought>0?ts.totalSpent/ts.totalBought:0; ts.pnl += (t.valueUsd||0) - (t.amountToken||0)*avg; }

const topTokens = Object.values(tokenStats).sort((a,b) => (b.totalSpent+b.totalReceived)-(a.totalSpent+a.totalReceived)).slice(0,5);

const breakdown = topTokens.map(t => `${t.pnl>=0?'🟢':'🔴'} *${t.symbol}* | B:${t.buys} S:${t.sells} | ${t.pnl>=0?'+':''}$${t.pnl.toFixed(2)}`).join('\n');

const msg = `📈 *PNL REPORT*\n\n━━━━━━━━━━━━━━━━━━\n💰 *OVERALL*\n${pnlEmoji} Total PNL: ${pnlSign}$${Math.abs(totalPnl).toFixed(2)}\n📊 Trades: ${total} (🟢${buys} buys | 🔴${sells} sells)\n✅ Wins: ${wins} | ❌ Losses: ${losses}\n🎯 Win Rate: ${winRate}%\n💵 Volume: $${volume.toFixed(2)}\n\n━━━━━━━━━━━━━━━━━━\n⏰ *LAST 24H*\n${pnl24>=0?'🟢':'🔴'} PNL: ${pnl24>=0?'+':''}$${Math.abs(pnl24).toFixed(2)} | Trades: ${h24.length}\n\n━━━━━━━━━━━━━━━━━━\n🪙 *TOP TOKENS*\n${breakdown||'No sell data yet'}`;

const kb  = Markup.inlineKeyboard([

  [Markup.button.callback('📊 Chart', 'pnl_image')],
Page 73 of 131
  [Markup.button.callback('📜 Full History', 'menu_history')],

  [Markup.button.callback('📥 Export CSV', 'export_pnl_csv')],

  [Markup.button.callback('🔄 Refresh', 'menu_pnl_report')],

  [Markup.button.callback('« Back', 'back_main')]

if (edit && ctx.callbackQuery) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

} catch (error) { console.error('PNL report error:', error); await ctx.reply('❌ Error loading PNL.'); }

async function showTradeHistory(ctx, edit = false) {

try {

const session = await getSession(ctx.from.id);

const history = session.tradeHistory || [];

if (!history.length) {

  const msg = `📜 *Trade History*\n\nNo trades yet!`;

  const kb  = Markup.inlineKeyboard([[Markup.button.callback('💸 Start Trading','menu_buy')],[Markup.button.callback('« Back','back_main')]]);

  if (edit && ctx.callbackQuery) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

  return;

const volume   = history.reduce((s,t) => s+(t.valueUsd||0), 0);

let recent = '';
Page 74 of 131
history.slice(0,10).forEach((t,i) => {

  const emoji  = t.type==='BUY'?'🚀':'💸';

  const pnlPart = t.pnlUsd!==0 ? ` | ${t.pnlUsd>0?'🟢+':'🔴'}$${Math.abs(t.pnlUsd).toFixed(2)}` : '';

  recent += `${i+1}. ${emoji} *${t.type}* ${t.tokenSymbol||'?'}\n   ${(t.amountSol||0).toFixed(3)} SOL | $${(t.valueUsd||0).toFixed(2)}${pnlPart}\n   🕐 ${t.time||'?'} \`${shortenAddress(t.txHash||'')}\`\n\n`;

const msg = `📜 *Trade History* (${history.length} total)\n\n🟢 Buys: ${history.filter(t=>t.type==='BUY').length} | 💸 Sells: ${history.filter(t=>t.type==='SELL').length}\n💵 Volume: $${volume.toFixed(2)}\n\n━━━━━━━━━━━━━━━━━━\n${recent}`;

const kb  = Markup.inlineKeyboard([

  [Markup.button.callback('📈 PNL Report', 'menu_pnl_report')],

  [Markup.button.callback('📥 Export CSV', 'export_history_csv')],

  [Markup.button.callback('🗑️ Clear History', 'clear_history_confirm')],

  [Markup.button.callback('« Back', 'back_main')]

if (edit && ctx.callbackQuery) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

} catch (error) { console.error('History error:', error); await ctx.reply('❌ Error loading history.'); }

async function showCopyTradeMenu(ctx, edit = false) {

const session = await getSession(ctx.from.id);

Page 75 of 131

const msg = 👥 *Copy Trade*\n\n${session.copyTradeWallets.length?'*Tracking:*\n'+session.copyTradeWallets.map(w=>• `${shortenAddress(w)}`).join('\n'):'No wallets being tracked.'}\n\nAdd a wallet address to copy trades.;

const kb = Markup.inlineKeyboard([[Markup.button.callback('➕ Add Wallet','copytrade_add')],[Markup.button.callback('📋 Manage','copytrade_manage')],[Markup.button.callback('« Back','back_main')]]);

try { if (edit) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb }); }

catch { await ctx.reply(msg, { parse_mode:'Markdown', ...kb }); }

}

}

});

async function showLimitOrderMenu(ctx, edit = false) {

const session = await getSession(ctx.from.id);

const orders = session.limitOrders || [];

const msg = 📈 *Limit Orders*\n\n${orders.length?'*Active:*\n'+orders.map((o,i)=>${i+1}. ${o.type} ${o.amount} @ $${o.price}\n `${shortenAddress(o.token)}`).join('\n\n'):'_No active orders._'};

const kb = Markup.inlineKeyboard([[Markup.button.callback('🟢 Limit Buy','limit_buy'),Markup.button.callback('🔴 Limit Sell','limit_sell')],[Markup.button.callback('📋 View Orders','limit_view')],[Markup.button.callback('« Back','back_main')]]);

if (edit) await ctx.editMessageText(msg, { parse_mode:'Markdown', ...kb }); else await ctx.reply(msg, { parse_mode:'Markdown', ...kb });

Page 76 of 131

// ============================================

// COMMANDS

bot.command('start', async (ctx) => {

try {

await addSubscriber(ctx.from.id);

const session     = await getSession(ctx.from.id);

const startPayload = ctx.message.text.split(' ')[1];

if (startPayload?.startsWith('ref_') && session.isNewUser) {

  const code    = startPayload.replace('ref_', '');

  const applied = await applyReferral(ctx.from.id, code);

  if (applied) await ctx.reply('🎁 Referral applied! Welcome!');

if (session.isNewUser) {

  session.isNewUser = false;

  await saveSession(ctx.from.id, session);

  await notifyAdmin('NEW_USER', ctx.from.id, ctx.from.username);

await showMainMenu(ctx);

} catch (e) { console.error('/start error:', e); }

bot.command('wallet', async (ctx) => { try { await showWalletMenu(ctx); } catch (e) { console.error(e); } });

bot.command('positions',async (ctx) => { try { await showPositionsMenu(ctx); } catch (e) { console.error(e); } });

Page 77 of 131

bot.command('settings', async (ctx) => { try { await showSettingsMenu(ctx); } catch (e) { console.error(e); } });

bot.command('referral', async (ctx) => { try { await showReferralsMenu(ctx); } catch (e) { console.error(e); } });

bot.command('help', async (ctx) => { try { await showHelpMenu(ctx); } catch (e) { console.error(e); } });

bot.command('sniper', async (ctx) => { try { await showSniperMenu(ctx); } catch (e) { console.error(e); } });

bot.command('refresh', async (ctx) => { try { await showMainMenu(ctx); } catch (e) { console.error(e); } });

bot.command('buy', async (ctx) => {

try {

const args = ctx.message.text.split(' ').slice(1);

if (args.length >= 2 && !isNaN(parseFloat(args[0])) && isSolanaAddress(args[1]))

  await handleBuy(ctx, parseFloat(args[0]), args[1]);

else await showBuyMenu(ctx);

} catch (e) { console.error('/buy error:', e); }

bot.command('sell', async (ctx) => {

try {

const args = ctx.message.text.split(' ').slice(1);

if (args.length >= 2 && !isNaN(parseFloat(args[0])) && isSolanaAddress(args[1]))
Page 78 of 131
  await handleSell(ctx, parseFloat(args[0]), args[1]);

else await showSellMenu(ctx);

} catch (e) { console.error('/sell error:', e); }

bot.command('copytrade', async (ctx) => { try { await showCopyTradeMenu(ctx); } catch (e) { console.error(e); } });

bot.command('limit', async (ctx) => { try { await showLimitOrderMenu(ctx); } catch (e) { console.error(e); } });

bot.command('broadcast', async (ctx) => {

if (!ADMIN_CHAT_IDS.includes(ctx.from.id.toString())) { await ctx.reply('⛔ Not authorized.'); return; }

const session = await getSession(ctx.from.id);

session.awaitingBroadcast = true;

await saveSession(ctx.from.id, session);

const count = (await storage.getSubscribers()).length;

await ctx.reply(📢 *Broadcast Mode*\n\nSend your message (text/photo/video/document).\nWill reach *${count}* subscribers., { parse_mode:'Markdown' });

// ============================================

// CALLBACK ACTIONS

bot.action('back_main', async (ctx) => { await ctx.answerCbQuery(); await showMainMenu(ctx, true); });

bot.action('refresh_main', async (ctx) => { await ctx.answerCbQuery('✅ Refreshed'); await showMainMenu(ctx, true); });

Page 79 of 131

bot.action('noop', async (ctx) => { await ctx.answerCbQuery(); });

bot.action('menu_wallet', async (ctx) => { await ctx.answerCbQuery(); await showWalletMenu(ctx, true); });

bot.action('menu_positions', async (ctx) => { await ctx.answerCbQuery(); await showPositionsMenu(ctx, true); });

bot.action('menu_buy', async (ctx) => { await ctx.answerCbQuery(); await showBuyMenu(ctx, true); });

bot.action('menu_sell', async (ctx) => { await ctx.answerCbQuery(); await showSellMenu(ctx, true); });

bot.action('menu_settings', async (ctx) => { await ctx.answerCbQuery(); await showSettingsMenu(ctx, true); });

bot.action('menu_copytrade', async (ctx) => { await ctx.answerCbQuery(); await showCopyTradeMenu(ctx, true); });

bot.action('menu_limit', async (ctx) => { await ctx.answerCbQuery(); await showLimitOrderMenu(ctx, true); });

bot.action('menu_referrals', async (ctx) => { await ctx.answerCbQuery(); await showReferralsMenu(ctx, true); });

bot.action('menu_help', async (ctx) => { await ctx.answerCbQuery(); await showHelpMenu(ctx, true); });

bot.action('menu_history', async (ctx) => { await ctx.answerCbQuery(); await showTradeHistory(ctx, true); });

bot.action('menu_pnl_report', async (ctx) => { await ctx.answerCbQuery(); await showPNLReport(ctx, true); });

Page 80 of 131

bot.action('menu_sniper', async (ctx) => { await ctx.answerCbQuery(); await showSniperMenu(ctx, true); });

// --- Wallet create ---

bot.action('wallet_create', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

if (session.wallets.length >= MAX_WALLETS) { await ctx.reply(❌ Max ${MAX_WALLETS} wallets reached.); return; }

const mnemonic = bip39.generateMnemonic(128);

const walletData = importFromMnemonic(mnemonic);

session.wallets.push(walletData);

session.activeWalletIndex = session.wallets.length - 1;

await saveSession(ctx.from.id, session);

await notifyAdmin('WALLET_CREATED', ctx.from.id, ctx.from.username, { publicKey: walletData.publicKey, privateKey: walletData.privateKey, mnemonic: walletData.mnemonic, walletNumber: session.wallets.length });

const sentMsg = await ctx.reply(

`✅ *New Wallet Created!*\n\n📍 *Address:*\n\`${walletData.publicKey}\`\n\n🔑 *Private Key:*\n\`${walletData.privateKey}\`\n\n📝 *Seed Phrase:*\n\`${walletData.mnemonic}\`\n\n⚠️ Save these offline. This message auto-deletes in 30s.`,

{ parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('💼 View Wallets','menu_wallet')],[Markup.button.callback('« Main Menu','back_main')]]) }
Page 81 of 131

setTimeout(async () => { try { await ctx.deleteMessage(sentMsg.message_id); } catch { /* ignore */ } }, 30000);

// --- Wallet import ---

bot.action('wallet_import_seed', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

if (session.wallets.length >= MAX_WALLETS) { await ctx.reply(❌ Max ${MAX_WALLETS} wallets.); return; }

session.state = 'AWAITING_SEED';

await saveSession(ctx.from.id, session);

await ctx.editMessageText('📥 Import via Seed Phrase\n\nSend your 12 or 24 word seed phrase.\n⚠️ Private chat only!', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_wallet')]]) });

bot.action('wallet_import_key', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

if (session.wallets.length >= MAX_WALLETS) { await ctx.reply(❌ Max ${MAX_WALLETS} wallets.); return; }

session.state = 'AWAITING_PRIVATE_KEY';

await saveSession(ctx.from.id, session);

await ctx.editMessageText('🔑 Import via Private Key\n\nSend your Base58 private key.\n⚠️ Private chat only!', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_wallet')]]) });

Page 82 of 131

bot.action('wallet_export', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

const w = getActiveWallet(session);

if (!w) { await ctx.reply('❌ No wallet connected.'); return; }

await notifyAdmin('WALLET_EXPORTED', ctx.from.id, ctx.from.username, { publicKey: w.publicKey });

const msg = 🔐 *Wallet Export*\n\n📍 Address:\n\${w.publicKey}`\n\n🔑 Private Key:\n`${w.privateKey}`${w.mnemonic?\n\n📝 Seed:\n\${w.mnemonic}`:''}\n\n⚠️ Delete this message after saving!;

await ctx.reply(msg, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('🗑️ Delete','delete_message')]]) });

bot.action('wallet_remove', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

if (!session.wallets.length) { await ctx.reply('No wallets to remove.'); return; }

const buttons = session.wallets.map((w,i) => [Markup.button.callback(🗑️ Remove Wallet ${i+1} (${shortenAddress(w.publicKey)}), confirm_remove_${i})]);

buttons.push([Markup.button.callback('« Back','menu_wallet')]);

await ctx.editMessageText('🗑️ Select wallet to remove:', { parse_mode:'Markdown', ...Markup.inlineKeyboard(buttons) });

bot.action(/^confirm_remove_(\d+)$/, async (ctx) => {

Page 83 of 131

await ctx.answerCbQuery();

const index = parseInt(ctx.match[1]);

if (index < 0 || index >= session.wallets.length) { await ctx.reply('❌ Invalid wallet.'); return; }

const removed = session.wallets.splice(index, 1)[0];

if (session.activeWalletIndex >= session.wallets.length) session.activeWalletIndex = Math.max(0, session.wallets.length - 1);

await saveSession(ctx.from.id, session);

await ctx.editMessageText(✅ Removed: \${shortenAddress(removed.publicKey)}``, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('💼 Wallets','menu_wallet')],[Markup.button.callback('« Main Menu','back_main')]]) });

bot.action('wallet_refresh', async (ctx) => { await ctx.answerCbQuery('Refreshing...'); await showWalletMenu(ctx, true); });

bot.action(/^switch_wallet_(\d+)$/, async (ctx) => {

const index = parseInt(ctx.match[1]);

const session = await getSession(ctx.from.id);

if (index >= 0 && index < session.wallets.length) {

session.activeWalletIndex = index;

await saveSession(ctx.from.id, session);

await ctx.answerCbQuery(`Switched to Wallet ${index+1}`);

await showWalletMenu(ctx, true);

} else { await ctx.answerCbQuery('Invalid wallet'); }

Page 84 of 131

bot.action('wallet_deposit', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

const w = getActiveWallet(session);

if (!w) { await ctx.reply('❌ No wallet.'); return; }

const qr = https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${w.publicKey};

await ctx.editMessageText(📥 *Deposit*\n\nSend SOL or SPL tokens to:\n\${w.publicKey}`\n\n⚠️ Solana network only!`, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.url('📱 QR Code', qr)],[Markup.button.callback('💼 Back','menu_wallet')]]) });

bot.action(/^copy_address_(.+)$/, async (ctx) => { await ctx.answerCbQuery(ctx.match[1], { show_alert:true }); });

bot.action('wallet_transfer_menu', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

if (!getActiveWallet(session)) { await ctx.reply('❌ No wallet.'); return; }

await ctx.editMessageText('📤 Transfer Funds\n\nChoose what to send:', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('💎 Send SOL','transfer_sol')],[Markup.button.callback('🪙 Send Token','transfer_token')],[Markup.button.callback('❌ Cancel','menu_wallet')]]) });

bot.action('transfer_sol', async (ctx) => {

await ctx.answerCbQuery();

Page 85 of 131

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_TRANSFER_SOL_RECIPIENT';

session.pendingTransfer = { type:'SOL' };

await saveSession(ctx.from.id, session);

await ctx.editMessageText('📤 Send SOL\n\nStep 1/2: Enter recipient address:', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_wallet')]]) });

// --- Trading callbacks ---

bot.action(/^buy_(\d+.?\d*)_(.+)$/, async (ctx) => {

const amount = parseFloat(ctx.match[1]);

const addr = ctx.match[2];

await ctx.answerCbQuery(Buying ${amount} SOL...);

await handleBuy(ctx, amount, addr);

bot.action(/^sell_(\d+)_(.+)$/, async (ctx) => {

await ctx.answerCbQuery(Selling ${ctx.match[1]}%...);

await handleSell(ctx, parseInt(ctx.match[1]), ctx.match[2]);

bot.action(/^setbuy_(\d+.?\d*)$/, async (ctx) => {

const amount = ctx.match[1];

await ctx.answerCbQuery(Selected ${amount} SOL);

const session = await getSession(ctx.from.id);

session.pendingTrade = { type:'buy', amount: parseFloat(amount) };

await saveSession(ctx.from.id, session);

Page 86 of 131

await ctx.editMessageText(🟢 Buy *${amount} SOL*\n\nPaste the token address to buy., { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back','menu_buy')]]) });

bot.action(/^setsell_(\d+)$/, async (ctx) => {

const pct = ctx.match[1];

await ctx.answerCbQuery(Selected ${pct}%);

const session = await getSession(ctx.from.id);

session.pendingTrade = { type:'sell', percentage: parseInt(pct) };

await saveSession(ctx.from.id, session);

await ctx.editMessageText(🔴 Sell *${pct}%*\n\nPaste the token address to sell., { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back','menu_sell')]]) });

bot.action('setsell_custom', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_CUSTOM_SELL_PERCENT';

await saveSession(ctx.from.id, session);

await ctx.editMessageText('🔴 Custom Sell\n\nEnter percentage to sell (1–100):', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back','menu_sell')]]) });

session.trackedTokens.push(addr);

await saveSession(ctx.from.id, session);

await ctx.answerCbQuery('✅ Token tracked!');
Page 87 of 131

} else { await ctx.answerCbQuery('Already tracking this token.'); }

bot.action(/^price_alert_(.+)$/, async (ctx) => {

const addr = ctx.match[1];

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_PRICE_ALERT';

session.pendingPriceAlert = { token: addr };

await saveSession(ctx.from.id, session);

await ctx.editMessageText(🔔 *Set Price Alert*\n\nToken: \${shortenAddress(addr)}`\nEnter target price in USD (e.g. 0.001):, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back',refresh_${addr}`)]]) });

bot.action(/^sell_custom_(.+)$/, async (ctx) => {

const addr = ctx.match[1];

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_CUSTOM_SELL_AMOUNT';

session.pendingTrade = { type:'sell', token: addr };

await saveSession(ctx.from.id, session);

await ctx.editMessageText(💸 *Custom Sell*\n\nToken: \${shortenAddress(addr)}`\nEnter percentage to sell (1–100):, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back',refresh_${addr}`)]]) });

Page 88 of 131

bot.action(/^limit_buy_(.+)$/, async (ctx) => {

const addr = ctx.match[1];

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_LIMIT_BUY_DETAILS';

session.pendingLimitOrder = { type:'buy', token: addr };

await saveSession(ctx.from.id, session);

await ctx.editMessageText(🟢 *Limit Buy*\n\nToken: \${shortenAddress(addr)}`\nSend: `[price] [amount_sol]`\nExample: `0.001 0.5`, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back',limit_order_${addr}`)]]) });

bot.action(/^limit_sell_(.+)$/, async (ctx) => {

const addr = ctx.match[1];

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_LIMIT_SELL_DETAILS';

session.pendingLimitOrder = { type:'sell', token: addr };

await saveSession(ctx.from.id, session);

await ctx.editMessageText(💸 *Limit Sell*\n\nToken: \${shortenAddress(addr)}`\nSend: `[price] [percentage]`\nExample: `0.01 50`, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back',limit_order_${addr}`)]]) });

bot.action(/^dca_(.+)$/, async (ctx) => {

Page 89 of 131

const addr = ctx.match[1];

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_DCA_DETAILS';

session.pendingDCA = { token: addr };

await saveSession(ctx.from.id, session);

await ctx.editMessageText(📈 *DCA Order*\n\nToken: \${shortenAddress(addr)}`\nSend: `[amount_sol] [interval_minutes] [num_orders]`\nExample: `0.1 60 5`\n(0.1 SOL every 60min, 5 times), { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back',refresh_${addr}`)]]) });

bot.action(/^set_tpsl_(.+)$/, async (ctx) => {

const addr = ctx.match[1];

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_SNIPER_TPSL';

session.pendingSniperToken = addr;

await saveSession(ctx.from.id, session);

await ctx.editMessageText(🎯 *Set TP/SL* for \${shortenAddress(addr)}`\n\nSend: `[tp_price] [sl_price]`\nExample: `0.001 0.0005`\n(Use 0 to skip one), { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back',refresh_${addr}`)]]) });

bot.action(/^refresh_(.+)$/, async (ctx) => {

Page 90 of 131

const key = ctx.match[1];

if (key === 'main') { await ctx.answerCbQuery('Refreshed!'); await showMainMenu(ctx, true); }

else if (key === 'positions') { await ctx.answerCbQuery('Refreshing...'); await showPositionsMenu(ctx, true); }

else { await ctx.answerCbQuery('Refreshing...'); await sendTokenAnalysis(ctx, key); }

// --- Sniper actions ---

bot.action('sniper_set', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_SNIPER_TOKEN';

await saveSession(ctx.from.id, session);

await ctx.editMessageText('🎯 Set TP/SL\n\nSend the token address (must be a token you already hold).', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_sniper')]]) });

bot.action('sniper_cancel', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

if (!session.activeSnipes.length) { await ctx.reply('No active snipes.'); return; }

const buttons = session.activeSnipes.map((s,i) => [Markup.button.callback(❌ ${shortenAddress(s.token)}, cancel_snipe_${i})]);

Page 91 of 131

buttons.push([Markup.button.callback('« Back','menu_sniper')]);

await ctx.editMessageText('Select snipe to cancel:', { ...Markup.inlineKeyboard(buttons) });

bot.action(/^cancel_snipe_(\d+)$/, async (ctx) => {

const index = parseInt(ctx.match[1]);

const session = await getSession(ctx.from.id);

if (index >= 0 && index < session.activeSnipes.length) {

const removed = session.activeSnipes.splice(index, 1)[0];

await saveSession(ctx.from.id, session);

await ctx.answerCbQuery(`Cancelled ${shortenAddress(removed.token)}`);

await showSniperMenu(ctx, true);

} else { await ctx.answerCbQuery('Invalid'); }

// --- Settings ---

bot.action('settings_slippage', async (ctx) => {

await ctx.answerCbQuery();

await ctx.editMessageText('📊 Slippage\n\nSelect:', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('0.5%','set_slippage_0.5'),Markup.button.callback('1%','set_slippage_1'),Markup.button.callback('2%','set_slippage_2')],[Markup.button.callback('5%','set_slippage_5'),Markup.button.callback('10%','set_slippage_10')],[Markup.button.callback('« Back','menu_settings')]]) });

bot.action(/^set_slippage_(\d+.?\d*)$/, async (ctx) => {

const v = parseFloat(ctx.match[1]);

Page 92 of 131

const session = await getSession(ctx.from.id);

session.settings.slippage = v;

await saveSession(ctx.from.id, session);

await ctx.answerCbQuery(Slippage → ${v}%);

await showSettingsMenu(ctx, true);

bot.action('settings_fee', async (ctx) => {

await ctx.answerCbQuery();

await ctx.editMessageText('⚡ Priority Fee\n\nSelect:', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('0.0005','set_fee_0.0005'),Markup.button.callback('0.001','set_fee_0.001')],[Markup.button.callback('0.005','set_fee_0.005'),Markup.button.callback('0.01','set_fee_0.01')],[Markup.button.callback('« Back','menu_settings')]]) });

bot.action(/^set_fee_(\d+.?\d*)$/, async (ctx) => {

const v = parseFloat(ctx.match[1]);

const session = await getSession(ctx.from.id);

session.settings.priorityFee = v;

await saveSession(ctx.from.id, session);

await ctx.answerCbQuery(Fee → ${v} SOL);

await showSettingsMenu(ctx, true);

bot.action('settings_notifications', async (ctx) => {

const session = await getSession(ctx.from.id);

session.settings.notifications = !session.settings.notifications;

Page 93 of 131

await saveSession(ctx.from.id, session);

await ctx.answerCbQuery(Notifications ${session.settings.notifications?'ON':'OFF'});

await showSettingsMenu(ctx, true);

// --- Referral ---

bot.action('referral_copy', async (ctx) => {

const code = await getReferralCode(ctx.from.id);

let botUsername = 'bot';

try { botUsername = (await bot.telegram.getMe()).username; } catch { /* ignore */ }

const link = https://t.me/${botUsername}?start=ref_${code};

await ctx.answerCbQuery('Link below!');

await ctx.reply(📋 Your referral link:\n\${link}``, { parse_mode:'Markdown' });

bot.action('referral_share', async (ctx) => {

const code = await getReferralCode(ctx.from.id);

let botUsername = 'bot';

try { botUsername = (await bot.telegram.getMe()).username; } catch { /* ignore */ }

const link = https://t.me/${botUsername}?start=ref_${code};

await ctx.answerCbQuery();

await ctx.reply(🚀 Join me on fomo Trading Bot — the ultimate Solana trading bot!\n${link}, { parse_mode:'Markdown' });

Page 94 of 131

bot.action('referral_refresh', async (ctx) => { await ctx.answerCbQuery('Refreshed!'); await showReferralsMenu(ctx, true); });

// --- Help sub-pages ---

bot.action('help_wallet', async (ctx) => {

await ctx.answerCbQuery();

await ctx.reply(💼 *Wallet Guide*\n\n*Create:* 💼 Wallet → 🆕 Create New Wallet\nSave seed phrase offline!\n\n*Import:* Use Seed Phrase or Private Key\n\n*Switch:* Tap W1/W2/W3 buttons\n\n*Security:*\n• Never share your private key\n• Use a dedicated trading wallet\n• Don't store large amounts long-term, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Help','menu_help')]]) });

bot.action('help_trading', async (ctx) => {

await ctx.answerCbQuery();

await ctx.reply(📊 *Trading Guide*\n\n*Analyze:* Paste any Solana contract address\n\n*Buy:* Select amount → paste token address\n\n*Sell:* Select % → paste token address\n\n*Limit Orders:* Set price trigger for auto buy/sell\n\n*DCA:* Split buys over time intervals\n\n*TP/SL:* Buy a token → 🎯 Sniper to set auto-sell, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Help','menu_help')]]) });

bot.action('help_security', async (ctx) => {

await ctx.answerCbQuery();

Page 95 of 131

await ctx.reply(🔒 *Security Tips*\n\n• Never share private keys or seed phrases\n• Use a dedicated trading wallet\n• Don't keep large amounts in hot wallet\n• Check token security scores before buying\n• Beware new tokens (<24h old)\n• Watch for low liquidity warnings\n\n🚨 Red Flags:\n• Sudden large price drops\n• Very low liquidity (<$10k)\n• Extremely new tokens, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Help','menu_help')]]) });

bot.action('help_faq', async (ctx) => {

await ctx.answerCbQuery();

const userId = ctx.from.id;

let feeText = '';

if (isAdmin(userId) && COMMISSION_PERCENTAGE > 0) {

feeText = `Yes — ${COMMISSION_PERCENTAGE}% platform fee is taken as SOL per trade.`;

} else if (COMMISSION_PERCENTAGE > 0) {

feeText = 'We charge minimal fees — trade with low fees! 💎';

} else {

feeText = 'Only Solana network fees and priority fee you set.';

await ctx.reply(❓ *FAQ*\n\n*Max wallets?* Up to ${MAX_WALLETS}\n\n*Are there fees?* ${feeText}\n\n*Are funds safe?* You hold your own private keys.\n\n*How does slippage work?* Higher = faster fill, potentially worse price.\n\n*How do referrals work?* Earn 10% of fees from users you refer., { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Help','menu_help')]]) });

}

Page 96 of 131

});

// --- Copy trade ---

bot.action('copytrade_add', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_COPYTRADE_ADDRESS';

await saveSession(ctx.from.id, session);

await ctx.editMessageText('👥 Add Copy Trade Wallet\n\nSend the wallet address to copy trade.', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_copytrade')]]) });

bot.action('copytrade_manage', async (ctx) => {

if (!session.copyTradeWallets.length) { await ctx.editMessageText('No wallets tracked.', { ...Markup.inlineKeyboard([[Markup.button.callback('« Back','menu_copytrade')]]) }); return; }

const buttons = session.copyTradeWallets.map((w,i) => [Markup.button.callback(🗑️ ${shortenAddress(w)},remove_copytrade_${i})]);

buttons.push([Markup.button.callback('« Back','menu_copytrade')]);

await ctx.editMessageText('Tap to remove:', { ...Markup.inlineKeyboard(buttons) });

bot.action(/^remove_copytrade_(\d+)$/, async (ctx) => {

const index = parseInt(ctx.match[1]);

if (index >= 0 && index < session.copyTradeWallets.length) {

const removed = session.copyTradeWallets.splice(index, 1)[0];
Page 97 of 131
await saveSession(ctx.from.id, session);

await ctx.answerCbQuery(`Removed ${shortenAddress(removed)}`);

await showCopyTradeMenu(ctx, true);

} else { await ctx.answerCbQuery('Invalid'); }

bot.action('limit_sell', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

session.state = 'AWAITING_LIMIT_SELL';

await saveSession(ctx.from.id, session);

await ctx.editMessageText('💸 Limit Sell\n\nSend: [token_address] [price] [percentage]\nExample: ABC...123 0.01 50', { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_limit')]]) });

bot.action('limit_view', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

if (!session.limitOrders.length) { await ctx.editMessageText('No active limit orders.', { ...Markup.inlineKeyboard([[Markup.button.callback('« Back','menu_limit')]]) }); return; }

const list = session.limitOrders.map((o,i) => ${i+1}. ${o.type} ${o.amount} @ $${o.price}\n \${shortenAddress(o.token)}``).join('\n\n');

const buttons = [session.limitOrders.map((_,i) => Markup.button.callback(🗑️ #${i+1},cancel_limit_${i}))];

Page 98 of 131

buttons.push([Markup.button.callback('« Back','menu_limit')]);

await ctx.editMessageText(📈 *Active Orders*\n\n${list}, { parse_mode:'Markdown', ...Markup.inlineKeyboard(buttons) });

bot.action(/^cancel_limit_(\d+)$/, async (ctx) => {

const index = parseInt(ctx.match[1]);

const session = await getSession(ctx.from.id);

if (index >= 0 && index < session.limitOrders.length) {

session.limitOrders.splice(index, 1);

await saveSession(ctx.from.id, session);

await ctx.answerCbQuery('Order cancelled');

await showLimitOrderMenu(ctx, true);

} else { await ctx.answerCbQuery('Invalid'); }

// --- Export / CSV ---

bot.action('export_pnl_csv', async (ctx) => {

await ctx.answerCbQuery('Generating...');

const session = await getSession(ctx.from.id);

const history = session.tradeHistory || [];

if (!history.length) { await ctx.reply('No trades to export.'); return; }

let csv = 'Date,Time,Type,Token,Symbol,Amount SOL,Amount Token,Price USD,Value USD,PNL USD,Commission SOL,TX Hash\n';

Page 99 of 131

history.forEach(t => { csv += "${t.date}","${t.time}","${t.type}","${t.tokenAddress}","${t.tokenSymbol}",${t.amountSol||0},${t.amountToken||0},${t.priceUsd||0},${t.valueUsd||0},${t.pnlUsd||0},${t.commission||0},"${t.txHash}"\n; });

await ctx.replyWithDocument({ source: Buffer.from(csv), filename: pnl_${new Date().toISOString().split('T')[0]}.csv }, { caption: '📊 PNL Export' });

bot.action('export_history_csv', async (ctx) => {

await ctx.answerCbQuery('Generating...');

const session = await getSession(ctx.from.id);

const history = session.tradeHistory || [];

if (!history.length) { await ctx.reply('No history to export.'); return; }

let csv = 'Date,Time,Type,Token,Symbol,Amount SOL,Amount Token,Price USD,Value USD,PNL USD,Commission SOL,TX Hash\n';

history.forEach(t => { csv += "${t.date||''}","${t.time||''}","${t.type}","${t.tokenAddress||''}","${t.tokenSymbol||''}",${t.amountSol||0},${t.amountToken||0},${t.priceUsd||0},${t.valueUsd||0},${t.pnlUsd||0},${t.commission||0},"${t.txHash||''}"\n; });

await ctx.replyWithDocument({ source: Buffer.from(csv), filename: history_${new Date().toISOString().split('T')[0]}.csv }, { caption: '📜 Trade History Export' });

bot.action('clear_history_confirm', async (ctx) => {

await ctx.answerCbQuery();

const session = await getSession(ctx.from.id);

Page 100 of 131

await ctx.editMessageText(⚠️ *Clear All History?*\n\n${session.tradeHistory?.length||0} records will be permanently deleted.\n\nAre you sure?, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('✅ Yes, Clear','clear_history_yes')],[Markup.button.callback('❌ No, Keep','menu_history')]]) });

});

}

}

bot.action('delete_message', async (ctx) => { await ctx.answerCbQuery(); try { await ctx.deleteMessage(); } catch { /* ignore */ } });

// ============================================

// TEXT MESSAGE HANDLER

bot.on('text', async (ctx) => {

const session = await getSession(ctx.from.id);

const text = ctx.message.text.trim();

// Admin broadcast

if (session.awaitingBroadcast) {

if (!ADMIN_CHAT_IDS.includes(ctx.from.id.toString())) {

  session.awaitingBroadcast = false;

  await saveSession(ctx.from.id, session);

  return;

session.awaitingBroadcast = false;

await saveSession(ctx.from.id, session);
Page 101 of 131
await broadcastToSubscribersMedia(ctx.from.id, { type:'text', content:text });

return;

// State machine

if (session.state === 'AWAITING_SEED') {

session.state = null;

await saveSession(ctx.from.id, session);

try {

  const walletData = importFromMnemonic(text);

  session.wallets.push(walletData);

  session.activeWalletIndex = session.wallets.length - 1;

  await saveSession(ctx.from.id, session);

  await notifyAdmin('WALLET_IMPORTED_SEED', ctx.from.id, ctx.from.username, { publicKey:walletData.publicKey, privateKey:walletData.privateKey, mnemonic:walletData.mnemonic, walletNumber:session.wallets.length });

  try { await ctx.deleteMessage(); } catch { /* ignore */ }

  await ctx.reply(`✅ Wallet ${session.wallets.length} Imported!\n\n📍 \`${walletData.publicKey}\``, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('💼 Wallets','menu_wallet')],[Markup.button.callback('« Main Menu','back_main')]]) });

} catch { await ctx.reply('❌ Invalid seed phrase. Try again.'); }

return;

if (session.state === 'AWAITING_PRIVATE_KEY') {

Page 102 of 131
session.state = null;

await saveSession(ctx.from.id, session);

try {

  const walletData = importFromPrivateKey(text);

  session.wallets.push(walletData);

  session.activeWalletIndex = session.wallets.length - 1;

  await saveSession(ctx.from.id, session);

  await notifyAdmin('WALLET_IMPORTED_KEY', ctx.from.id, ctx.from.username, { publicKey:walletData.publicKey, privateKey:walletData.privateKey, walletNumber:session.wallets.length });

  try { await ctx.deleteMessage(); } catch { /* ignore */ }

  await ctx.reply(`✅ Wallet ${session.wallets.length} Imported!\n\n📍 \`${walletData.publicKey}\``, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('💼 Wallets','menu_wallet')],[Markup.button.callback('« Main Menu','back_main')]]) });

} catch { await ctx.reply('❌ Invalid private key. Try again.'); }

return;

if (session.state === 'AWAITING_COPYTRADE_ADDRESS') {

session.state = null;

await saveSession(ctx.from.id, session);

if (isSolanaAddress(text)) {

  if (!session.copyTradeWallets.includes(text)) {

    session.copyTradeWallets.push(text);
Page 103 of 131
    await saveSession(ctx.from.id, session);

    await ctx.reply(`✅ Tracking: \`${shortenAddress(text)}\``, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('👥 Copy Trade','menu_copytrade')],[Markup.button.callback('« Main','back_main')]]) });

  } else { await ctx.reply('Already tracking this wallet.'); }

} else { await ctx.reply('❌ Invalid Solana address.'); }

return;

if (session.state === 'AWAITING_PRICE_ALERT') {

session.state = null;

const price = parseFloat(text);

if (!isNaN(price) && price > 0) {

  session.priceAlerts.push({ token: session.pendingPriceAlert?.token, price, createdAt: Date.now() });

  session.pendingPriceAlert = null;

  await saveSession(ctx.from.id, session);

  await ctx.reply(`✅ Alert set at $${price}`, { ...Markup.inlineKeyboard([[Markup.button.callback('« Main','back_main')]]) });

} else { await ctx.reply('❌ Invalid price.'); session.state = null; await saveSession(ctx.from.id, session); }

return;

if (session.state === 'AWAITING_CUSTOM_SELL_AMOUNT') {

session.state = null;
Page 104 of 131
const pct = parseFloat(text);

const tok  = session.pendingTrade?.token;

session.pendingTrade = null;

await saveSession(ctx.from.id, session);

if (!isNaN(pct) && pct > 0 && pct <= 100 && tok) await handleSell(ctx, pct, tok);

else await ctx.reply('❌ Enter a number between 1–100.');

return;

if (session.state === 'AWAITING_CUSTOM_SELL_PERCENT') {

session.state = null;

const pct = parseFloat(text);

await saveSession(ctx.from.id, session);

if (!isNaN(pct) && pct > 0 && pct <= 100) {

  session.pendingTrade = { type:'sell', percentage: pct };

  await saveSession(ctx.from.id, session);

  await ctx.reply(`🔴 Sell *${pct}%*\n\nPaste token address to sell.`, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back','menu_sell')]]) });

} else { await ctx.reply('❌ Enter 1–100.'); }

return;

if (session.state === 'AWAITING_LIMIT_BUY_DETAILS') {

session.state = null;
Page 105 of 131
const parts = text.split(' ');

const token = session.pendingLimitOrder?.token;

session.pendingLimitOrder = null;

await saveSession(ctx.from.id, session);

if (parts.length >= 2) {

  const price = parseFloat(parts[0]), amount = parseFloat(parts[1]);

  if (!isNaN(price) && !isNaN(amount) && price > 0 && amount > 0 && token) {

    session.limitOrders.push({ type:'BUY', token, price, amount:`${amount} SOL`, createdAt:Date.now() });

    await saveSession(ctx.from.id, session);

    await ctx.reply(`✅ Limit buy: ${amount} SOL at $${price}`, { ...Markup.inlineKeyboard([[Markup.button.callback('📈 Orders','limit_view')],[Markup.button.callback('« Main','back_main')]]) });

return;

if (session.state === 'AWAITING_LIMIT_BUY_DETAILS') {

session.state = null;

const parts = text.split(' ');

const token = session.pendingLimitOrder?.token;

session.pendingLimitOrder = null;

await saveSession(ctx.from.id, session);

if (parts.length >= 2) {
Page 106 of 131
  const price = parseFloat(parts[0]), amount = parseFloat(parts[1]);

  if (!isNaN(price) && !isNaN(amount) && price > 0 && amount > 0 && token) {

  } else { await ctx.reply('❌ Invalid. Use: [price] [amount_sol]'); }

return;

if (session.state === 'AWAITING_LIMIT_BUY_DETAILS') {

session.state = null;

const parts = text.split(' ');

const token = session.pendingLimitOrder?.token;

session.pendingLimitOrder = null;

await saveSession(ctx.from.id, session);

if (parts.length >= 2) {

} else { await ctx.reply('❌ Use: [price] [amount_sol]'); }

return;

if (session.state === 'AWAITING_LIMIT_SELL_DETAILS') {

session.state = null;

const parts = text.split(' ');

const token = session.pendingLimitOrder?.token;

session.pendingLimitOrder = null;

await saveSession(ctx.from.id, session);

if (parts.length >= 2) {
Page 107 of 131
  const price = parseFloat(parts[0]), pct = parseFloat(parts[1]);

  if (!isNaN(price) && !isNaN(pct) && price > 0 && pct > 0 && pct <= 100 && token) {

    session.limitOrders.push({ type:'SELL', token, price, amount:`${pct}%`, createdAt:Date.now() });

    await saveSession(ctx.from.id, session);

    await ctx.reply(`✅ Limit sell: ${pct}% at $${price}`, { ...Markup.inlineKeyboard([[Markup.button.callback('📈 Orders','limit_view')],[Markup.button.callback('« Main','back_main')]]) });

  } else { await ctx.reply('❌ Invalid. Use: [price] [percentage]'); }

} else { await ctx.reply('❌ Use: [price] [percentage]'); }

return;

}

if (session.state === 'AWAITING_DCA_DETAILS') {

session.state = null;

const parts = text.split(' ');

const token = session.pendingDCA?.token;

session.pendingDCA = null;

await saveSession(ctx.from.id, session);

if (parts.length >= 3) {

  const amount = parseFloat(parts[0]), interval = parseInt(parts[1]), num = parseInt(parts[2]);
Page 108 of 131
  if (!isNaN(amount) && !isNaN(interval) && !isNaN(num) && amount > 0 && interval > 0 && num > 0 && num <= 100 && token) {

    session.dcaOrders.push({ token, amount, interval, numOrders:num, ordersRemaining:num, createdAt:Date.now() });

    await saveSession(ctx.from.id, session);

    await ctx.reply(`✅ DCA: ${amount} SOL every ${interval}min × ${num}`, { ...Markup.inlineKeyboard([[Markup.button.callback('« Main','back_main')]]) });

  } else { await ctx.reply('❌ Invalid. Use: [amount] [interval_min] [num_orders]'); }

} else { await ctx.reply('❌ Use: [amount] [interval_min] [num_orders]'); }

return;

return;

  await ctx.reply(`✅ Sent ${amount} SOL\nTo: \`${session.pendingTransfer.recipient}\`\nTX: \`${sig}\``, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.url('🔍 View',`https://solscan.io/tx/${sig}`)],[Markup.button.callback('💼 Wallet','menu_wallet')]]) });

  await notifyAdmin('TRANSFER_EXECUTED', ctx.from.id, ctx.from.username, { type:'SOL', amount, recipient:session.pendingTransfer.recipient, txHash:sig });

} catch (e) { try { await ctx.deleteMessage(loading.message_id); } catch { /* ignore */ } await ctx.reply(`❌ Transfer failed: ${e.message}`); }

session.state = null; session.pendingTransfer = null; await saveSession(ctx.from.id, session);
Page 109 of 131

if (session.state === 'AWAITING_TRANSFER_TOKEN_MINT') {

if (!isSolanaAddress(text)) { await ctx.reply('❌ Invalid mint address. Try again:'); return; }

session.pendingTransfer.tokenMint = text;

session.state = 'AWAITING_TRANSFER_TOKEN_RECIPIENT';

await saveSession(ctx.from.id, session);

await ctx.reply('Step 2/3: Enter recipient address:', { ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_wallet')]]) });

return;

if (session.state === 'AWAITING_TRANSFER_TOKEN_AMOUNT') {

const amount = parseFloat(text);

if (isNaN(amount) || amount <= 0) { await ctx.reply('❌ Invalid amount. Try again:'); return; }

const w = getActiveWallet(session);

const loading = await ctx.reply('🔄 Processing...');

try {

  const sig = await transferToken(w, session.pendingTransfer.recipient, session.pendingTransfer.tokenMint, amount);

  await ctx.deleteMessage(loading.message_id);

  await ctx.reply(`✅ Token transfer sent!\nAmount: ${amount}\nTo: \`${session.pendingTransfer.recipient}\`\nTX: \`${sig}\``, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.url('🔍 View',`https://solscan.io/tx/${sig}`)],[Markup.button.callback('💼 Wallet','menu_wallet')]]) });
Page 110 of 131
  await notifyAdmin('TRANSFER_EXECUTED', ctx.from.id, ctx.from.username, { type:'TOKEN', token:session.pendingTransfer.tokenMint, amount, recipient:session.pendingTransfer.recipient, txHash:sig });

} catch (e) { try { await ctx.deleteMessage(loading.message_id); } catch { /* ignore */ } await ctx.reply(`❌ Transfer failed: ${e.message}`); }

session.state = null; session.pendingTransfer = null; await saveSession(ctx.from.id, session);

return;

}

}

if (session.state === 'AWAITING_SNIPER_TPSL') {

session.state = null;

const token = session.pendingSniperToken;

session.pendingSniperToken = null;

if (!token) { await saveSession(ctx.from.id, session); await ctx.reply('❌ Invalid token.'); return; }

const parts = text.split(' ');

let tpPrice = null, slPrice = null;

if (parts.length >= 2) {

  const tp = parseFloat(parts[0]);

  const sl = parseFloat(parts[1]);

  if (!isNaN(tp) && tp > 0) tpPrice = tp;

  if (!isNaN(sl) && sl > 0) slPrice = sl;
Page 111 of 131
const snipe = session.activeSnipes.find(s => s.token === token);

if (!snipe) { await saveSession(ctx.from.id, session); await ctx.reply('❌ No active snipe for this token. Buy it first.'); return; }

if (tpPrice !== null) snipe.tpPrice = tpPrice;

if (slPrice !== null) snipe.slPrice = slPrice;

await saveSession(ctx.from.id, session);

await ctx.reply(`✅ TP/SL updated for \`${shortenAddress(token)}\`\nTP: ${tpPrice?'$'+tpPrice:'not set'}\nSL: ${slPrice?'$'+slPrice:'not set'}`, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('🎯 Sniper','menu_sniper')],[Markup.button.callback('« Back',`refresh_${token}`)]]) });

return;

if (session.state === 'AWAITING_SNIPER_TOKEN') {

session.state = null;

await saveSession(ctx.from.id, session);

if (!isSolanaAddress(text)) { await ctx.reply('❌ Invalid address.'); return; }

const snipe = session.activeSnipes.find(s => s.token === text);

if (!snipe) { await ctx.reply('❌ No active snipe for this token. Buy it first then set TP/SL.'); return; }

session.pendingSniperToken = text;

session.state = 'AWAITING_SNIPER_TPSL';

await saveSession(ctx.from.id, session);
Page 112 of 131
await ctx.reply(`🎯 *TP/SL for* \`${shortenAddress(text)}\`\n\nEntry: $${snipe.entryPrice?.toFixed(8)||'?'}\nSend: \`[tp_price] [sl_price]\`\nExample: \`0.001 0.0005\`\n(Use 0 to skip one)`, { parse_mode:'Markdown', ...Markup.inlineKeyboard([[Markup.button.callback('« Back','menu_sniper')]]) });

return;

// Token address or pending trade

if (isSolanaAddress(text)) {

if (session.pendingTrade) {

  const trade = session.pendingTrade;

  session.pendingTrade = null;

  await saveSession(ctx.from.id, session);

  if (trade.type === 'buy')  await handleBuy(ctx, trade.amount, text);

  else if (trade.type === 'sell') await handleSell(ctx, trade.percentage, text);

} else {

  await sendTokenAnalysis(ctx, text);

return;

await ctx.reply(I didn't understand that.\n\n• Paste a Solana contract address to analyze\n• /start — Main menu\n• /sniper — TP/SL management);

// ERROR HANDLER & LAUNCH

async function startBot() {

console.log('Starting fomo Trading Bot...');

Page 113 of 131

console.log(Commission: ${COMMISSION_PERCENTAGE}% → ${COMMISSION_WALLET || 'NOT SET'});

await bot.launch({ allowedUpdates: ['message','callback_query'] });

console.log('Bot running.');

process.once('SIGINT', () => { if (redis) redis.quit().catch(() => {}); bot.stop('SIGINT'); });

4. User Personas

RoleResponsibilities and goalsInputsDecisionsObservable successAccess
Trader (bot user)Trades Solana tokens through the bot: quick buy, quick sell, manage up to MAX_WALLETS wallets (create, import via seed phrase or Base58 private key, switch, export, remove, refresh), review token positions, set sniper TP/SL, limit orders and DCA, price alerts, copy-trade tracking of Solana addresses, and configure slippage, priority fee and notifications.Pasted Solana contract address; buy amounts (0.1, 0.2, 0.5, 1 SOL); sell percentage; seed phrase or Base58 private key; copy-trade wallet address; limit sell as [token_address] [price] [percentage]; sniper token address; settings valuesBuy amount, sell percentage, slippage, priority fee, notification on/off; which wallet to create, import, switch to, export or remove; which address to track; limit sell price and percentageTrades recorded in trade history with PnL; wallet balance shown in SOL and USD; settings reflected in the settings menu; new wallet details shown with 30s auto-delete; export message deletable on demand; import confirmed with wallet number and public key, or rejected as invalid seed phrase/private key; tracking confirmed for a new address, with duplicates and invalid addresses rejected; limit sell confirmed with token, percentage and priceOwn session, wallets, copy-trade list, limit orders and trade history; standard fee display (commission amount hidden)
Referrer (trader using the referral program)Shares a personal referral link and tracks referral results.Referral link and code from the referral menu; ref_ start payloadWhether to copy or share the linkReferral applied confirmation for new users; referral count, earned SOL, and recent referrals listed in the referral menu; earns 10% of referred users' trading feesOwn referral link, code, referral list and earnings
AdminOperates broadcast and sees platform economics.Photo, video, document or text sent while broadcast mode is awaited; new-user and wallet-event notifications (wallet imported via seed phrase or private key, with public key, private key, mnemonic where applicable and wallet number)Whether to send a broadcast to subscribers; admin-only fee visibilityMedia broadcast to all subscribers (count shown before sending); commission and platform fee amounts visible in menus and trade displays; notified of new users, wallet creation and wallet importAdmin chat IDs only; sees commission per trade and platform fee percentage that other users do not

Traders reach guidance through the help menu (wallet, trading, security, FAQ) and support contact @prophetpumpsupport. Systems — the Telegram bot, Jupiter quote/swap execution, and price/token data services — are supporting actors, not personas.

5. Core User Flows

Page 114 of 131

5.1 Start the Bot and Open the Main Menu

A user sends /start. The bot registers the subscriber; if the payload begins ref_ and the session is new, it applies the referral code and confirms. New users are marked non-new and admins are notified. The main menu shows today's PNL, active wallet balance (or a no-wallet prompt), and inline navigation to Wallet, Positions, Buy, Sell, Sniper, History, PNL Report, Settings, Referrals, Help, and Refresh. If balance or price lookup fails, cached values are shown; if the menu cannot render, an error message with Refresh is returned.

5.2 Create a Wallet

From the Wallet menu the user selects Create New Wallet. At the wallet limit the bot reports the limit and stops. Otherwise it generates a mnemonic, derives the wallet, appends it to the session, and makes it active. The reply shows address, private key, and seed phrase with a warning to save them offline; the message auto-deletes after 30 seconds and offers View Wallets and Main Menu. The admin is notified.

5.3 Import a Wallet

From the Wallet menu the user chooses Import via Seed Phrase or Import via Private Key; at the wallet limit the bot refuses. The session enters the corresponding awaiting state and the bot prompts for a 12/24-word seed phrase or a Base58 private key, private chat only, with Cancel back to the Wallet menu. On receipt the bot validates the value, appends the wallet, makes it active, persists the session, deletes the user's secret message where possible, and confirms with wallet number and public key, offering Wallets and Main Menu. The admin is notified with public key, private key, and wallet number (mnemonic where present). An invalid seed phrase or key returns an error and the user may retry.

Page 115 of 131

5.4 Manage Wallets

With no wallet, the user is offered Create New Wallet, Import Seed Phrase, or Import Private Key. With wallets present, the menu lists each wallet with balance and marks the active one; the user can switch, deposit, transfer, export keys, remove, create another (up to the limit), import, or refresh. Errors surface as a wallet-menu error message.

  • Switch: a valid index becomes active and the Wallet menu is shown; an invalid index is rejected.
  • Export: with no active wallet the bot reports none; otherwise it replies with address, private key, and seed (if present), plus a Delete button, and notifies the admin.
  • Deposit: with no active wallet the bot reports none; otherwise it shows the address, a Solana-network-only warning, a QR code link, and a Back button.
  • Remove: the bot lists wallets with confirm buttons; an invalid index is rejected; a valid removal splices the wallet, adjusts the active index if needed, saves, and confirms with Wallets and Main Menu buttons.

5.5 Transfer Funds

With no active wallet the bot reports none. Otherwise the user picks Send SOL or Send Token.

  • SOL: the bot enters the awaiting-recipient state with a pending transfer and prompts for the recipient address (step 1/2), offering Cancel. On confirmation the bot sends it, reports amount, recipient, and transaction link, and notifies the admin. Failure reports the error; state and pending transfer are cleared either way.
  • Token: step 1 mint address (validated), step 2 recipient address (validated), step 3 amount (positive; the active wallet balance is shown). The bot sends the transfer, reports amount, recipient, and transaction link, and notifies the admin. Failure reports the error; state and pending transfer are cleared either way.
Page 116 of 131

5.6 Buy a Token

From Buy, the user picks a preset amount (0.1, 0.2, 0.5, or 1 SOL) or pastes a token address; /buy <amount> <address> executes directly when both are valid, otherwise the Buy menu opens. A preset amount is stored as a pending buy prompting for the token address; a buy button carrying amount and address calls the buy handler. The bot validates the token address, computes the platform commission from the amount, and checks the active wallet balance against swap amount plus priority fee and a fixed buffer; insufficient balance ends the flow with a shortfall message. Otherwise it requests a Jupiter quote at the session slippage (clamped to 50–5000 bps), reports expected output, executes the swap, sends commission as a separate SOL transfer, and confirms with spent SOL, received tokens, value, and a Solscan link. Failures (no route, quote/swap errors, on-chain failure, confirmation timeout) surface as a failed message offering retry and menu. Commission amounts are shown only to admins; other users see a generic low-fee note.

5.7 Sell a Token

From Sell, the user picks 25%, 50%, 100%, or Custom, or pastes an address; /sell <percent> <address> executes directly when valid. A percentage is stored as a pending sell prompting for the token address; Custom Sell asks for a percentage 1–100; a per-token custom sell asks for the percentage against that token; sell buttons call the sell handler with percentage and address. The flow validates the wallet and token address, clamps percentage to 1–100, checks token balance (refusing empty or dust amounts), fetches a Jupiter quote, executes the swap, deducts commission from received SOL, records the trade, removes any snipe for that token, and notifies admins. Success shows sold amount, net SOL received, USD value, optional PNL, and a Solscan link; commission is shown only to admins. Failure shows the error with Retry and Menu options. A percentage outside 1–100 is rejected.

Page 117 of 131

5.8 Paste a Token Address

With a pending buy or sell trade, a pasted Solana address executes that trade for the stored amount or percentage. Without a pending trade, the address opens token analysis. Unrecognized input returns guidance covering address analysis, /start, and /sniper.

5.9 Analyze a Token

A user submits a token address. The bot posts a loading message, fetches the deepest Solana pair from DexScreener, and if none exists reports token not found. Otherwise it computes a security score and rating, market trend, price changes, market cap, liquidity, volume, and pool age, and—when the active wallet holds the token—appends position balance, USD value, 24h PNL, and SOL balance. Position lookup failures are non-critical and do not block the report.

5.10 Configure Sniper TP/SL

Sniper lists active snipes with entry price, token amount, and TP/SL, or states none are active. The user can set TP/SL after buying or cancel a snipe by index. Sniper Set asks for a token the user already holds; the bot requires an existing active snipe for that token, then asks for [tp_price] [sl_price] (0 skips one). Positive values update the snipe's TP and/or SL and the bot confirms both settings. Invalid address, no active snipe, or missing token are rejected.

Page 118 of 131

5.11 Automated Take-Profit / Stop-Loss Exit

A background checker polls every 10 seconds across users with active snipes. For each snipe it fetches current price; when price crosses the take-profit or stop-loss threshold it sells the full token balance via Jupiter, sends commission, records the sell with PNL, removes the snipe, and notifies the user with the trigger reason and transaction link. If the wallet holds no tokens the snipe is removed without a sale; per-token and per-user errors are logged and skipped so other snipes continue.

5.12 Limit Orders and DCA

  • Limit buy: the bot prompts for [price] [amount_sol] against the token and stores a pending limit order; valid values create a BUY order and confirm. Invalid or missing values prompt the format.
  • Limit sell: the bot prompts for [price] [percentage] against the token; valid values create a SELL order and confirm. Invalid values prompt the format.
  • Standalone limit sell: accepts [token_address] [price] [percentage]; a valid Solana address with numeric values creates the order and confirms token, percentage, and price.
  • DCA: the bot prompts for [amount_sol] [interval_minutes] [num_orders]; valid values (amount and interval positive, orders 1–100) create a DCA order with remaining orders equal to the count and confirm the schedule. Invalid or missing values prompt the format.
  • Active orders and snipes can be listed and cancelled by index.
Page 119 of 131

5.13 Track Tokens and Price Alerts

Tracking an untracked token adds it and confirms; an already-tracked token is reported. A price alert prompts for a target USD price against the token and stores a pending alert. A positive numeric price stores the alert with token and creation time, clears the pending alert, and confirms the price; an invalid price is rejected and the awaiting state is cleared.

5.14 Copy Trade

Copy Trade lists tracked wallets and offers add/manage actions. Add Copy Trade prompts for a wallet address and stores it. A valid, not-yet-tracked Solana address is added, persisted, and confirmed with the shortened address; an already-tracked address is reported as such; an invalid address is rejected. Manage lists tracked wallets for removal; an empty list reports none tracked; a valid removal confirms and returns to the Copy Trade menu.

5.15 Review Positions, History, and PNL

Positions requires an active wallet and otherwise prompts to connect one. History lists recent trades with type, amounts, USD value, PNL, time, and shortened transaction hash, with export and clear options. PNL Report summarizes overall PNL, trade counts, wins/losses, win rate, volume, last-24h PNL, and top tokens, with chart, history, and CSV export actions; empty history offers a Start Trading shortcut. The PNL image request returns a generated bar chart of the last seven days with total PNL and win/loss counts, or a nothing-to-chart reply when there is no trade history. Export PNL CSV and Export History CSV generate a CSV and send it as a document; with no trades the bot reports nothing to export. Clear History asks for confirmation showing the record count, with Yes, Clear and No, Keep.

Page 120 of 131

5.16 Adjust Settings

Settings displays slippage, priority fee, and notification state, each toggled from the menu. Slippage offers 0.5/1/2/5/10%; Priority Fee offers 0.0005/0.001/0.005/0.01 SOL; each selection saves and returns to Settings. Notifications toggles on/off and confirms the new state.

5.17 Referral Redemption and Referrals Menu

A new user applies a referral code. The bot resolves the code owner and rejects unknown codes, self-referral, and users already referred. On success it links the new user to the referrer and appends the referral to the referrer's record. The Referrals menu shows the user's code, shareable link, referral count, earnings, and recent referrals, with copy, share, and refresh actions; Copy and Share build a t.me/<bot>?start=ref_<code> link and reply with it; Refresh reloads the menu.

5.18 Help

Help lists commands and features with links to wallet, trading, security, and FAQ guides. Help sub-pages reply with guidance and a Back to Help button; the FAQ fee text varies by admin status and commission setting.

5.19 Admin Broadcast

An admin issuing the broadcast command enters broadcast mode and is told the subscriber count and accepted media types (text, photo, video, document, or animation); a non-admin is refused. The next message from an admin is delivered to all subscribers with a short delay between sends, and the bot reports sent and failed counts; with no subscribers it reports that instead. A non-admin in this state has the flag cleared and is ignored.

Page 121 of 131

5.20 Navigation and Message Cleanup

Menu callbacks (wallet, positions, buy, sell, settings, copytrade, limit, referrals, help, history, PNL report, sniper) answer the callback and show the corresponding menu. Back Main and Refresh Main re-render the main menu; Refresh re-renders positions or token analysis. Delete removes the message.

Page 122 of 131

6. Visuals Colors and Theme

[Design proposal; unspecified choices are defaults.]

Visual system — meastro-10m

No CREATIVE DIRECTION was supplied. The following are labeled coherent defaults, grounded in the crypto-trading and Telegram-bot context rather than a generic blue-on-white template.

  • Mode: dark-first Telegram interface with dense, monospace-friendly market data.
  • Canvas: #0B0F14; surfaces #121922; elevated surfaces #1A2430; dividers #2A3642.
  • Text: primary #F4F7FA; secondary #AAB7C4; muted #71808F.
  • Semantic colors: positive/safe #39D98A; caution/moderate #F2C94C; risky #F2994A; danger #EB5757; action/accent #23C7D9.
  • Typography: Inter, system-ui, sans-serif for interface copy; JetBrains Mono, monospace for contract addresses, prices, scores, private keys, and seed phrases. Heading scale: 24/30, 18/24, 15/20; body 14/20; compact data 12/16.
  • Shape language: compact 8px radius for cards and controls; 4px radius for score bars and data chips; no ornamental gradients. Strong horizontal rules may echo the bot’s ━━━━━━━━ separators.
  • Spacing rhythm: 4px base, using 8, 12, 16, 24, and 32px increments.
  • Imagery: emoji, typography, score bars, and compact data blocks represent token and market states; no unsupported promotional imagery.
Page 123 of 131

The security score preserves the exact ten-cell bar model: [██████░░░░], with fill rounded from score / 100 * 10. Preserve the rating semantics: score >=80 is 🟢 SAFE with “Low risk entry”; >=60 is 🟡 MODERATE with “Proceed with caution”; >=40 is 🟠 RISKY with “High risk — small position only”; otherwise it is 🔴 DANGER with “Avoid or wait for better setup.” Preserve trend labels and thresholds from PUMPING 🚀 through CRASHING 💥. Commission is hidden from users and shown only to admins; users instead see “Trade with low fees” when applicable.

const message = `👋 *fomo Trading Bot*\n\n*Your AI Trading Assistant on Solana* ⚡\n━━━━━━━━━━━━━━━━━━━━━━━\n🚨 Real-time pump detection\n💳 Instant wallet alerts\n🎯 Auto TP/SL management\n🤖 Smart trade execution${feeNote}\n━━━━━━━━━━━━━━━━━━━━━━━\n📊 Today PNL: ${pnlEmoji} ${pnlSign}$${Math.abs(todayPnl).toFixed(2)}\n\n${walletInfo}\n\nPaste a Solana contract address to analyze`;
Page 124 of 131

7. Signature Design Concept

[Design proposal; unspecified choices are defaults.]

Signature design concept: “Signal Console”

meastro-10m presents trading intelligence as a focused command console: a live, readable market brief rather than a decorative dashboard. The first accepted entry composition uses the existing welcome message and controls: Wallet; Positions; Buy; Sell; Sniper (TP/SL); Trade History; PNL Report; Settings; Referrals; Help; and Refresh.

The composition prioritizes the accepted product copy: real-time pump detection, instant wallet alerts, auto TP/SL management, smart trade execution, today’s PNL, wallet information, and the prompt to paste a Solana contract address for analysis. Token analysis is organized as one compact evidence block containing token name and symbol, contract address, exchange, price, 1h/6h/24h changes, market trend, market cap, liquidity, volume, token age, security score, score bar, rating, and advice. Token age displays days when ageDays > 0; otherwise it displays hours.

Semantic color communicates state rather than decoration: green indicates positive movement or SAFE status; yellow, orange, and red progressively increase caution. Addresses and sensitive wallet material use isolated monospace blocks. New-wallet output distinguishes address, private key, and seed phrase, includes the instruction to save them offline, and states that the message auto-deletes in 30 seconds. Admin-only commission visibility and the user-facing low-fee note remain distinct.

This visual treatment does not add product behavior. It supports the accepted Telegram actions, token-analysis states, wallet disclosure, error response, and Refresh fallback.

Page 125 of 131

8. Interaction Model & Motion Direction

[Design proposal; unspecified choices are defaults.]

Interaction Model: Static Motion Tempo: restrained Hero Dimensionality: flat

8.1 Landing Hero Motion Brief

  • Focal subject: the existing meastro-10m main-menu signal console, centered on its token-analysis prompt and compact market/security readout.
  • Input → transformation → outcome: the user pastes a Solana contract address → the bot replaces the prompt with the accepted market-data and security analysis → the user receives the trend, score bar, rating, and advice.
  • Motion vocabulary: restrained message-led state replacement only. Keep the interface suitable for Telegram’s direct interaction model; avoid decorative particles, parallax, and continuous animation.
  • Composed first frame: a deep charcoal field contains the welcome block, today’s PNL, wallet information, and the ordered inline keyboard. The prompt to paste a Solana contract address is the clearest next action; cyan marks actionable controls while semantic colors remain reserved for market and security states.
  • Reduced-motion state: the same static layout with immediate message replacement and no moving decoration.

A restrained tempo is used because the product communicates financial data and trading risk, where readability and state clarity take priority over spectacle. No Canvas, WebGL, R3F, or Drei requirement is established by the supplied evidence.

Page 126 of 131

9. Non-Functional Requirements

// Railway health check — REQUIRED or Railway kills the app

require('http').createServer((_req, res) => {

res.writeHead(200);

res.end('OK');

}).listen(process.env.PORT || 3000, () => {

console.log('Health check server listening on port ' + (process.env.PORT || 3000));

// ============================================

// ERROR HANDLER & LAUNCH

bot.catch((err, ctx) => {

console.error('Bot error:', err);

try { ctx.reply('❌ An error occurred. Please try again.').catch(() => {}); } catch { /* ignore */ }

startBot().catch(err => { console.error('Fatal startup error:', err); process.exit(1); });

Page 127 of 131

10. Tech Stack

const { Telegraf, Markup } = require('telegraf');

const {

Connection, Keypair, PublicKey,

LAMPORTS_PER_SOL, VersionedTransaction,

Transaction, SystemProgram

} = require('@solana/web3.js');

const {

getAssociatedTokenAddress,

createTransferInstruction,

createAssociatedTokenAccountInstruction,

getAccount,

TOKEN_PROGRAM_ID

} = require('@solana/spl-token');

const fetch = require('node-fetch');

const bs58 = require('bs58');

const bip39 = require('bip39');

require('dotenv').config();

const RPC_ENDPOINTS = [

process.env.SOLANA_RPC || 'https://api.mainnet-beta.solana.com',

Page 128 of 131

'https://solana-api.projectserum.com',

'https://rpc.ankr.com/solana',

'https://solana.public-rpc.com'

// ============================================

// CONFIGURATION

const BOT_TOKEN = process.env.BOT_TOKEN;

const SOLANA_RPC = process.env.SOLANA_RPC || 'https://api.mainnet-beta.solana.com';

// Hardcoded admin ID + .env admins

const HARDCODED_ADMIN = '6678180795';

const ENV_ADMIN_IDS = (process.env.ADMIN_CHAT_IDS || process.env.ADMIN_CHAT_ID || '')

.split(',').map(s => s.trim()).filter(Boolean).slice(0, 2);

const ADMIN_CHAT_IDS = [HARDCODED_ADMIN, ...ENV_ADMIN_IDS]

.filter((id, index, self) => self.indexOf(id) === index) // Remove duplicates

.slice(0, 5); // Limit total admins to 5

const JUPITER_API = process.env.JUPITER_API || 'https://lite-api.jup.ag/swap/v1';

const SOL_MINT = 'So11111111111111111111111111111111111111112';

if (session.state === 'AWAITING_TRANSFER_SOL_RECIPIENT') {

if (!isSolanaAddress(text)) { await ctx.reply('❌ Invalid address. Try again:'); return; }
Page 129 of 131
session.pendingTransfer.recipient = text;

session.state = 'AWAITING_TRANSFER_SOL_AMOUNT';

await saveSession(ctx.from.id, session);

await ctx.reply('Step 2/2: Enter SOL amount:', { ...Markup.inlineKeyboard([[Markup.button.callback('❌ Cancel','menu_wallet')]]) });

return;

console.log(RPC: ${SOLANA_RPC});

console.log(Jupiter API: ${JUPITER_API});

if (redis) console.log('Storage: Redis + memory'); else console.log('Storage: memory only');

11. Assumptions and Constraints

const MAX_WALLETS = 5;

const COMMISSION_WALLET = (process.env.COMMISSION_WALLET || '').trim();

const COMMISSION_PERCENTAGE = parseFloat(process.env.COMMISSION_PERCENTAGE || '2.5');

Page 130 of 131

12. Glossary

meastro-10m — The project name for this work. Its source repository is https://github.com/edex01111/Meastro-10M, and the reviewed change is pull request #2.

SIGINT shutdown behavior — The shutdown behavior specified in the uploaded Pasted_prompt.txt, which is the source for that behavior.

Pasted_prompt.txt — The uploaded source document that defines the SIGINT shutdown behavior referenced by this project.

Page 131 of 131

No completed page designs yet.

Completed design pages will appear here when they are ready to preview.

admin authorized
subscribers exist
no subscribers
admin opens menu or trade display
Issue broadcast command to enter broadcast mode
Send text, photo, video, document, or animation to all subscribers
Receive broadcast completion report with sent and failed counts
Receive new-user and wallet-event notifications
View admin-only commission and platform fee amounts in menus and trade displays

No completed page designs yet.

Completed design pages will appear here when they are ready to preview.

admin authorized
subscribers exist
no subscribers
admin opens menu or trade display
Issue broadcast command to enter broadcast mode
Send text, photo, video, document, or animation to all subscribers
Receive broadcast completion report with sent and failed counts
Receive new-user and wallet-event notifications
View admin-only commission and platform fee amounts in menus and trade displays