| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const CG = "https://api.coingecko.com/api/v3"; |
| const FX = "https://open.er-api.com/v6/latest"; |
|
|
| export class ToolError extends Error {} |
|
|
| |
| |
| |
|
|
| const cache = new Map(); |
| const TTL_MS = 60_000; |
|
|
| async function getJSON(url) { |
| const hit = cache.get(url); |
| if (hit && Date.now() - hit.at < TTL_MS) return hit.data; |
|
|
| let res; |
| try { |
| res = await fetch(url, { headers: { accept: "application/json" } }); |
| } catch (e) { |
| |
| |
| throw new ToolError(`network error contacting ${new URL(url).host}: ${e.message}`); |
| } |
|
|
| if (res.status === 429) { |
| throw new ToolError("rate limited by CoinGecko (free tier). Wait ~60s and retry."); |
| } |
| if (!res.ok) throw new ToolError(`upstream returned HTTP ${res.status}`); |
|
|
| const data = await res.json(); |
| cache.set(url, { at: Date.now(), data }); |
| return data; |
| } |
|
|
| const round = (n, dp = 2) => |
| typeof n === "number" && isFinite(n) ? Number(n.toFixed(dp)) : null; |
|
|
| |
| |
| |
|
|
| export const TOOL_SCHEMAS = [ |
| { |
| type: "function", |
| function: { |
| name: "search_coin", |
| description: |
| "Resolve a coin name or ticker symbol to its CoinGecko id. Call this FIRST whenever the user names a coin in words (e.g. 'Bitcoin', 'Solana') and you do not already know its id.", |
| parameters: { |
| type: "object", |
| properties: { |
| query: { |
| type: "string", |
| description: "Coin name or symbol, e.g. 'bitcoin', 'ETH', 'solana'.", |
| }, |
| }, |
| required: ["query"], |
| }, |
| }, |
| }, |
| { |
| type: "function", |
| function: { |
| name: "get_price", |
| description: |
| "Get the current price, 24-hour change and market cap of a coin. Requires a CoinGecko id (use search_coin first if unsure).", |
| parameters: { |
| type: "object", |
| properties: { |
| coin_id: { |
| type: "string", |
| description: "CoinGecko id, e.g. 'bitcoin' (NOT the symbol 'BTC').", |
| }, |
| vs_currency: { |
| type: "string", |
| description: "Quote currency code, lowercase. Defaults to 'usd'.", |
| default: "usd", |
| }, |
| }, |
| required: ["coin_id"], |
| }, |
| }, |
| }, |
| { |
| type: "function", |
| function: { |
| name: "get_market_chart", |
| description: |
| "Get summary statistics of a coin's price over the last N days: open, close, low, high and percent change. Use for trend or 'how has X done' questions.", |
| parameters: { |
| type: "object", |
| properties: { |
| coin_id: { type: "string", description: "CoinGecko id, e.g. 'ethereum'." }, |
| days: { |
| type: "integer", |
| description: "Look-back window in days (1-365). Defaults to 7.", |
| default: 7, |
| }, |
| vs_currency: { |
| type: "string", |
| description: "Quote currency code, lowercase. Defaults to 'usd'.", |
| default: "usd", |
| }, |
| }, |
| required: ["coin_id"], |
| }, |
| }, |
| }, |
| { |
| type: "function", |
| function: { |
| name: "get_coin_info", |
| description: |
| "Get descriptive facts about a coin: full name, symbol, market-cap rank, launch date, categories, all-time high and a short description. Use for 'what is X' questions, not for prices.", |
| parameters: { |
| type: "object", |
| properties: { |
| coin_id: { type: "string", description: "CoinGecko id, e.g. 'cardano'." }, |
| }, |
| required: ["coin_id"], |
| }, |
| }, |
| }, |
| { |
| type: "function", |
| function: { |
| name: "convert_currency", |
| description: |
| "Convert an amount of fiat money from one currency to another using live exchange rates. Use this to express a price in another currency, e.g. USD to TRY.", |
| parameters: { |
| type: "object", |
| properties: { |
| amount: { type: "number", description: "The amount to convert." }, |
| from_currency: { type: "string", description: "Source ISO 4217 code, e.g. 'USD'." }, |
| to_currency: { type: "string", description: "Target ISO 4217 code, e.g. 'TRY'." }, |
| }, |
| required: ["amount", "from_currency", "to_currency"], |
| }, |
| }, |
| }, |
| ]; |
|
|
| |
| |
| |
|
|
| export const TOOL_IMPLS = { |
| async search_coin({ query }) { |
| if (!query) throw new ToolError("missing required argument 'query'"); |
| const d = await getJSON(`${CG}/search?query=${encodeURIComponent(query)}`); |
| const coins = (d.coins || []).slice(0, 4).map((c) => ({ |
| id: c.id, |
| symbol: (c.symbol || "").toUpperCase(), |
| name: c.name, |
| market_cap_rank: c.market_cap_rank ?? null, |
| })); |
| if (!coins.length) throw new ToolError(`no coin found matching '${query}'`); |
| return { query, matches: coins, best_match_id: coins[0].id }; |
| }, |
|
|
| async get_price({ coin_id, vs_currency = "usd" }) { |
| if (!coin_id) throw new ToolError("missing required argument 'coin_id'"); |
| const vs = String(vs_currency).toLowerCase(); |
| const id = String(coin_id).toLowerCase(); |
| const d = await getJSON( |
| `${CG}/simple/price?ids=${encodeURIComponent(id)}&vs_currencies=${encodeURIComponent(vs)}` + |
| `&include_24hr_change=true&include_market_cap=true` |
| ); |
| const row = d[id]; |
| if (!row || row[vs] === undefined) { |
| throw new ToolError( |
| `no price for id '${coin_id}' in '${vs}'. Use search_coin to get the correct id.` |
| ); |
| } |
| return { |
| coin_id: id, |
| vs_currency: vs.toUpperCase(), |
| price: round(row[vs], 6), |
| change_24h_pct: round(row[`${vs}_24h_change`]), |
| market_cap: round(row[`${vs}_market_cap`], 0), |
| }; |
| }, |
|
|
| async get_market_chart({ coin_id, days = 7, vs_currency = "usd" }) { |
| if (!coin_id) throw new ToolError("missing required argument 'coin_id'"); |
| const vs = String(vs_currency).toLowerCase(); |
| const id = String(coin_id).toLowerCase(); |
| const n = Math.min(Math.max(parseInt(days, 10) || 7, 1), 365); |
| const d = await getJSON( |
| `${CG}/coins/${encodeURIComponent(id)}/market_chart?vs_currency=${vs}&days=${n}` |
| ); |
| const prices = (d.prices || []).map((p) => p[1]).filter((v) => isFinite(v)); |
| if (!prices.length) throw new ToolError(`no price history for '${coin_id}'`); |
|
|
| const open = prices[0]; |
| const close = prices[prices.length - 1]; |
| return { |
| coin_id: id, |
| vs_currency: vs.toUpperCase(), |
| days: n, |
| open: round(open, 6), |
| close: round(close, 6), |
| low: round(Math.min(...prices), 6), |
| high: round(Math.max(...prices), 6), |
| change_pct: round(((close - open) / open) * 100), |
| data_points: prices.length, |
| }; |
| }, |
|
|
| async get_coin_info({ coin_id }) { |
| if (!coin_id) throw new ToolError("missing required argument 'coin_id'"); |
| const id = String(coin_id).toLowerCase(); |
| const d = await getJSON( |
| `${CG}/coins/${encodeURIComponent(id)}` + |
| `?localization=false&tickers=false&market_data=true` + |
| `&community_data=false&developer_data=false` |
| ); |
| const desc = (d.description?.en || "").replace(/<[^>]*>/g, "").trim(); |
| return { |
| coin_id: d.id, |
| name: d.name, |
| symbol: (d.symbol || "").toUpperCase(), |
| market_cap_rank: d.market_cap_rank ?? null, |
| genesis_date: d.genesis_date ?? null, |
| categories: (d.categories || []).filter(Boolean).slice(0, 3), |
| all_time_high_usd: round(d.market_data?.ath?.usd, 2), |
| |
| description: desc.length > 280 ? desc.slice(0, 280) + "..." : desc, |
| }; |
| }, |
|
|
| async convert_currency({ amount, from_currency, to_currency }) { |
| const amt = Number(amount); |
| if (!isFinite(amt)) throw new ToolError("'amount' must be a number"); |
| if (!from_currency || !to_currency) { |
| throw new ToolError("both 'from_currency' and 'to_currency' are required"); |
| } |
| const from = String(from_currency).toUpperCase(); |
| const to = String(to_currency).toUpperCase(); |
| if (from === to) { |
| return { amount: amt, from_currency: from, to_currency: to, rate: 1, result: round(amt) }; |
| } |
| const d = await getJSON(`${FX}/${encodeURIComponent(from)}`); |
| if (d.result === "error" || !d.rates) { |
| throw new ToolError(`unknown source currency '${from}'`); |
| } |
| const rate = d.rates[to]; |
| if (rate === undefined) throw new ToolError(`unknown target currency '${to}'`); |
| return { |
| amount: amt, |
| from_currency: from, |
| to_currency: to, |
| rate: round(rate, 6), |
| result: round(amt * rate), |
| rate_date: d.time_last_update_utc || null, |
| }; |
| }, |
| }; |
|
|
| |
| |
| |
| |
| |
| export async function runTool(name, args) { |
| const impl = TOOL_IMPLS[name]; |
| if (!impl) { |
| return { error: `unknown tool '${name}'. Available: ${Object.keys(TOOL_IMPLS).join(", ")}` }; |
| } |
| try { |
| return await impl(args || {}); |
| } catch (e) { |
| return { error: e instanceof ToolError ? e.message : `unexpected failure: ${e.message}` }; |
| } |
| } |
|
|