@elizaos/plugin-coingecko
Version:
CoinGecko plugin for ElizaOS - provides cryptocurrency market data, prices, and on-chain analytics
1,526 lines (1,464 loc) • 81.7 kB
JavaScript
// src/index.ts
import { toV2ActionExample } from "@elizaos/core/v1";
// src/actions/getMarkets.ts
import {
logger as logger2,
ModelType
} from "@elizaos/core";
import axios2 from "axios";
import { z as z2 } from "zod";
// src/environment.ts
import { z } from "zod";
var coingeckoConfigSchema = z.object({
COINGECKO_API_KEY: z.string().nullable(),
COINGECKO_PRO_API_KEY: z.string().nullable()
}).refine((data) => data.COINGECKO_API_KEY || data.COINGECKO_PRO_API_KEY, {
message: "Either COINGECKO_API_KEY or COINGECKO_PRO_API_KEY must be provided"
});
async function validateCoingeckoConfig(runtime) {
const config = {
COINGECKO_API_KEY: runtime.getSetting("COINGECKO_API_KEY"),
COINGECKO_PRO_API_KEY: runtime.getSetting("COINGECKO_PRO_API_KEY")
};
return coingeckoConfigSchema.parse(config);
}
function getApiConfig(config) {
const isPro = !!config.COINGECKO_PRO_API_KEY;
return {
baseUrl: isPro ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3",
apiKey: isPro ? config.COINGECKO_PRO_API_KEY : config.COINGECKO_API_KEY,
headerKey: isPro ? "x-cg-pro-api-key" : "x-cg-demo-api-key"
};
}
// src/providers/categoriesProvider.ts
import {
logger
} from "@elizaos/core";
import axios from "axios";
var CACHE_TTL = 5 * 60;
var MAX_RETRIES = 3;
async function fetchCategories(runtime) {
var _a;
const config = await validateCoingeckoConfig(runtime);
const { baseUrl, apiKey, headerKey } = getApiConfig(config);
const response = await axios.get(
`${baseUrl}/coins/categories/list`,
{
headers: {
accept: "application/json",
[headerKey]: apiKey
},
timeout: 5e3
// 5 second timeout
}
);
if (!((_a = response.data) == null ? void 0 : _a.length)) {
throw new Error("Invalid categories data received");
}
return response.data;
}
async function fetchWithRetry(runtime) {
let lastError = null;
for (let i = 0; i < MAX_RETRIES; i++) {
try {
return await fetchCategories(runtime);
} catch (error) {
lastError = error;
logger.error(`Categories fetch attempt ${i + 1} failed:`, error);
await new Promise((resolve) => setTimeout(resolve, 1e3 * (i + 1)));
}
}
throw lastError || new Error("Failed to fetch categories after multiple attempts");
}
var cache = {};
async function getCategories(runtime) {
try {
if (cache.data && cache.timestamp && Date.now() - cache.timestamp < CACHE_TTL * 1e3) {
return cache.data;
}
const categories = await fetchWithRetry(runtime);
cache.data = categories;
cache.timestamp = Date.now();
return categories;
} catch (error) {
logger.error("Error fetching categories:", error);
throw error;
}
}
function formatCategoriesContext(categories) {
const popularCategories = [
"layer-1",
"defi",
"meme",
"ai-meme-coins",
"artificial-intelligence",
"gaming",
"metaverse"
];
const popular = categories.filter((c) => popularCategories.includes(c.category_id)).map((c) => `${c.name} (${c.category_id})`);
return `
Available cryptocurrency categories:
Popular categories:
${popular.map((c) => `- ${c}`).join("\n")}
Total available categories: ${categories.length}
You can use these category IDs when filtering cryptocurrency market data.
`.trim();
}
var categoriesProvider = {
name: "CATEGORIES_PROVIDER",
// eslint-disable-next-line
get: async (runtime, message, state) => {
try {
const categories = await getCategories(runtime);
return {
text: formatCategoriesContext(categories),
data: {
categories
}
};
} catch (error) {
logger.error("Categories provider error:", error);
return {
text: "Cryptocurrency categories are temporarily unavailable. Please try again later."
};
}
}
};
async function getCategoriesData(runtime) {
return getCategories(runtime);
}
// src/templates/markets.ts
var getMarketsTemplate = `
Extract the following parameters for market listing:
- **vs_currency** (string): Target currency for price data (default: "usd")
- **category** (string, optional): Specific category ID from the available categories
- **per_page** (number): Number of results to return (1-250, default: 20)
- **order** (string): Sort order for results, one of:
- market_cap_desc: Highest market cap first
- market_cap_asc: Lowest market cap first
- volume_desc: Highest volume first
- volume_asc: Lowest volume first
Available Categories:
{{categories}}
Provide the values in the following JSON format:
\`\`\`json
{
"vs_currency": "<currency>",
"category": "<category_id>",
"per_page": <number>,
"order": "<sort_order>",
"page": 1,
"sparkline": false
}
\`\`\`
Example request: "Show me the top 10 gaming cryptocurrencies"
Example response:
\`\`\`json
{
"vs_currency": "usd",
"category": "gaming",
"per_page": 10,
"order": "market_cap_desc",
"page": 1,
"sparkline": false
}
\`\`\`
Example request: "What are the best performing coins by volume?"
Example response:
\`\`\`json
{
"vs_currency": "usd",
"per_page": 20,
"order": "volume_desc",
"page": 1,
"sparkline": false
}
\`\`\`
Here are the recent user messages for context:
{{recentMessages}}
Based on the conversation above, if the request is for a market listing/ranking, extract the appropriate parameters and respond with a JSON object. If the request is for specific coins only, respond with null.`;
// src/actions/getMarkets.ts
function formatCategory(category, categories) {
if (!category) return void 0;
const normalizedInput = category.toLowerCase().trim();
const exactMatch = categories.find((c) => c.category_id === normalizedInput);
if (exactMatch) {
return exactMatch.category_id;
}
const nameMatch = categories.find(
(c) => c.name.toLowerCase() === normalizedInput || c.name.toLowerCase().replace(/[^a-z0-9]+/g, "-") === normalizedInput
);
if (nameMatch) {
return nameMatch.category_id;
}
const partialMatch = categories.find(
(c) => c.name.toLowerCase().includes(normalizedInput) || c.category_id.includes(normalizedInput)
);
if (partialMatch) {
return partialMatch.category_id;
}
return void 0;
}
var GetMarketsSchema = z2.object({
vs_currency: z2.string().default("usd"),
category: z2.string().optional(),
order: z2.enum(["market_cap_desc", "market_cap_asc", "volume_desc", "volume_asc"]).default("market_cap_desc"),
per_page: z2.number().min(1).max(250).default(20),
page: z2.number().min(1).default(1),
sparkline: z2.boolean().default(false)
});
var isGetMarketsContent = (obj) => {
return GetMarketsSchema.safeParse(obj).success;
};
var getMarkets_default = {
name: "GET_MARKETS",
similes: [
"MARKET_OVERVIEW",
"TOP_RANKINGS",
"MARKET_LEADERBOARD",
"CRYPTO_RANKINGS",
"BEST_PERFORMING_COINS",
"TOP_MARKET_CAPS"
],
// eslint-disable-next-line
validate: async (runtime, _message) => {
await validateCoingeckoConfig(runtime);
return true;
},
// Comprehensive endpoint for market rankings, supports up to 250 coins per request
description: "Get ranked list of top cryptocurrencies sorted by market metrics (without specifying coins)",
handler: async (runtime, message, state, _options, callback) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
logger2.log("Starting CoinGecko GET_MARKETS handler...");
let currentState = state;
if (!currentState) {
currentState = await runtime.composeState(message);
} else {
currentState = await runtime.composeState(message);
}
try {
const config = await validateCoingeckoConfig(runtime);
const { baseUrl, apiKey, headerKey } = getApiConfig(config);
const categories = await getCategoriesData(runtime);
const marketsContext = getMarketsTemplate.replace(
"{{categories}}",
categories.map((c) => `- ${c.name} (ID: ${c.category_id})`).join("\n")
).replace(
"{{recentMessages}}",
currentState.recentMessagesFormatted || ""
);
const result = await runtime.useModel(ModelType.TEXT_SMALL, {
prompt: marketsContext,
schema: GetMarketsSchema
});
if (!isGetMarketsContent(result)) {
logger2.error("Invalid market data format received");
return false;
}
const content = result;
logger2.log("Content from template:", content);
if (!content) {
return false;
}
const formattedCategory = formatCategory(content.category, categories);
if (content.category && !formattedCategory) {
throw new Error(
`Invalid category: ${content.category}. Please choose from the available categories.`
);
}
logger2.log("Making API request with params:", {
url: `${baseUrl}/coins/markets`,
category: formattedCategory,
vs_currency: content.vs_currency,
order: content.order,
per_page: content.per_page,
page: content.page
});
const response = await axios2.get(
`${baseUrl}/coins/markets`,
{
headers: {
accept: "application/json",
[headerKey]: apiKey
},
params: {
vs_currency: content.vs_currency,
category: formattedCategory,
order: content.order,
per_page: content.per_page,
page: content.page,
sparkline: content.sparkline
}
}
);
if (!((_a = response.data) == null ? void 0 : _a.length)) {
throw new Error("No market data received from CoinGecko API");
}
const formattedData = response.data.map((coin) => ({
name: coin.name,
symbol: coin.symbol.toUpperCase(),
marketCapRank: coin.market_cap_rank,
currentPrice: coin.current_price,
priceChange24h: coin.price_change_24h,
priceChangePercentage24h: coin.price_change_percentage_24h,
marketCap: coin.market_cap,
volume24h: coin.total_volume,
high24h: coin.high_24h,
low24h: coin.low_24h,
circulatingSupply: coin.circulating_supply,
totalSupply: coin.total_supply,
maxSupply: coin.max_supply,
lastUpdated: coin.last_updated
}));
const categoryDisplay = content.category ? `${((_b = categories.find((c) => c.category_id === formattedCategory)) == null ? void 0 : _b.name.toUpperCase()) || content.category.toUpperCase()} ` : "";
const responseText = [
`Top ${formattedData.length} ${categoryDisplay}Cryptocurrencies by ${content.order === "volume_desc" || content.order === "volume_asc" ? "Volume" : "Market Cap"}:`,
...formattedData.map(
(coin, index) => `${index + 1}. ${coin.name} (${coin.symbol}) | $${coin.currentPrice.toLocaleString()} | ${coin.priceChangePercentage24h.toFixed(2)}% | MCap: $${(coin.marketCap / 1e9).toFixed(2)}B`
)
].join("\n");
logger2.success("Market data retrieved successfully!");
if (callback) {
callback({
text: responseText,
content: {
markets: formattedData,
params: {
vs_currency: content.vs_currency,
category: content.category,
order: content.order,
per_page: content.per_page,
page: content.page
},
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
});
}
return true;
} catch (error) {
logger2.error("Error in GET_MARKETS handler:", error);
let errorMessage;
if (((_c = error.response) == null ? void 0 : _c.status) === 429) {
errorMessage = "Rate limit exceeded. Please try again later.";
} else if (((_d = error.response) == null ? void 0 : _d.status) === 403) {
errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data.";
} else if (((_e = error.response) == null ? void 0 : _e.status) === 400) {
errorMessage = "Invalid request parameters. Please check your input.";
} else {
errorMessage = `Error fetching market data: ${error.message}`;
}
if (callback) {
callback({
text: errorMessage,
error: {
message: error.message,
statusCode: (_f = error.response) == null ? void 0 : _f.status,
params: (_g = error.config) == null ? void 0 : _g.params,
requiresProPlan: ((_h = error.response) == null ? void 0 : _h.status) === 403
}
});
}
return false;
}
},
examples: [
[
{
name: "{{user1}}",
content: {
text: "Show me the top cryptocurrencies by market cap"
}
},
{
name: "{{agent}}",
content: {
text: "I'll fetch the current market data for top cryptocurrencies.",
action: "GET_MARKETS"
}
},
{
name: "{{agent}}",
content: {
text: "Here are the top cryptocurrencies:\n1. Bitcoin (BTC) | $45,000 | +2.5% | MCap: $870.5B\n{{dynamic}}"
}
}
]
]
};
// src/actions/getPrice.ts
import {
logger as logger4,
ModelType as ModelType2
} from "@elizaos/core";
import axios4 from "axios";
import { z as z3 } from "zod";
// src/providers/coinsProvider.ts
import {
logger as logger3
} from "@elizaos/core";
import axios3 from "axios";
var CACHE_TTL2 = 5 * 60;
var MAX_RETRIES2 = 3;
async function fetchCoins(runtime, includePlatform = false) {
var _a;
const config = await validateCoingeckoConfig(runtime);
const { baseUrl, apiKey, headerKey } = getApiConfig(config);
const response = await axios3.get(`${baseUrl}/coins/list`, {
params: {
include_platform: includePlatform
},
headers: {
accept: "application/json",
[headerKey]: apiKey
},
timeout: 5e3
// 5 second timeout
});
if (!((_a = response.data) == null ? void 0 : _a.length)) {
throw new Error("Invalid coins data received");
}
return response.data;
}
async function fetchWithRetry2(runtime, includePlatform = false) {
let lastError = null;
for (let i = 0; i < MAX_RETRIES2; i++) {
try {
return await fetchCoins(runtime, includePlatform);
} catch (error) {
lastError = error;
logger3.error(`Coins fetch attempt ${i + 1} failed:`, error);
await new Promise((resolve) => setTimeout(resolve, 1e3 * (i + 1)));
}
}
throw lastError || new Error("Failed to fetch coins after multiple attempts");
}
var cache2 = {};
async function getCoins(runtime, includePlatform = false) {
try {
if (cache2.data && cache2.timestamp && Date.now() - cache2.timestamp < CACHE_TTL2 * 1e3) {
return cache2.data;
}
const coins = await fetchWithRetry2(runtime, includePlatform);
cache2.data = coins;
cache2.timestamp = Date.now();
return coins;
} catch (error) {
logger3.error("Error fetching coins:", error);
throw error;
}
}
function formatCoinsContext(coins) {
const popularCoins = [
"bitcoin",
"ethereum",
"binancecoin",
"ripple",
"cardano",
"solana",
"polkadot",
"dogecoin"
];
const popular = coins.filter((c) => popularCoins.includes(c.id)).map((c) => `${c.name} (${c.symbol.toUpperCase()}) - ID: ${c.id}`);
return `
Available cryptocurrencies:
Popular coins:
${popular.map((c) => `- ${c}`).join("\n")}
Total available coins: ${coins.length}
You can use these coin IDs when querying specific cryptocurrency data.
`.trim();
}
var coinsProvider = {
name: "COINS_PROVIDER",
// eslint-disable-next-line
get: async (runtime, message, state) => {
try {
const coins = await getCoins(runtime);
return {
text: formatCoinsContext(coins),
data: {
coins
}
};
} catch (error) {
logger3.error("Coins provider error:", error);
return {
text: "Cryptocurrency list is temporarily unavailable. Please try again later."
};
}
}
};
async function getCoinsData(runtime, includePlatform = false) {
return getCoins(runtime, includePlatform);
}
// src/templates/price.ts
var getPriceTemplate = `
Extract the following parameters for cryptocurrency price data:
- **coinIds** (string | string[]): The ID(s) of the cryptocurrency/cryptocurrencies to get prices for (e.g., "bitcoin" or ["bitcoin", "ethereum"])
- **currency** (string | string[]): The currency/currencies to display prices in (e.g., "usd" or ["usd", "eur", "jpy"]) - defaults to ["usd"]
- **include_market_cap** (boolean): Whether to include market cap data - defaults to false
- **include_24hr_vol** (boolean): Whether to include 24h volume data - defaults to false
- **include_24hr_change** (boolean): Whether to include 24h price change data - defaults to false
- **include_last_updated_at** (boolean): Whether to include last update timestamp - defaults to false
Provide the values in the following JSON format:
\`\`\`json
{
"coinIds": "bitcoin",
"currency": ["usd"],
"include_market_cap": false,
"include_24hr_vol": false,
"include_24hr_change": false,
"include_last_updated_at": false
}
\`\`\`
Example request: "What's the current price of Bitcoin?"
Example response:
\`\`\`json
{
"coinIds": "bitcoin",
"currency": ["usd"],
"include_market_cap": false,
"include_24hr_vol": false,
"include_24hr_change": false,
"include_last_updated_at": false
}
\`\`\`
Example request: "Show me ETH price and market cap in EUR with last update time"
Example response:
\`\`\`json
{
"coinIds": "ethereum",
"currency": ["eur"],
"include_market_cap": true,
"include_24hr_vol": false,
"include_24hr_change": false,
"include_last_updated_at": true
}
\`\`\`
Example request: "What's the current price of Bitcoin in USD, JPY and EUR?"
Example response:
\`\`\`json
{
"coinIds": "bitcoin",
"currency": ["usd", "jpy", "eur"],
"include_market_cap": false,
"include_24hr_vol": false,
"include_24hr_change": false,
"include_last_updated_at": false
}
\`\`\`
Here are the recent user messages for context:
{{recentMessages}}
Based on the conversation above, if the request is for cryptocurrency price data, extract the appropriate parameters and respond with a JSON object. If the request is not related to price data, respond with null.`;
// src/actions/getPrice.ts
var GetPriceSchema = z3.object({
coinIds: z3.union([z3.string(), z3.array(z3.string())]),
currency: z3.union([z3.string(), z3.array(z3.string())]).default(["usd"]),
include_market_cap: z3.boolean().default(false),
include_24hr_vol: z3.boolean().default(false),
include_24hr_change: z3.boolean().default(false),
include_last_updated_at: z3.boolean().default(false)
});
var isGetPriceContent = (obj) => {
return GetPriceSchema.safeParse(obj).success;
};
function formatCoinIds(input) {
if (Array.isArray(input)) {
return input.join(",");
}
return input;
}
var getPrice_default = {
name: "GET_PRICE",
similes: [
"COIN_PRICE_CHECK",
"SPECIFIC_COINS_PRICE",
"COIN_PRICE_LOOKUP",
"SELECTED_COINS_PRICE",
"PRICE_DETAILS",
"COIN_PRICE_DATA"
],
// eslint-disable-next-line
validate: async (runtime, _message) => {
await validateCoingeckoConfig(runtime);
return true;
},
description: "Get price and basic market data for one or more specific cryptocurrencies (by name/symbol)",
handler: async (runtime, message, state, _options, callback) => {
var _a, _b, _c, _d, _e, _f;
logger4.log("Starting CoinGecko GET_PRICE handler...");
let currentState = state;
if (!currentState) {
currentState = await runtime.composeState(message);
} else {
currentState = await runtime.composeState(message);
}
try {
logger4.log("Composing price context...");
const priceContext = getPriceTemplate.replace(
"{{recentMessages}}",
currentState.recentMessagesFormatted || ""
);
logger4.log("Generating content from template...");
const result = await runtime.useModel(ModelType2.TEXT_SMALL, {
prompt: priceContext,
schema: GetPriceSchema
});
if (!isGetPriceContent(result)) {
logger4.error("Invalid price request format");
return false;
}
const content = result;
logger4.log("Generated content:", content);
const currencies = Array.isArray(content.currency) ? content.currency : [content.currency];
const vs_currencies = currencies.join(",").toLowerCase();
const coinIds = formatCoinIds(content.coinIds);
logger4.log("Formatted request parameters:", { coinIds, vs_currencies });
const config = await validateCoingeckoConfig(runtime);
const { baseUrl, apiKey, headerKey } = getApiConfig(config);
logger4.log(`Fetching prices for ${coinIds} in ${vs_currencies}...`);
logger4.log("API request URL:", `${baseUrl}/simple/price`);
logger4.log("API request params:", {
ids: coinIds,
vs_currencies,
include_market_cap: content.include_market_cap,
include_24hr_vol: content.include_24hr_vol,
include_24hr_change: content.include_24hr_change,
include_last_updated_at: content.include_last_updated_at
});
const response = await axios4.get(
`${baseUrl}/simple/price`,
{
params: {
ids: coinIds,
vs_currencies,
include_market_cap: content.include_market_cap,
include_24hr_vol: content.include_24hr_vol,
include_24hr_change: content.include_24hr_change,
include_last_updated_at: content.include_last_updated_at
},
headers: {
accept: "application/json",
[headerKey]: apiKey
}
}
);
if (Object.keys(response.data).length === 0) {
throw new Error(
"No price data available for the specified coins and currency"
);
}
const coins = await getCoinsData(runtime);
const formattedResponse = Object.entries(response.data).map(([coinId, data]) => {
const coin = coins.find((c) => c.id === coinId);
const coinName = coin ? `${coin.name} (${coin.symbol.toUpperCase()})` : coinId;
const parts = [`${coinName}:`];
for (const currency of currencies) {
const upperCurrency = currency.toUpperCase();
if (data[currency]) {
parts.push(
` ${upperCurrency}: ${data[currency].toLocaleString(
void 0,
{
style: "currency",
currency
}
)}`
);
}
if (content.include_market_cap) {
const marketCap = data[`${currency}_market_cap`];
if (marketCap !== void 0) {
parts.push(
` Market Cap (${upperCurrency}): ${marketCap.toLocaleString(
void 0,
{
style: "currency",
currency,
maximumFractionDigits: 0
}
)}`
);
}
}
if (content.include_24hr_vol) {
const volume = data[`${currency}_24h_vol`];
if (volume !== void 0) {
parts.push(
` 24h Volume (${upperCurrency}): ${volume.toLocaleString(
void 0,
{
style: "currency",
currency,
maximumFractionDigits: 0
}
)}`
);
}
}
if (content.include_24hr_change) {
const change = data[`${currency}_24h_change`];
if (change !== void 0) {
const changePrefix = change >= 0 ? "+" : "";
parts.push(
` 24h Change (${upperCurrency}): ${changePrefix}${change.toFixed(2)}%`
);
}
}
}
if (content.include_last_updated_at && data.last_updated_at) {
const lastUpdated = new Date(
data.last_updated_at * 1e3
).toLocaleString();
parts.push(` Last Updated: ${lastUpdated}`);
}
return parts.join("\n");
}).filter(Boolean);
if (formattedResponse.length === 0) {
throw new Error("Failed to format price data for the specified coins");
}
const responseText = formattedResponse.join("\n\n");
logger4.success("Price data retrieved successfully!");
if (callback) {
callback({
text: responseText,
content: {
prices: Object.entries(response.data).reduce(
(acc, [coinId, data]) => {
const coinPrices = currencies.reduce(
(currencyAcc, currency) => {
const currencyData = {
price: data[currency],
marketCap: data[`${currency}_market_cap`],
volume24h: data[`${currency}_24h_vol`],
change24h: data[`${currency}_24h_change`],
lastUpdated: data.last_updated_at
};
Object.assign(currencyAcc, { [currency]: currencyData });
return currencyAcc;
},
{}
);
Object.assign(acc, { [coinId]: coinPrices });
return acc;
},
{}
),
params: {
currencies: currencies.map((c) => c.toUpperCase()),
include_market_cap: content.include_market_cap,
include_24hr_vol: content.include_24hr_vol,
include_24hr_change: content.include_24hr_change,
include_last_updated_at: content.include_last_updated_at
}
}
});
}
return true;
} catch (error) {
logger4.error("Error in GET_PRICE handler:", error);
let errorMessage;
if (((_a = error.response) == null ? void 0 : _a.status) === 429) {
errorMessage = "Rate limit exceeded. Please try again later.";
} else if (((_b = error.response) == null ? void 0 : _b.status) === 403) {
errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data.";
} else if (((_c = error.response) == null ? void 0 : _c.status) === 400) {
errorMessage = "Invalid request parameters. Please check your input.";
} else {
errorMessage = error.message || "Failed to fetch price data";
}
if (callback) {
callback({
text: errorMessage,
content: {
error: error.message,
statusCode: (_d = error.response) == null ? void 0 : _d.status,
params: (_e = error.config) == null ? void 0 : _e.params,
requiresProPlan: ((_f = error.response) == null ? void 0 : _f.status) === 403
}
});
}
return false;
}
},
examples: [
[
{
name: "{{user1}}",
content: {
text: "What's the current price of Bitcoin?"
}
},
{
name: "{{agent}}",
content: {
text: "I'll check the current Bitcoin price for you.",
action: "GET_PRICE"
}
},
{
name: "{{agent}}",
content: {
text: "The current price of Bitcoin is {{dynamic}} USD"
}
}
],
[
{
name: "{{user1}}",
content: {
text: "Check ETH and BTC prices in EUR with market cap"
}
},
{
name: "{{agent}}",
content: {
text: "I'll check the current prices with market cap data.",
action: "GET_PRICE"
}
},
{
name: "{{agent}}",
content: {
text: "Bitcoin: EUR {{dynamic}} | Market Cap: \u20AC{{dynamic}}\nEthereum: EUR {{dynamic}} | Market Cap: \u20AC{{dynamic}}"
}
}
]
]
};
// src/actions/getPricePerAddress.ts
import {
logger as logger5,
ModelType as ModelType3
} from "@elizaos/core";
import axios5 from "axios";
import { z as z4 } from "zod";
// src/templates/priceAddress.ts
var getPriceByAddressTemplate = `
Extract the following parameters for token price data:
- **chainId** (string): The blockchain network ID (e.g., "ethereum", "polygon", "binance-smart-chain")
- **tokenAddress** (string): The contract address of the token
- **include_market_cap** (boolean): Whether to include market cap data - defaults to true
Normalize chain IDs to lowercase names: ethereum, polygon, binance-smart-chain, avalanche, fantom, arbitrum, optimism, etc.
Token address should be the complete address string, maintaining its original case.
Provide the values in the following JSON format:
\`\`\`json
{
"chainId": "ethereum",
"tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"include_market_cap": true
}
\`\`\`
Example request: "What's the price of USDC on Ethereum? Address: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
Example response:
\`\`\`json
{
"chainId": "ethereum",
"tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"include_market_cap": true
}
\`\`\`
Example request: "Check the price for this token on Polygon: 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
Example response:
\`\`\`json
{
"chainId": "polygon",
"tokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
"include_market_cap": true
}
\`\`\`
Example request: "Get price for BONK token on Solana with address HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC"
Example response:
\`\`\`json
{
"chainId": "solana",
"tokenAddress": "HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC"
}
\`\`\`
Here are the recent user messages for context:
{{recentMessages}}
Based on the conversation above, use last question made and if the request is for token price data and includes both a chain and address, extract the appropriate parameters and respond with a JSON object. If the request is not related to token price data or missing required information, respond with null.`;
// src/actions/getPricePerAddress.ts
var GetTokenPriceSchema = z4.object({
chainId: z4.string(),
tokenAddress: z4.string()
});
var isGetTokenPriceContent = (obj) => {
return GetTokenPriceSchema.safeParse(obj).success;
};
var getPricePerAddress_default = {
name: "GET_TOKEN_PRICE_BY_ADDRESS",
similes: [
"FETCH_TOKEN_PRICE_BY_ADDRESS",
"CHECK_TOKEN_PRICE_BY_ADDRESS",
"LOOKUP_TOKEN_BY_ADDRESS"
],
// eslint-disable-next-line
validate: async (runtime, _message) => {
await validateCoingeckoConfig(runtime);
return true;
},
description: "Get the current USD price for a token using its blockchain address",
handler: async (runtime, message, state, _options, callback) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
logger5.log("Starting GET_TOKEN_PRICE_BY_ADDRESS handler...");
let currentState = state;
if (!currentState) {
currentState = await runtime.composeState(message);
} else {
currentState = await runtime.composeState(message);
}
try {
logger5.log("Composing token price context...");
const context = getPriceByAddressTemplate.replace(
"{{recentMessages}}",
currentState.recentMessagesFormatted || ""
);
logger5.log("Generating content from template...");
const result = await runtime.useModel(ModelType3.TEXT_SMALL, {
prompt: context,
schema: GetTokenPriceSchema
});
if (!isGetTokenPriceContent(result)) {
logger5.error("Invalid token price request format");
return false;
}
const content = result;
logger5.log("Generated content:", content);
const config = await validateCoingeckoConfig(runtime);
const { baseUrl, apiKey, headerKey } = getApiConfig(config);
logger5.log("Fetching token data...");
const response = await axios5.get(
`${baseUrl}/coins/${content.chainId}/contract/${content.tokenAddress}`,
{
headers: {
accept: "application/json",
[headerKey]: apiKey
}
}
);
const tokenData = response.data;
if (!((_b = (_a = tokenData.market_data) == null ? void 0 : _a.current_price) == null ? void 0 : _b.usd)) {
throw new Error(
`No price data available for token ${content.tokenAddress} on ${content.chainId}`
);
}
const parts = [
`${tokenData.name} (${tokenData.symbol.toUpperCase()})`,
`Address: ${content.tokenAddress}`,
`Chain: ${content.chainId}`,
`Price: $${tokenData.market_data.current_price.usd.toFixed(6)} USD`
];
if ((_c = tokenData.market_data.market_cap) == null ? void 0 : _c.usd) {
parts.push(
`Market Cap: $${tokenData.market_data.market_cap.usd.toLocaleString()} USD`
);
}
const responseText = parts.join("\n");
logger5.success("Token price data retrieved successfully!");
if (callback) {
callback({
text: responseText,
content: {
token: {
name: tokenData.name,
symbol: tokenData.symbol,
address: content.tokenAddress,
chain: content.chainId,
price: tokenData.market_data.current_price.usd,
marketCap: (_d = tokenData.market_data.market_cap) == null ? void 0 : _d.usd
}
}
});
}
return true;
} catch (error) {
logger5.error("Error in GET_TOKEN_PRICE_BY_ADDRESS handler:", error);
let errorMessage;
if (((_e = error.response) == null ? void 0 : _e.status) === 429) {
errorMessage = "Rate limit exceeded. Please try again later.";
} else if (((_f = error.response) == null ? void 0 : _f.status) === 403) {
errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data.";
} else if (((_g = error.response) == null ? void 0 : _g.status) === 400) {
errorMessage = "Invalid request parameters. Please check your input.";
} else {
errorMessage = "Failed to fetch token price. Please try again later.";
}
if (callback) {
callback({
text: errorMessage,
content: {
error: error.message,
statusCode: (_h = error.response) == null ? void 0 : _h.status,
requiresProPlan: ((_i = error.response) == null ? void 0 : _i.status) === 403
}
});
}
return false;
}
},
examples: [
[
{
name: "{{user1}}",
content: {
text: "What's the price of the USDC token on Ethereum? The address is 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
}
},
{
name: "{{agent}}",
content: {
text: "I'll check the USDC token price for you.",
action: "GET_TOKEN_PRICE_BY_ADDRESS"
}
},
{
name: "{{agent}}",
content: {
text: "USD Coin (USDC)\nAddress: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\nChain: ethereum\nPrice: {{dynamic}} USD\nMarket Cap: ${{dynamic}} USD"
}
}
]
]
};
// src/actions/getTopGainersLosers.ts
import {
logger as logger6,
ModelType as ModelType4
} from "@elizaos/core";
import axios6 from "axios";
import { z as z5 } from "zod";
// src/templates/gainersLosers.ts
var getTopGainersLosersTemplate = `
Extract the following parameters for top gainers and losers data:
- **vs_currency** (string): The target currency to display prices in (e.g., "usd", "eur") - defaults to "usd"
- **duration** (string): Time range for price changes - one of "24h", "7d", "14d", "30d", "60d", "1y" - defaults to "24h"
- **top_coins** (string): Filter by market cap ranking (e.g., "100", "1000") - defaults to "1000"
Provide the values in the following JSON format:
\`\`\`json
{
"vs_currency": "usd",
"duration": "24h",
"top_coins": "1000"
}
\`\`\`
Example request: "Show me the biggest gainers and losers today"
Example response:
\`\`\`json
{
"vs_currency": "usd",
"duration": "24h",
"top_coins": "1000"
}
\`\`\`
Example request: "What are the top movers in EUR for the past week?"
Example response:
\`\`\`json
{
"vs_currency": "eur",
"duration": "7d",
"top_coins": "300"
}
\`\`\`
Example request: "Show me monthly performance of top 100 coins"
Example response:
\`\`\`json
{
"vs_currency": "usd",
"duration": "30d",
"top_coins": "100"
}
\`\`\`
Here are the recent user messages for context:
{{recentMessages}}
Based on the conversation above, if the request is for top gainers and losers data, extract the appropriate parameters and respond with a JSON object. If the request is not related to top movers data, respond with null.`;
// src/actions/getTopGainersLosers.ts
var DurationEnum = z5.enum(["1h", "24h", "7d", "14d", "30d", "60d", "1y"]);
var GetTopGainersLosersSchema = z5.object({
vs_currency: z5.string().default("usd"),
duration: DurationEnum.default("24h"),
top_coins: z5.string().default("1000")
});
var isGetTopGainersLosersContent = (obj) => {
return GetTopGainersLosersSchema.safeParse(obj).success;
};
var getTopGainersLosers_default = {
name: "GET_TOP_GAINERS_LOSERS",
similes: [
"TOP_MOVERS",
"BIGGEST_GAINERS",
"BIGGEST_LOSERS",
"PRICE_CHANGES",
"BEST_WORST_PERFORMERS"
],
// eslint-disable-next-line
validate: async (runtime, _message) => {
await validateCoingeckoConfig(runtime);
return true;
},
description: "Get list of top gaining and losing cryptocurrencies by price change",
handler: async (runtime, message, state, _options, callback) => {
var _a, _b, _c, _d, _e, _f;
logger6.log("Starting CoinGecko GET_TOP_GAINERS_LOSERS handler...");
let currentState = state;
if (!currentState) {
currentState = await runtime.composeState(message);
} else {
currentState = await runtime.composeState(message);
}
try {
logger6.log("Composing gainers/losers context...");
const context = getTopGainersLosersTemplate.replace(
"{{recentMessages}}",
currentState.recentMessagesFormatted || ""
);
logger6.log("Generating content from template...");
const result = await runtime.useModel(ModelType4.TEXT_LARGE, {
prompt: context,
schema: GetTopGainersLosersSchema
});
if (!isGetTopGainersLosersContent(result)) {
logger6.error("Invalid gainers/losers request format");
return false;
}
const content = result;
logger6.log("Generated content:", content);
const config = await validateCoingeckoConfig(runtime);
const { baseUrl, apiKey, headerKey } = getApiConfig(config);
logger6.log("Fetching top gainers/losers data...");
logger6.log("API request params:", {
vs_currency: content.vs_currency,
duration: content.duration,
top_coins: content.top_coins
});
const response = await axios6.get(
`${baseUrl}/coins/top_gainers_losers`,
{
headers: {
accept: "application/json",
[headerKey]: apiKey
},
params: {
vs_currency: content.vs_currency,
duration: content.duration,
top_coins: content.top_coins
}
}
);
if (!response.data) {
throw new Error("No data received from CoinGecko API");
}
const responseText = [
"Top Gainers:",
...response.data.top_gainers.map((coin, index) => {
const changeKey = `usd_${content.duration}_change`;
const change = coin[changeKey];
return `${index + 1}. ${coin.name} (${coin.symbol.toUpperCase()}) | $${coin.usd.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 8 })} | ${change >= 0 ? "+" : ""}${change.toFixed(2)}%${coin.market_cap_rank ? ` | Rank #${coin.market_cap_rank}` : ""}`;
}),
"",
"Top Losers:",
...response.data.top_losers.map((coin, index) => {
const changeKey = `usd_${content.duration}_change`;
const change = coin[changeKey];
return `${index + 1}. ${coin.name} (${coin.symbol.toUpperCase()}) | $${coin.usd.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 8 })} | ${change >= 0 ? "+" : ""}${change.toFixed(2)}%${coin.market_cap_rank ? ` | Rank #${coin.market_cap_rank}` : ""}`;
})
].join("\n");
if (callback) {
callback({
text: responseText,
content: {
data: response.data,
params: {
vs_currency: content.vs_currency,
duration: content.duration,
top_coins: content.top_coins
}
}
});
}
return true;
} catch (error) {
logger6.error("Error in GET_TOP_GAINERS_LOSERS handler:", error);
let errorMessage;
if (((_a = error.response) == null ? void 0 : _a.status) === 429) {
errorMessage = "Rate limit exceeded. Please try again later.";
} else if (((_b = error.response) == null ? void 0 : _b.status) === 403) {
errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data.";
} else if (((_c = error.response) == null ? void 0 : _c.status) === 400) {
errorMessage = "Invalid request parameters. Please check your input.";
} else {
errorMessage = `Error fetching top gainers/losers data: ${error.message}`;
}
if (callback) {
callback({
text: errorMessage,
content: {
error: error.message,
statusCode: (_d = error.response) == null ? void 0 : _d.status,
params: (_e = error.config) == null ? void 0 : _e.params,
requiresProPlan: ((_f = error.response) == null ? void 0 : _f.status) === 403
}
});
}
return false;
}
},
examples: [
[
{
name: "{{user1}}",
content: {
text: "What are the top gaining and losing cryptocurrencies?"
}
},
{
name: "{{agent}}",
content: {
text: "I'll check the top gainers and losers for you.",
action: "GET_TOP_GAINERS_LOSERS"
}
},
{
name: "{{agent}}",
content: {
text: "Here are the top gainers and losers:\nTop Gainers:\n1. Bitcoin (BTC) | $45,000 | +5.2% | Rank #1\n{{dynamic}}"
}
}
],
[
{
name: "{{user1}}",
content: {
text: "Show me the best and worst performing crypto today"
}
},
{
name: "{{agent}}",
content: {
text: "I'll fetch the current top movers in the crypto market.",
action: "GET_TOP_GAINERS_LOSERS"
}
},
{
name: "{{agent}}",
content: {
text: "Here are today's best and worst performers:\n{{dynamic}}"
}
}
]
]
};
// src/actions/getTrending.ts
import {
logger as logger7,
ModelType as ModelType5
} from "@elizaos/core";
import axios7 from "axios";
import { z as z6 } from "zod";
// src/templates/trending.ts
var getTrendingTemplate = `
Extract the following parameters for trending data:
- **include_nfts** (boolean): Whether to include NFTs in the response (default: true)
- **include_categories** (boolean): Whether to include categories in the response (default: true)
Provide the values in the following JSON format:
\`\`\`json
{
"include_nfts": true,
"include_categories": true
}
\`\`\`
Example request: "What's trending in crypto?"
Example response:
\`\`\`json
{
"include_nfts": true,
"include_categories": true
}
\`\`\`
Example request: "Show me trending coins only"
Example response:
\`\`\`json
{
"include_nfts": false,
"include_categories": false
}
\`\`\`
Here are the recent user messages for context:
{{recentMessages}}
Based on the conversation above, if the request is for trending market data, extract the appropriate parameters and respond with a JSON object. If the request is not related to trending data, respond with null.`;
// src/actions/getTrending.ts
var GetTrendingSchema = z6.object({
include_nfts: z6.boolean().default(true),
include_categories: z6.boolean().default(true)
});
var isGetTrendingContent = (obj) => {
return GetTrendingSchema.safeParse(obj).success;
};
var getTrending_default = {
name: "GET_TRENDING",
similes: [
"TRENDING_COINS",
"TRENDING_CRYPTO",
"HOT_COINS",
"POPULAR_COINS",
"TRENDING_SEARCH"
],
// eslint-disable-next-line
validate: async (runtime, _message) => {
await validateCoingeckoConfig(runtime);
return true;
},
description: "Get list of trending cryptocurrencies, NFTs, and categories from CoinGecko",
handler: async (runtime, message, state, _options, callback) => {
var _a, _b;
logger7.log("Starting CoinGecko GET_TRENDING handler...");
let currentState = state;
if (!currentState) {
currentState = await runtime.composeState(message);
} else {
currentState = await runtime.composeState(message);
}
try {
logger7.log("Composing trending context...");
const trendingContext = getTrendingTemplate.replace(
"{{recentMessages}}",
currentState.recentMessagesFormatted || ""
);
const result = await runtime.useModel(ModelType5.TEXT_LARGE, {
prompt: trendingContext,
schema: GetTrendingSchema
});
if (!isGetTrendingContent(result)) {
logger7.error("Invalid trending request format");
return false;
}
const config = await validateCoingeckoConfig(runtime);
const { baseUrl, apiKey, headerKey } = getApiConfig(config);
logger7.log("Fetching trending data...");
const response = await axios7.get(
`${baseUrl}/search/trending`,
{
headers: {
[headerKey]: apiKey
}
}
);
if (!response.data) {
throw new Error("No data received from CoinGecko API");
}
const formattedData = {
coins: response.data.coins.map(({ item }) => ({
name: item.name,
symbol: item.symbol.toUpperCase(),
marketCapRank: item.market_cap_rank,
id: item.id,
thumbnail: item.thumb,
largeImage: item.large
})),
nfts: response.data.nfts.map((nft) => ({
name: nft.name,
symbol: nft.symbol,
id: nft.id,
thumbnail: nft.thumb
})),
categories: response.data.categories.map((category) => ({
name: category.name,
id: category.id
}))
};
const responseText = [
"Trending Coins:",
...formattedData.coins.map(
(coin, index) => `${index + 1}. ${coin.name} (${coin.symbol})${coin.marketCapRank ? ` - Rank #${coin.marketCapRank}` : ""}`
),
"",
"Trending NFTs:",
...formattedData.nfts.length ? formattedData.nfts.map(
(nft, index) => `${index + 1}. ${nft.name} (${nft.symbol})`
) : ["No trending NFTs available"],
"",
"Trending Categories:",
...formattedData.categories.length ? formattedData.categories.map(
(category, index) => `${index + 1}. ${category.name}`
) : ["No trending categories available"]
].join("\n");
logger7.success("Trending data retrieved successfully!");
if (callback) {
callback({
text: responseText,
content: {
trending: formattedData,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}
});
}
return true;
} catch (error) {
logger7.error("Error in GET_TRENDING handler:", error);
const errorMessage = ((_a = error.response) == null ? void 0 : _a.status) === 429 ? "Rate limit exceeded. Please try again later." : `Error fetching trending data: ${error.message}`;
if (callback) {
callback({
text: errorMessage,
content: {
error: error.message,
statusCode: (_b = error.response) == null ? void 0 : _b.status
}
});
}
return false;
}
},
examples: [
[
{
name: "{{user1}}",
content: {
text: "What are the trending cryptocurrencies?"
}
},
{
name: "{{agent}}",
content: {
text: "I'll check the trending cryptocurrencies for you.",
action: "GET_TRENDING"
}
},
{
name: "{{agent}}",
content: {
text: "Here are the trending cryptocurrencies:\n1. Bitcoin (BTC) - Rank #1\n2. Ethereum (ETH) - Rank #2\n{{dynamic}}"
}
}
],
[
{
name: "{{user1}}",
content: {
text: "Show me what's hot in crypto right now"
}
},
{
name: "{{agent}}",
content: {
text: "I'll fetch the current trending cryptocurrencies.",
action: "GET_TRENDING"
}
},
{
name: "{{agent}}",
content: {
text: "Here are the trending cryptocurrencies:\n{{dynamic}}"
}
}
]
]
};
// src/actions/getTrendingPools.ts
import {
logger as logger8,
ModelType as ModelType6
} from "@elizaos/core";
import axios8 from "axios";
import { z as z7 } from "zod";
// src/templates/trendingPools.ts
var getTrendingPoolsTemplate = `Determine if this is a trending pools request. If it is one of the specified situations, perform the corresponding action:
Situation 1: "Get all trending pools"