Skip to content

Commit 28e613a

Browse files
authored
Merge pull request #2262 from 0xCardinalError/feature/gecko_price_per_addy
feat: CoinGecko - add price per address functionality
2 parents 60bb094 + dd0d263 commit 28e613a

File tree

3 files changed

+273
-1
lines changed

3 files changed

+273
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import {
2+
ActionExample,
3+
composeContext,
4+
Content,
5+
elizaLogger,
6+
generateObject,
7+
HandlerCallback,
8+
IAgentRuntime,
9+
Memory,
10+
ModelClass,
11+
State,
12+
type Action,
13+
} from "@elizaos/core";
14+
import axios from "axios";
15+
import { z } from "zod";
16+
import { getApiConfig, validateCoingeckoConfig } from "../environment";
17+
import { getPriceByAddressTemplate } from "../templates/priceAddress";
18+
19+
// Schema definition for the token price request
20+
export const GetTokenPriceSchema = z.object({
21+
chainId: z.string(),
22+
tokenAddress: z.string(),
23+
});
24+
25+
export type GetTokenPriceContent = z.infer<typeof GetTokenPriceSchema> &
26+
Content;
27+
28+
export const isGetTokenPriceContent = (
29+
obj: any
30+
): obj is GetTokenPriceContent => {
31+
return GetTokenPriceSchema.safeParse(obj).success;
32+
};
33+
34+
interface TokenResponse {
35+
id: string;
36+
symbol: string;
37+
name: string;
38+
market_data: {
39+
current_price: {
40+
usd: number;
41+
};
42+
market_cap: {
43+
usd: number;
44+
};
45+
};
46+
}
47+
48+
export default {
49+
name: "GET_TOKEN_PRICE_BY_ADDRESS",
50+
similes: [
51+
"FETCH_TOKEN_PRICE_BY_ADDRESS",
52+
"CHECK_TOKEN_PRICE_BY_ADDRESS",
53+
"LOOKUP_TOKEN_BY_ADDRESS",
54+
],
55+
validate: async (runtime: IAgentRuntime, message: Memory) => {
56+
await validateCoingeckoConfig(runtime);
57+
return true;
58+
},
59+
description:
60+
"Get the current USD price for a token using its blockchain address",
61+
handler: async (
62+
runtime: IAgentRuntime,
63+
message: Memory,
64+
state: State,
65+
_options: { [key: string]: unknown },
66+
callback?: HandlerCallback
67+
): Promise<boolean> => {
68+
elizaLogger.log("Starting GET_TOKEN_PRICE_BY_ADDRESS handler...");
69+
70+
if (!state) {
71+
state = (await runtime.composeState(message)) as State;
72+
} else {
73+
state = await runtime.updateRecentMessageState(state);
74+
}
75+
76+
try {
77+
elizaLogger.log("Composing token price context...");
78+
const context = composeContext({
79+
state,
80+
template: getPriceByAddressTemplate,
81+
});
82+
83+
elizaLogger.log("Generating content from template...");
84+
const result = await generateObject({
85+
runtime,
86+
context,
87+
modelClass: ModelClass.SMALL,
88+
schema: GetTokenPriceSchema,
89+
});
90+
91+
if (!isGetTokenPriceContent(result.object)) {
92+
elizaLogger.error("Invalid token price request format");
93+
return false;
94+
}
95+
96+
const content = result.object;
97+
elizaLogger.log("Generated content:", content);
98+
99+
// Get API configuration
100+
const config = await validateCoingeckoConfig(runtime);
101+
const { baseUrl, apiKey } = getApiConfig(config);
102+
103+
// Fetch token data
104+
elizaLogger.log("Fetching token data...");
105+
const response = await axios.get<TokenResponse>(
106+
`${baseUrl}/coins/${content.chainId}/contract/${content.tokenAddress}`,
107+
{
108+
headers: {
109+
accept: "application/json",
110+
"x-cg-pro-api-key": apiKey,
111+
},
112+
}
113+
);
114+
115+
const tokenData = response.data;
116+
if (!tokenData.market_data?.current_price?.usd) {
117+
throw new Error(
118+
`No price data available for token ${content.tokenAddress} on ${content.chainId}`
119+
);
120+
}
121+
122+
// Format response
123+
const parts = [
124+
`${tokenData.name} (${tokenData.symbol.toUpperCase()})`,
125+
`Address: ${content.tokenAddress}`,
126+
`Chain: ${content.chainId}`,
127+
`Price: $${tokenData.market_data.current_price.usd.toFixed(6)} USD`,
128+
];
129+
130+
if (tokenData.market_data.market_cap?.usd) {
131+
parts.push(
132+
`Market Cap: $${tokenData.market_data.market_cap.usd.toLocaleString()} USD`
133+
);
134+
}
135+
136+
const responseText = parts.join("\n");
137+
elizaLogger.success("Token price data retrieved successfully!");
138+
139+
if (callback) {
140+
callback({
141+
text: responseText,
142+
content: {
143+
token: {
144+
name: tokenData.name,
145+
symbol: tokenData.symbol,
146+
address: content.tokenAddress,
147+
chain: content.chainId,
148+
price: tokenData.market_data.current_price.usd,
149+
marketCap: tokenData.market_data.market_cap?.usd,
150+
},
151+
},
152+
});
153+
}
154+
155+
return true;
156+
} catch (error) {
157+
elizaLogger.error(
158+
"Error in GET_TOKEN_PRICE_BY_ADDRESS handler:",
159+
error
160+
);
161+
162+
let errorMessage;
163+
if (error.response?.status === 429) {
164+
errorMessage = "Rate limit exceeded. Please try again later.";
165+
} else if (error.response?.status === 403) {
166+
errorMessage =
167+
"This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data.";
168+
} else if (error.response?.status === 400) {
169+
errorMessage =
170+
"Invalid request parameters. Please check your input.";
171+
} else {
172+
errorMessage =
173+
"Failed to fetch token price. Please try again later.";
174+
}
175+
176+
if (callback) {
177+
callback({
178+
text: errorMessage,
179+
content: {
180+
error: error.message,
181+
statusCode: error.response?.status,
182+
requiresProPlan: error.response?.status === 403,
183+
},
184+
});
185+
}
186+
return false;
187+
}
188+
},
189+
190+
examples: [
191+
[
192+
{
193+
user: "{{user1}}",
194+
content: {
195+
text: "What's the price of the USDC token on Ethereum? The address is 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
196+
},
197+
},
198+
{
199+
user: "{{agent}}",
200+
content: {
201+
text: "I'll check the USDC token price for you.",
202+
action: "GET_TOKEN_PRICE_BY_ADDRESS",
203+
},
204+
},
205+
{
206+
user: "{{agent}}",
207+
content: {
208+
text: "USD Coin (USDC)\nAddress: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48\nChain: ethereum\nPrice: {{dynamic}} USD\nMarket Cap: ${{dynamic}} USD",
209+
},
210+
},
211+
],
212+
] as ActionExample[][],
213+
} as Action;

