crypto.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { tool } from "ai";
  2. import { z } from "zod";
  3. // =============================================================================
  4. // Helpers
  5. // =============================================================================
  6. function handleFetchError(res: Response, coinId: string) {
  7. if (res.status === 404) {
  8. return { error: `Cryptocurrency not found: ${coinId}` };
  9. }
  10. if (res.status === 429) {
  11. return { error: "CoinGecko rate limit exceeded. Try again in a minute." };
  12. }
  13. return { error: `Failed to fetch crypto data: ${res.statusText}` };
  14. }
  15. function sampleTimeSeries(
  16. prices: [number, number][],
  17. maxPoints: number,
  18. ): Array<{ date: string; price: number }> {
  19. const step = Math.max(1, Math.floor(prices.length / maxPoints));
  20. return prices
  21. .filter((_, i) => i % step === 0)
  22. .map(([timestamp, price]) => ({
  23. date: new Date(timestamp).toLocaleDateString("en-US", {
  24. month: "short",
  25. day: "numeric",
  26. }),
  27. price: Math.round(price * 100) / 100,
  28. }));
  29. }
  30. // =============================================================================
  31. // getCryptoPrice — current market data + 7-day sparkline
  32. // =============================================================================
  33. /**
  34. * Get cryptocurrency market data from CoinGecko.
  35. * Free public API, no API key required.
  36. * https://docs.coingecko.com/reference/introduction
  37. */
  38. export const getCryptoPrice = tool({
  39. description:
  40. "Get current price, market cap, 24h change, and 7-day sparkline for a cryptocurrency. For longer price history (30d, 90d, 365d), use getCryptoPriceHistory instead.",
  41. inputSchema: z.object({
  42. coinId: z
  43. .string()
  44. .describe(
  45. "CoinGecko coin ID (e.g., 'bitcoin', 'ethereum', 'solana', 'dogecoin', 'cardano')",
  46. ),
  47. }),
  48. execute: async ({ coinId }) => {
  49. const url = `https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coinId)}?localization=false&tickers=false&community_data=false&developer_data=false&sparkline=true`;
  50. const res = await fetch(url, {
  51. headers: { Accept: "application/json" },
  52. });
  53. if (!res.ok) return handleFetchError(res, coinId);
  54. const data = (await res.json()) as {
  55. id: string;
  56. symbol: string;
  57. name: string;
  58. market_data: {
  59. current_price: { usd: number };
  60. market_cap: { usd: number };
  61. total_volume: { usd: number };
  62. price_change_percentage_24h: number;
  63. price_change_percentage_7d: number;
  64. price_change_percentage_30d: number;
  65. high_24h: { usd: number };
  66. low_24h: { usd: number };
  67. ath: { usd: number };
  68. ath_date: { usd: string };
  69. circulating_supply: number;
  70. total_supply: number | null;
  71. sparkline_7d: { price: number[] };
  72. };
  73. market_cap_rank: number;
  74. };
  75. const md = data.market_data;
  76. // Convert sparkline (hourly array) to dated points
  77. const now = Date.now();
  78. const sparkline = md.sparkline_7d.price;
  79. const step = Math.max(1, Math.floor(sparkline.length / 14));
  80. const sparklineData = sparkline
  81. .filter((_, i) => i % step === 0)
  82. .map((price, i) => {
  83. const hourIndex = i * step;
  84. const ts = now - (sparkline.length - hourIndex) * 3600_000;
  85. return {
  86. date: new Date(ts).toLocaleDateString("en-US", {
  87. month: "short",
  88. day: "numeric",
  89. }),
  90. price: Math.round(price * 100) / 100,
  91. };
  92. });
  93. return {
  94. id: data.id,
  95. symbol: data.symbol.toUpperCase(),
  96. name: data.name,
  97. rank: data.market_cap_rank,
  98. price: md.current_price.usd,
  99. marketCap: md.market_cap.usd,
  100. volume24h: md.total_volume.usd,
  101. change24h: Math.round(md.price_change_percentage_24h * 100) / 100,
  102. change7d: Math.round(md.price_change_percentage_7d * 100) / 100,
  103. change30d: Math.round(md.price_change_percentage_30d * 100) / 100,
  104. high24h: md.high_24h.usd,
  105. low24h: md.low_24h.usd,
  106. allTimeHigh: md.ath.usd,
  107. allTimeHighDate: md.ath_date.usd,
  108. circulatingSupply: md.circulating_supply,
  109. totalSupply: md.total_supply,
  110. sparkline7d: sparklineData,
  111. };
  112. },
  113. });
  114. // =============================================================================
  115. // getCryptoPriceHistory — flexible date range price history
  116. // =============================================================================
  117. export const getCryptoPriceHistory = tool({
  118. description:
  119. "Get historical price data for a cryptocurrency over a specified number of days (e.g., 30, 90, 365). Returns date-labeled data points suitable for charting.",
  120. inputSchema: z.object({
  121. coinId: z
  122. .string()
  123. .describe("CoinGecko coin ID (e.g., 'bitcoin', 'ethereum', 'solana')"),
  124. days: z
  125. .number()
  126. .int()
  127. .min(1)
  128. .max(365)
  129. .describe("Number of days of history to fetch (e.g., 30, 90, 365)"),
  130. }),
  131. execute: async ({ coinId, days }) => {
  132. const url = `https://api.coingecko.com/api/v3/coins/${encodeURIComponent(coinId)}/market_chart?vs_currency=usd&days=${days}`;
  133. const res = await fetch(url, {
  134. headers: { Accept: "application/json" },
  135. });
  136. if (!res.ok) return handleFetchError(res, coinId);
  137. const data = (await res.json()) as {
  138. prices: [number, number][];
  139. };
  140. const priceHistory = sampleTimeSeries(data.prices, 20);
  141. return {
  142. coinId,
  143. days,
  144. priceHistory,
  145. };
  146. },
  147. });