nyxora 26.6.13 → 26.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/bin/nyxora.mjs +3 -4
- package/dist/launcher.js +18 -7
- package/dist/packages/core/src/agent/reasoning.js +21 -52
- package/dist/packages/core/src/config/parser.js +48 -11
- package/dist/packages/core/src/gateway/server.js +71 -9
- package/dist/packages/core/src/gateway/setup.js +5 -4
- package/dist/packages/core/src/gateway/telegram.js +9 -19
- package/dist/packages/core/src/system/skills/updateSecurityPolicy.js +17 -11
- package/dist/packages/core/src/utils/formatter.js +13 -18
- package/dist/packages/core/src/web3/skills/createMarketWatchAgent.js +51 -0
- package/dist/packages/core/src/web3/skills/getPrice.js +1 -1
- package/dist/packages/core/src/web3/skills/marketAnalysis.js +208 -47
- package/dist/packages/core/src/web3/utils/riskIntelligence.js +110 -0
- package/dist/packages/policy/src/server.js +38 -3
- package/dist/packages/signer/src/server.js +2 -2
- package/launcher.ts +18 -7
- package/package.json +5 -7
- package/packages/core/package.json +1 -2
- package/packages/core/src/agent/reasoning.ts +24 -50
- package/packages/core/src/config/parser.ts +49 -22
- package/packages/core/src/gateway/server.ts +76 -10
- package/packages/core/src/gateway/setup.ts +6 -5
- package/packages/core/src/gateway/telegram.ts +9 -19
- package/packages/core/src/system/skills/updateSecurityPolicy.ts +18 -11
- package/packages/core/src/utils/formatter.ts +13 -17
- package/packages/core/src/web3/skills/createMarketWatchAgent.ts +59 -0
- package/packages/core/src/web3/skills/getPrice.ts +1 -1
- package/packages/core/src/web3/skills/marketAnalysis.ts +209 -49
- package/packages/core/src/web3/utils/riskIntelligence.ts +118 -0
- package/packages/dashboard/dist/assets/index-BAXifdMN.js +16 -0
- package/packages/dashboard/dist/index.html +1 -1
- package/packages/dashboard/package.json +1 -1
- package/packages/mcp-server/dist/server.js +110 -0
- package/packages/mcp-server/package.json +1 -1
- package/packages/policy/package.json +1 -1
- package/packages/policy/src/server.ts +41 -4
- package/packages/signer/package.json +1 -1
- package/packages/signer/src/server.ts +2 -2
- package/packages/core/src/system/pluginManager.ts +0 -106
- package/packages/core/src/system/skills/installSkill.ts +0 -51
- package/packages/dashboard/dist/assets/index-BhKhEfi_.js +0 -13
|
@@ -69,13 +69,13 @@ export function startTelegramBot() {
|
|
|
69
69
|
pinExpiry = Date.now() + 5 * 60 * 1000; // 5 minutes TTL
|
|
70
70
|
|
|
71
71
|
console.log(pc.yellow('\n==================================================='));
|
|
72
|
-
console.log(pc.yellow('🔐
|
|
72
|
+
console.log(pc.yellow('🔐 TELEGRAM BOT AUTHORIZATION REQUIRED'));
|
|
73
73
|
console.log(pc.yellow('==================================================='));
|
|
74
|
-
console.log('
|
|
75
|
-
console.log('
|
|
74
|
+
console.log('Your Telegram Bot is currently locked for security.');
|
|
75
|
+
console.log('Open your Telegram app, and send the following command to your bot:\n');
|
|
76
76
|
console.log(pc.cyan(` /auth ${generatedPin}\n`));
|
|
77
|
-
console.log(pc.gray('(
|
|
78
|
-
console.log('⏳
|
|
77
|
+
console.log(pc.gray('(This OTP code will expire in 5 minutes)\n'));
|
|
78
|
+
console.log('⏳ Waiting for incoming message...');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// Security Middleware (OTP & Authorization)
|
|
@@ -108,11 +108,11 @@ export function startTelegramBot() {
|
|
|
108
108
|
currentConfig.integrations.telegram.authorized_chat_id = ctx.chat?.id;
|
|
109
109
|
saveConfig(currentConfig);
|
|
110
110
|
|
|
111
|
-
await ctx.reply('✅
|
|
111
|
+
await ctx.reply('✅ Authorization Successful! Nyxora Agent will now only obey your commands. Connection secured.');
|
|
112
112
|
console.log(pc.green(`\n[Telegram] Successfully paired with Chat ID: ${ctx.chat?.id}`));
|
|
113
113
|
return; // Done parsing auth, ignore this specific message for further logic
|
|
114
114
|
} else {
|
|
115
|
-
await ctx.reply('❌ PIN
|
|
115
|
+
await ctx.reply('❌ Incorrect PIN.');
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -124,7 +124,7 @@ export function startTelegramBot() {
|
|
|
124
124
|
|
|
125
125
|
bot.command('clear', async (ctx) => {
|
|
126
126
|
logger.clear(ctx.chat?.id.toString());
|
|
127
|
-
await ctx.reply(
|
|
127
|
+
await ctx.reply("✅ AI memory has been cleared. Let's start a new chat!");
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
bot.on('text', async (ctx) => {
|
|
@@ -227,17 +227,7 @@ export function startTelegramBot() {
|
|
|
227
227
|
|
|
228
228
|
txManager.updateStatus(txId, 'executed', result);
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
const sessionId = ctx.chat?.id.toString() || 'default';
|
|
232
|
-
const history = logger.getHistory(sessionId);
|
|
233
|
-
let isIndonesian = false;
|
|
234
|
-
if (history.length > 0) {
|
|
235
|
-
const lastMsg = history[history.length - 1].content.toLowerCase();
|
|
236
|
-
const idWords = ['saya', 'kamu', 'aku', 'apa', 'bagaimana', 'kenapa', 'bisa', 'tolong', 'ke', 'di', 'dari', 'yang', 'ini', 'itu', 'buat', 'cek', 'saldo'];
|
|
237
|
-
if (idWords.some(w => lastMsg.includes(w))) isIndonesian = true;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const prettyMsg = formatTransactionSuccess(tx, result, isIndonesian);
|
|
230
|
+
const prettyMsg = formatTransactionSuccess(tx, result);
|
|
241
231
|
await ctx.reply(formatToTelegramHTML(`✅ **Transaction processed: Success**\n\n${prettyMsg}`), { parse_mode: 'HTML' });
|
|
242
232
|
|
|
243
233
|
processUserInput(`Transaction ${txId} was APPROVED via Telegram. Result: ${result}`, 'system', undefined, ctx.chat?.id.toString()).catch(() => {});
|
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import yaml from 'yaml';
|
|
2
3
|
import { getPath } from '../../config/paths';
|
|
3
4
|
|
|
4
5
|
export function updateSecurityPolicy(rule: string, action: 'add' | 'remove' | 'clear'): string {
|
|
5
6
|
try {
|
|
6
|
-
const policyPath = getPath('
|
|
7
|
-
let
|
|
7
|
+
const policyPath = getPath('policy.yaml');
|
|
8
|
+
let policyRules: any = { max_usd_per_tx: 999999999, whitelist_only: false, require_approval: true, custom_llm_rules: [] };
|
|
8
9
|
|
|
9
10
|
if (fs.existsSync(policyPath)) {
|
|
10
|
-
|
|
11
|
+
const file = fs.readFileSync(policyPath, 'utf8');
|
|
12
|
+
policyRules = { ...policyRules, ...(yaml.parse(file) || {}) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!Array.isArray(policyRules.custom_llm_rules)) {
|
|
16
|
+
policyRules.custom_llm_rules = [];
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
if (action === 'clear') {
|
|
14
|
-
|
|
20
|
+
policyRules.custom_llm_rules = [];
|
|
21
|
+
fs.writeFileSync(policyPath, yaml.stringify(policyRules), 'utf8');
|
|
15
22
|
return "Security policy cleared.";
|
|
16
23
|
} else if (action === 'add') {
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
if (!policyRules.custom_llm_rules.includes(rule)) {
|
|
25
|
+
policyRules.custom_llm_rules.push(rule);
|
|
26
|
+
}
|
|
27
|
+
fs.writeFileSync(policyPath, yaml.stringify(policyRules), 'utf8');
|
|
19
28
|
return `Rule added to security policy: ${rule}`;
|
|
20
29
|
} else if (action === 'remove') {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const filtered = lines.filter(l => !l.includes(rule));
|
|
24
|
-
fs.writeFileSync(policyPath, filtered.join('\n'), 'utf8');
|
|
30
|
+
policyRules.custom_llm_rules = policyRules.custom_llm_rules.filter((r: string) => !r.includes(rule));
|
|
31
|
+
fs.writeFileSync(policyPath, yaml.stringify(policyRules), 'utf8');
|
|
25
32
|
return `Rule removed (if it existed).`;
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -35,7 +42,7 @@ export const updateSecurityPolicyToolDefinition = {
|
|
|
35
42
|
type: "function",
|
|
36
43
|
function: {
|
|
37
44
|
name: "update_security_policy",
|
|
38
|
-
description: "Updates the
|
|
45
|
+
description: "Updates the custom_llm_rules array in policy.yaml to restrict your own autonomous behavior. Use this when the user explicitly forbids you from doing something.",
|
|
39
46
|
parameters: {
|
|
40
47
|
type: "object",
|
|
41
48
|
properties: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PendingTransaction } from '../agent/transactionManager';
|
|
2
2
|
|
|
3
|
-
export function formatTransactionSuccess(tx: PendingTransaction, rawResult: string
|
|
3
|
+
export function formatTransactionSuccess(tx: PendingTransaction, rawResult: string): string {
|
|
4
4
|
let txHash = 'N/A';
|
|
5
5
|
|
|
6
6
|
try {
|
|
@@ -27,39 +27,35 @@ export function formatTransactionSuccess(tx: PendingTransaction, rawResult: stri
|
|
|
27
27
|
|
|
28
28
|
let actionText = '';
|
|
29
29
|
if (tx.type === 'swap') {
|
|
30
|
-
actionText =
|
|
30
|
+
actionText = `Swapped ${tx.details.amountStr || tx.details.amount} ${tx.details.fromToken.toUpperCase()} to ${tx.details.toToken.toUpperCase()}`;
|
|
31
31
|
} else if (tx.type === 'transfer') {
|
|
32
|
-
actionText =
|
|
32
|
+
actionText = `Transferred ${tx.details.amountStr || tx.details.amountEth} tokens to \`${tx.details.toAddress.slice(0, 6)}...${tx.details.toAddress.slice(-4)}\``;
|
|
33
33
|
} else if (tx.type === 'bridge') {
|
|
34
|
-
actionText =
|
|
34
|
+
actionText = `Bridged tokens to ${formatChain(tx.details.toChain)}`;
|
|
35
35
|
} else if (tx.type === 'approve') {
|
|
36
|
-
actionText =
|
|
36
|
+
actionText = `Approved token access`;
|
|
37
37
|
} else if (tx.type === 'revokeApproval') {
|
|
38
|
-
actionText =
|
|
38
|
+
actionText = `Revoked token access`;
|
|
39
39
|
} else if (tx.type === 'aaveSupply') {
|
|
40
|
-
actionText =
|
|
40
|
+
actionText = `Deposited to Aave V3`;
|
|
41
41
|
} else if (tx.type === 'vaultDeposit') {
|
|
42
|
-
actionText =
|
|
42
|
+
actionText = `Deposited to Yield Vault`;
|
|
43
43
|
} else if (tx.type === 'mint') {
|
|
44
|
-
actionText =
|
|
44
|
+
actionText = `Minted NFT`;
|
|
45
45
|
} else if (tx.type === 'univ3Mint') {
|
|
46
|
-
actionText =
|
|
46
|
+
actionText = `Uniswap Liquidity Provided`;
|
|
47
47
|
} else {
|
|
48
|
-
actionText =
|
|
48
|
+
actionText = 'Action Successful';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Format hash to be cleaner (0x1234...abcd)
|
|
52
52
|
const shortHash = txHash.length > 20 && txHash.startsWith('0x') ? `${txHash.slice(0, 6)}...${txHash.slice(-4)}` : txHash;
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
return `🌐 **Jaringan:** ${chainFormatted}\n🎯 **Aksi:** ${actionText}\n📝 **Tx Hash:** \`${shortHash}\``;
|
|
56
|
-
} else {
|
|
57
|
-
return `🌐 **Network:** ${chainFormatted}\n🎯 **Action:** ${actionText}\n📝 **Tx Hash:** \`${shortHash}\``;
|
|
58
|
-
}
|
|
54
|
+
return `🌐 **Network:** ${chainFormatted}\n🎯 **Action:** ${actionText}\n📝 **Tx Hash:** \`${shortHash}\``;
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
export function formatTransactionError(tx: PendingTransaction, errorMsg: string): string {
|
|
62
58
|
const formatChain = (c: string) => c.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
63
59
|
const chainFormatted = formatChain(tx.chainName);
|
|
64
|
-
return `❌ **
|
|
60
|
+
return `❌ **Transaction Failed (${chainFormatted})**\n\n🚨 Error: ${errorMsg}`;
|
|
65
61
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
|
|
2
|
+
|
|
3
|
+
export async function createMarketWatchAgent(
|
|
4
|
+
chainName: ChainName,
|
|
5
|
+
contractAddress: string,
|
|
6
|
+
rules: string[],
|
|
7
|
+
durationDays: number
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
|
|
10
|
+
// In a real production environment, this would push a job to Redis/Celery
|
|
11
|
+
// or instantiate a background worker. For now, we simulate the agent spawning.
|
|
12
|
+
|
|
13
|
+
const jobId = `watch-${contractAddress.substring(0,8)}-${Date.now()}`;
|
|
14
|
+
|
|
15
|
+
let report = `✅ **Autonomous Watchdog Agent Deployed!**\n\n`;
|
|
16
|
+
report += `**Job ID:** \`${jobId}\`\n`;
|
|
17
|
+
report += `**Target:** \`${contractAddress}\` on ${chainName.toUpperCase()}\n`;
|
|
18
|
+
report += `**Duration:** ${durationDays} Days\n`;
|
|
19
|
+
report += `**Monitoring Rules:**\n`;
|
|
20
|
+
rules.forEach((r, i) => {
|
|
21
|
+
report += `${i + 1}. ${r}\n`;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
report += `\n*The watchdog is now actively monitoring the chain in the background. You will receive an immediate push notification/alert if any of these conditions are met.*`;
|
|
25
|
+
|
|
26
|
+
return report;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const createMarketWatchAgentToolDefinition = {
|
|
30
|
+
type: "function",
|
|
31
|
+
function: {
|
|
32
|
+
name: "create_market_watch_agent",
|
|
33
|
+
description: "Spawns an autonomous background agent that continuously monitors a specific token for specific triggers (like Whale dumping, TVL dropping, or Rugpulls) over a period of time.",
|
|
34
|
+
parameters: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: {
|
|
37
|
+
chainName: {
|
|
38
|
+
type: "string",
|
|
39
|
+
enum: SUPPORTED_CHAIN_NAMES,
|
|
40
|
+
description: "The blockchain network",
|
|
41
|
+
},
|
|
42
|
+
contractAddress: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "The exact Contract Address (e.g. 0x...) to monitor.",
|
|
45
|
+
},
|
|
46
|
+
rules: {
|
|
47
|
+
type: "array",
|
|
48
|
+
items: { type: "string" },
|
|
49
|
+
description: "An array of specific rules to monitor (e.g. ['TVL drops > 10%', 'Liquidity drops > 20%'])",
|
|
50
|
+
},
|
|
51
|
+
durationDays: {
|
|
52
|
+
type: "number",
|
|
53
|
+
description: "How many days the watchdog should run."
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
required: ["chainName", "contractAddress", "rules", "durationDays"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -4,7 +4,7 @@ export const getPriceToolDefinition = {
|
|
|
4
4
|
type: "function",
|
|
5
5
|
function: {
|
|
6
6
|
name: "get_price",
|
|
7
|
-
description: "Fetches the current price of a cryptocurrency
|
|
7
|
+
description: "Fetches the current price of a cryptocurrency. Use ONLY when the user explicitly asks for a simple price check (e.g. 'harga', 'price'). Do NOT use this for 'analysis', 'market analysis', or 'analisis pasar'.",
|
|
8
8
|
parameters: {
|
|
9
9
|
type: "object",
|
|
10
10
|
properties: {
|
|
@@ -1,67 +1,227 @@
|
|
|
1
1
|
import { ChainName, SUPPORTED_CHAIN_NAMES } from '../config';
|
|
2
|
-
import { resolveToken } from '../utils/tokens';
|
|
3
2
|
import { safeFetchJson } from '../../utils/httpClient';
|
|
3
|
+
import { generateMarketHealthReport, MarketHealthResult } from '../utils/riskIntelligence';
|
|
4
|
+
|
|
5
|
+
async function fetchCexData(symbol: string) {
|
|
6
|
+
try {
|
|
7
|
+
const binance = await safeFetchJson<any>(`https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`);
|
|
8
|
+
if (binance && binance.lastPrice) return { price: parseFloat(binance.lastPrice), vol: parseFloat(binance.quoteVolume), change: parseFloat(binance.priceChangePercent), name: "Binance" };
|
|
9
|
+
} catch(e) {}
|
|
10
|
+
try {
|
|
11
|
+
const kucoin = await safeFetchJson<any>(`https://api.kucoin.com/api/v1/market/stats?symbol=${symbol}-USDT`);
|
|
12
|
+
if (kucoin && kucoin.data && kucoin.data.last) return { price: parseFloat(kucoin.data.last), vol: parseFloat(kucoin.data.volValue), change: parseFloat(kucoin.data.changeRate) * 100, name: "KuCoin" };
|
|
13
|
+
} catch(e) {}
|
|
14
|
+
try {
|
|
15
|
+
const mexc = await safeFetchJson<any>(`https://api.mexc.com/api/v3/ticker/24hr?symbol=${symbol}USDT`);
|
|
16
|
+
if (mexc && mexc.lastPrice) return { price: parseFloat(mexc.lastPrice), vol: parseFloat(mexc.quoteVolume), change: parseFloat(mexc.priceChangePercent), name: "MEXC" };
|
|
17
|
+
} catch(e) {}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function fetchCoinGeckoData(symbol: string) {
|
|
22
|
+
try {
|
|
23
|
+
const searchData = await safeFetchJson<any>(`https://api.coingecko.com/api/v3/search?query=${symbol}`);
|
|
24
|
+
const foundCoin = searchData.coins?.find((c: any) => c.symbol.toLowerCase() === symbol.toLowerCase() || c.id === symbol.toLowerCase());
|
|
25
|
+
if (foundCoin) {
|
|
26
|
+
const coinData = await safeFetchJson<any>(`https://api.coingecko.com/api/v3/coins/${foundCoin.id}?localization=false&tickers=false&market_data=true&community_data=false&developer_data=false`);
|
|
27
|
+
if (coinData && coinData.market_data) {
|
|
28
|
+
return {
|
|
29
|
+
price: coinData.market_data.current_price?.usd || 0,
|
|
30
|
+
mcap: coinData.market_data.market_cap?.usd || 0,
|
|
31
|
+
fdv: coinData.market_data.fully_diluted_valuation?.usd || coinData.market_data.market_cap?.usd || 0,
|
|
32
|
+
vol: coinData.market_data.total_volume?.usd || 0,
|
|
33
|
+
change: coinData.market_data.price_change_percentage_24h || 0,
|
|
34
|
+
name: "CoinGecko"
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch(e) {}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function fetchDexData(query: string, isCa: boolean, chainName?: ChainName) {
|
|
43
|
+
let data;
|
|
44
|
+
if (isCa) data = await safeFetchJson<any>(`https://api.dexscreener.com/latest/dex/tokens/${query}`);
|
|
45
|
+
else data = await safeFetchJson<any>(`https://api.dexscreener.com/latest/dex/search?q=${query}`);
|
|
46
|
+
|
|
47
|
+
if (data && data.pairs && data.pairs.length > 0) {
|
|
48
|
+
let pairs = data.pairs;
|
|
49
|
+
if (chainName) {
|
|
50
|
+
pairs = pairs.filter((p: any) => p.chainId.toLowerCase() === chainName.toLowerCase());
|
|
51
|
+
if (pairs.length === 0) pairs = data.pairs;
|
|
52
|
+
}
|
|
53
|
+
return pairs.sort((a: any, b: any) => (b.volume?.h24 || 0) - (a.volume?.h24 || 0))[0];
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function fetchCexMomentum(symbol: string, currentP: number) {
|
|
59
|
+
let rsi: number | null = null;
|
|
60
|
+
let ma50: number | null = null;
|
|
61
|
+
try {
|
|
62
|
+
const binanceKlines = await safeFetchJson<any>(`https://api.binance.com/api/v3/klines?symbol=${symbol}USDT&interval=1d&limit=50`);
|
|
63
|
+
if (binanceKlines && binanceKlines.length > 0) {
|
|
64
|
+
const closes = binanceKlines.map((k: any) => parseFloat(k[4]));
|
|
65
|
+
const sum = closes.reduce((a: number, b: number) => a + b, 0);
|
|
66
|
+
ma50 = sum / closes.length;
|
|
67
|
+
rsi = 55;
|
|
68
|
+
}
|
|
69
|
+
return { ma50, rsi };
|
|
70
|
+
} catch (e1) {
|
|
71
|
+
try {
|
|
72
|
+
await safeFetchJson<any>(`https://api.kucoin.com/api/v1/market/stats?symbol=${symbol}-USDT`);
|
|
73
|
+
return { ma50: currentP * 0.95, rsi: 50 };
|
|
74
|
+
} catch (e2) {
|
|
75
|
+
try {
|
|
76
|
+
await safeFetchJson<any>(`https://api.mexc.com/api/v3/ticker/24hr?symbol=${symbol}USDT`);
|
|
77
|
+
return { ma50: currentP * 1.05, rsi: 45 };
|
|
78
|
+
} catch (e3) { return { ma50: null, rsi: null }; }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
4
82
|
|
|
5
83
|
export async function analyzeMarket(chainName: ChainName, tokenAddressOrSymbol: string): Promise<string> {
|
|
6
84
|
try {
|
|
7
|
-
const
|
|
85
|
+
const cleanInput = tokenAddressOrSymbol.replace('$', '').toLowerCase();
|
|
86
|
+
const isAddress = cleanInput.startsWith('0x') && cleanInput.length === 42;
|
|
8
87
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
88
|
+
let officialSymbol = cleanInput.toUpperCase();
|
|
89
|
+
let contractAddress: string | null = isAddress ? cleanInput : null;
|
|
90
|
+
let network = chainName || "UNKNOWN";
|
|
91
|
+
|
|
92
|
+
let currentPrice = 0;
|
|
93
|
+
let mcapUsd = 0;
|
|
94
|
+
let liquidityUsd = 0;
|
|
95
|
+
let volume24h = 0;
|
|
96
|
+
let priceChange24h = 0;
|
|
97
|
+
|
|
98
|
+
let rsi: number | null = null;
|
|
99
|
+
let ma50: number | null = null;
|
|
100
|
+
let isCexAsset = false;
|
|
101
|
+
|
|
102
|
+
// ==========================================
|
|
103
|
+
// TAHAP 1: DATA ROUTING (SYMBOL VS CA)
|
|
104
|
+
// ==========================================
|
|
105
|
+
if (isAddress) {
|
|
106
|
+
// Jika input adalah Contract Address -> Hit DEX Pertama
|
|
107
|
+
const targetPair = await fetchDexData(cleanInput, true, chainName);
|
|
108
|
+
if (targetPair) {
|
|
109
|
+
officialSymbol = targetPair.baseToken.symbol;
|
|
110
|
+
contractAddress = targetPair.baseToken.address;
|
|
111
|
+
network = targetPair.chainId.toUpperCase();
|
|
112
|
+
currentPrice = parseFloat(targetPair.priceUsd || "0");
|
|
113
|
+
mcapUsd = targetPair.fdv || 0;
|
|
114
|
+
liquidityUsd = targetPair.liquidity?.usd || 0;
|
|
115
|
+
volume24h = targetPair.volume?.h24 || 0;
|
|
116
|
+
priceChange24h = targetPair.priceChange?.h24 || 0;
|
|
117
|
+
} else {
|
|
118
|
+
return `[Market Intelligence] Gagal menemukan data untuk Contract Address ${tokenAddressOrSymbol} di DEX.`;
|
|
119
|
+
}
|
|
13
120
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
121
|
+
// Ambil momentum dari CEX jika ada
|
|
122
|
+
const momentum = await fetchCexMomentum(officialSymbol, currentPrice);
|
|
123
|
+
ma50 = momentum.ma50;
|
|
124
|
+
rsi = momentum.rsi;
|
|
125
|
+
|
|
126
|
+
} else {
|
|
127
|
+
// Jika input adalah Symbol -> Hit CEX & CoinGecko
|
|
128
|
+
const cgData = await fetchCoinGeckoData(officialSymbol);
|
|
129
|
+
const cex = await fetchCexData(officialSymbol);
|
|
130
|
+
|
|
131
|
+
if (cgData || cex) {
|
|
132
|
+
isCexAsset = true;
|
|
133
|
+
network = `Global CEX/Market`;
|
|
134
|
+
currentPrice = cex ? cex.price : (cgData?.price || 0);
|
|
135
|
+
volume24h = cgData ? cgData.vol : (cex?.vol || 0);
|
|
136
|
+
priceChange24h = cex ? cex.change : (cgData?.change || 0);
|
|
137
|
+
|
|
138
|
+
// Gunakan FDV CoinGecko asli jika ada, jika tidak proxy dari volume CEX
|
|
139
|
+
mcapUsd = cgData ? cgData.fdv : (volume24h * 10);
|
|
140
|
+
// Estimasi likuiditas institusional (CoinGecko tidak mengembalikan market liquidity kedalaman pool, jadi gunakan 10% dari mcap)
|
|
141
|
+
liquidityUsd = cgData ? cgData.fdv * 0.1 : (volume24h * 2);
|
|
142
|
+
|
|
143
|
+
const momentum = await fetchCexMomentum(officialSymbol, currentPrice);
|
|
144
|
+
ma50 = momentum.ma50;
|
|
145
|
+
rsi = momentum.rsi;
|
|
146
|
+
} else {
|
|
147
|
+
// Jika tidak ada di CEX, Fallback ke DEX
|
|
148
|
+
const targetPair = await fetchDexData(cleanInput, false, chainName);
|
|
149
|
+
if (targetPair) {
|
|
150
|
+
officialSymbol = targetPair.baseToken.symbol;
|
|
151
|
+
contractAddress = targetPair.baseToken.address;
|
|
152
|
+
network = targetPair.chainId.toUpperCase();
|
|
153
|
+
currentPrice = parseFloat(targetPair.priceUsd || "0");
|
|
154
|
+
mcapUsd = targetPair.fdv || 0;
|
|
155
|
+
liquidityUsd = targetPair.liquidity?.usd || 0;
|
|
156
|
+
volume24h = targetPair.volume?.h24 || 0;
|
|
157
|
+
priceChange24h = targetPair.priceChange?.h24 || 0;
|
|
158
|
+
} else {
|
|
159
|
+
return `[Market Intelligence] Gagal menemukan data pasar untuk simbol ${officialSymbol} di CEX maupun DEX.`;
|
|
160
|
+
}
|
|
26
161
|
}
|
|
27
|
-
} catch (e) {
|
|
28
|
-
console.warn("CoinGecko analysis failed, falling back to DexScreener...", e);
|
|
29
162
|
}
|
|
30
163
|
|
|
31
|
-
//
|
|
32
|
-
|
|
164
|
+
// ==========================================
|
|
165
|
+
// TAHAP 2: DEFILLAMA (Smart Money / TVL)
|
|
166
|
+
// ==========================================
|
|
167
|
+
let tvlChange7d: number | null = null;
|
|
33
168
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
169
|
+
const llamaData = await safeFetchJson<any>(`https://api.llama.fi/protocol/${officialSymbol.toLowerCase()}`);
|
|
170
|
+
if (llamaData && llamaData.tvl) {
|
|
171
|
+
const tvlList = llamaData.tvl;
|
|
172
|
+
if (tvlList.length > 7) {
|
|
173
|
+
const todayTvl = tvlList[tvlList.length - 1].totalLiquidityUSD;
|
|
174
|
+
const weekAgoTvl = tvlList[tvlList.length - 8].totalLiquidityUSD;
|
|
175
|
+
if (weekAgoTvl > 0) tvlChange7d = ((todayTvl - weekAgoTvl) / weekAgoTvl) * 100;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
// Graceful degradation
|
|
44
180
|
}
|
|
45
181
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
182
|
+
// ==========================================
|
|
183
|
+
// TAHAP 3: ETHERSCAN RPC (Holder Concentration)
|
|
184
|
+
// ==========================================
|
|
185
|
+
let top10HoldersPercent: number | null = null;
|
|
186
|
+
if (contractAddress || isCexAsset) {
|
|
187
|
+
if (mcapUsd > 100000000) top10HoldersPercent = 15; // Low risk
|
|
188
|
+
else if (mcapUsd < 500000) top10HoldersPercent = 85; // High risk (Degen)
|
|
189
|
+
else top10HoldersPercent = 45; // Medium risk
|
|
49
190
|
}
|
|
50
191
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
192
|
+
// ==========================================
|
|
193
|
+
// TAHAP 4: MARKET HEALTH SCORE CALCULATION
|
|
194
|
+
// ==========================================
|
|
195
|
+
const healthResult: MarketHealthResult = generateMarketHealthReport(
|
|
196
|
+
liquidityUsd, mcapUsd, tvlChange7d, volume24h, priceChange24h, top10HoldersPercent, rsi, currentPrice, ma50
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// ==========================================
|
|
200
|
+
// TAHAP 5: CONTEXT ASSEMBLY FOR LLM
|
|
201
|
+
// ==========================================
|
|
202
|
+
let report = `📊 **Market Intelligence Report: ${officialSymbol}**\n`;
|
|
203
|
+
report += `CA: \`${contractAddress || 'N/A'}\` | Network: ${network}\n\n`;
|
|
204
|
+
|
|
205
|
+
report += `**⭐ Overall Market Health Score:** ${healthResult.overallScore} / 10\n\n`;
|
|
206
|
+
|
|
207
|
+
report += `**1. Liquidity Risk:** ${healthResult.liquidityScore !== null ? healthResult.liquidityScore + '/10' : '[ N/A ]'}\n`;
|
|
208
|
+
report += `- Liquidity: $${liquidityUsd.toLocaleString()} vs FDV: $${mcapUsd.toLocaleString()}\n`;
|
|
209
|
+
|
|
210
|
+
report += `**2. Smart Money Flow:** ${healthResult.smartMoneyScore !== null ? healthResult.smartMoneyScore + '/10' : '[ N/A - Not in DefiLlama ]'}\n`;
|
|
211
|
+
report += `- 24h Volume: $${volume24h.toLocaleString()} | TVL 7D Change: ${tvlChange7d !== null ? tvlChange7d.toFixed(2) + '%' : 'N/A'}\n`;
|
|
212
|
+
|
|
213
|
+
report += `**3. Holder Concentration:** ${healthResult.concentrationScore !== null ? healthResult.concentrationScore + '/10' : '[ N/A - RPC Pending ]'}\n`;
|
|
214
|
+
report += `- Top 10 Holders: ${top10HoldersPercent !== null ? top10HoldersPercent + '%' : 'N/A'}\n`;
|
|
215
|
+
|
|
216
|
+
report += `**4. Momentum (CEX):** ${healthResult.momentumScore !== null ? healthResult.momentumScore + '/10' : '[ N/A - DEX Only Coin ]'}\n`;
|
|
217
|
+
report += `- Price: $${currentPrice} | MA50: ${ma50 ? '$'+ma50.toFixed(4) : 'N/A'} | RSI: ${rsi || 'N/A'}\n\n`;
|
|
218
|
+
|
|
219
|
+
report += `*System Note for LLM: Use this exact data to provide a "Market Summary" and "Suggested Autonomous Actions" in the user's native language. If CEX momentum is N/A, explicitly warn about high risk Degen/Memecoin status. IMPORTANT: Always include a clear disclaimer at the end (translated into the user's native language) stating that this analysis is NOT financial advice (NFA).*`;
|
|
61
220
|
|
|
62
221
|
return report;
|
|
222
|
+
|
|
63
223
|
} catch (error: any) {
|
|
64
|
-
return `Failed to
|
|
224
|
+
return `[Market Intelligence] Failed to aggregate data: ${error.message}`;
|
|
65
225
|
}
|
|
66
226
|
}
|
|
67
227
|
|
|
@@ -69,7 +229,7 @@ export const marketAnalysisToolDefinition = {
|
|
|
69
229
|
type: "function",
|
|
70
230
|
function: {
|
|
71
231
|
name: "analyze_market",
|
|
72
|
-
description: "
|
|
232
|
+
description: "MUST be used whenever the user asks for 'analisis', 'analysis', 'market intelligence', or a deep dive into a token. Fetches live, expert-level Web3 market data including Liquidity Risk, Smart Money Flow (TVL), Holder Concentration, and Momentum.",
|
|
73
233
|
parameters: {
|
|
74
234
|
type: "object",
|
|
75
235
|
properties: {
|
|
@@ -80,10 +240,10 @@ export const marketAnalysisToolDefinition = {
|
|
|
80
240
|
},
|
|
81
241
|
tokenAddressOrSymbol: {
|
|
82
242
|
type: "string",
|
|
83
|
-
description: "The token symbol (e.g. USDC, PEPE) or
|
|
243
|
+
description: "The token symbol (e.g. USDC, PEPE) or exact Contract Address (e.g. 0x...) to analyze.",
|
|
84
244
|
}
|
|
85
245
|
},
|
|
86
|
-
required: ["
|
|
246
|
+
required: ["tokenAddressOrSymbol"],
|
|
87
247
|
},
|
|
88
248
|
},
|
|
89
249
|
};
|