packages/plugin-coingecko/src/index.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Plugin } from "@elizaos/core";
22
import getMarkets from "./actions/getMarkets";
33
import getPrice from "./actions/getPrice";
4+
import getPricePerAddress from "./actions/getPricePerAddress";
45
import getTopGainersLosers from "./actions/getTopGainersLosers";
56
import getTrending from "./actions/getTrending";
67
import { categoriesProvider } from "./providers/categoriesProvider";
@@ -9,7 +10,13 @@ import { coinsProvider } from "./providers/coinsProvider";
910
export const coingeckoPlugin: Plugin = {
1011
name: "coingecko",
1112
description: "CoinGecko Plugin for Eliza",
12-
actions: [getPrice, getTrending, getMarkets, getTopGainersLosers],
13+
actions: [
14+
getPrice,
15+
getPricePerAddress,
16+
getTrending,
17+
getMarkets,
18+
getTopGainersLosers,
19+
],
1320
evaluators: [],
1421
providers: [categoriesProvider, coinsProvider],
1522
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export const getPriceByAddressTemplate = `
2+
Extract the following parameters for token price data:
3+
- **chainId** (string): The blockchain network ID (e.g., "ethereum", "polygon", "binance-smart-chain")
4+
- **tokenAddress** (string): The contract address of the token
5+
- **include_market_cap** (boolean): Whether to include market cap data - defaults to true
6+
7+
Normalize chain IDs to lowercase names: ethereum, polygon, binance-smart-chain, avalanche, fantom, arbitrum, optimism, etc.
8+
Token address should be the complete address string, maintaining its original case.
9+
10+
Provide the values in the following JSON format:
11+
12+
\`\`\`json
13+
{
14+
"chainId": "ethereum",
15+
"tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
16+
"include_market_cap": true
17+
}
18+
\`\`\`
19+
20+
Example request: "What's the price of USDC on Ethereum? Address: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
21+
Example response:
22+
\`\`\`json
23+
{
24+
"chainId": "ethereum",
25+
"tokenAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
26+
"include_market_cap": true
27+
}
28+
\`\`\`
29+
30+
Example request: "Check the price for this token on Polygon: 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
31+
Example response:
32+
\`\`\`json
33+
{
34+
"chainId": "polygon",
35+
"tokenAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
36+
"include_market_cap": true
37+
}
38+
\`\`\`
39+
40+
Example request: "Get price for BONK token on Solana with address HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC"
41+
Example response:
42+
\`\`\`json
43+
{
44+
"chainId": "solana",
45+
"tokenAddress": "HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC"
46+
}
47+
\`\`\`
48+
49+
Here are the recent user messages for context:
50+
{{recentMessages}}
51+
52+
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.`;

0 commit comments

Comments
 (0)