Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add CoinGecko plugin #1382

Open
wants to merge 33 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e4a0845
add basic stuff
0xCardinalError Dec 20, 2024
4f64778
add first atempt of provider/action
0xCardinalError Dec 20, 2024
359cf72
add gecko to packages
0xCardinalError Dec 20, 2024
3aa5eec
add proper definitions and types
0xCardinalError Dec 20, 2024
f6f01d8
add all the code work gecko plugin
0xCardinalError Dec 22, 2024
85d7826
cleanup from redundant code
0xCardinalError Dec 22, 2024
aa1736a
add readme and .env value example
0xCardinalError Dec 22, 2024
1b29edb
add new action, show price per address and base chain
0xCardinalError Dec 22, 2024
c6d4b52
add token name
0xCardinalError Dec 22, 2024
1d61aec
set types in types file
0xCardinalError Dec 22, 2024
e0c39f3
remove obsolete changes
0xCardinalError Dec 22, 2024
6e08102
update with develop, fix conflicts with index and package.json
0xCardinalError Dec 23, 2024
08d023d
proper agent package.json
0xCardinalError Dec 23, 2024
d404d82
proper root package.json
0xCardinalError Dec 23, 2024
62c26cb
adjust formating
0xCardinalError Dec 23, 2024
8bd0c15
original package added
0xCardinalError Dec 23, 2024
cb1c23f
with no formating
0xCardinalError Dec 23, 2024
8341408
Merge branch 'develop' into gecko_plugin
0xCardinalError Dec 24, 2024
94defa6
Update index.ts
0xCardinalError Dec 24, 2024
5880993
Update package.json
0xCardinalError Dec 24, 2024
3132cf5
Update .npmignore
0xCardinalError Dec 24, 2024
f341828
Update tsconfig.json
0xCardinalError Dec 24, 2024
316a65e
Update package.json
0xCardinalError Dec 24, 2024
eb96c57
add back gecko plugin insert
0xCardinalError Dec 24, 2024
4d15501
add new path and add to docker
0xCardinalError Dec 24, 2024
2690047
add proper paths to package
0xCardinalError Dec 24, 2024
fc26d7b
change wrong path for imports, use elizaOS now for all
0xCardinalError Dec 24, 2024
aa21173
use different generator, simplfy context for better precision
0xCardinalError Dec 24, 2024
b4981d5
use generateMessageResponse also for price per address
0xCardinalError Dec 25, 2024
5e82ec6
cleanup types
0xCardinalError Dec 25, 2024
15ef830
Merge branch 'develop' into gecko_plugin
0xCardinalError Dec 25, 2024
8871555
Merge branch 'develop' into gecko_plugin
0xCardinalError Dec 26, 2024
6b31fe2
add gecko to lock file
0xCardinalError Dec 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,9 @@ STORY_API_BASE_URL= # Story API base URL
STORY_API_KEY= # Story API key
PINATA_JWT= # Pinata JWT for uploading files to IPFS

# Coingecko
COINGECKO_API_KEY= #your-gecko-key-here

# Cronos zkEVM
CRONOSZKEVM_ADDRESS=
CRONOSZKEVM_PRIVATE_KEY=
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@elizaos/plugin-evm": "workspace:*",
"@elizaos/plugin-flow": "workspace:*",
"@elizaos/plugin-story": "workspace:*",
"@elizaos/plugin-gecko": "workspace:*",
"@elizaos/plugin-goat": "workspace:*",
"@elizaos/plugin-icp": "workspace:*",
"@elizaos/plugin-image-generation": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
webhookPlugin,
} from "@elizaos/plugin-coinbase";
import { confluxPlugin } from "@elizaos/plugin-conflux";
import { coingeckoPlugin } from "@elizaos/plugin-gecko";
import { evmPlugin } from "@elizaos/plugin-evm";
import { storyPlugin } from "@elizaos/plugin-story";
import { flowPlugin } from "@elizaos/plugin-flow";
Expand Down Expand Up @@ -501,6 +502,7 @@ export async function createAgent(
getSecret(character, "CONFLUX_CORE_PRIVATE_KEY")
? confluxPlugin
: null,
getSecret(character, "COINGECKO_API_KEY") ? coingeckoPlugin : null,
nodePlugin,
getSecret(character, "SOLANA_PUBLIC_KEY") ||
(getSecret(character, "WALLET_PUBLIC_KEY") &&
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-gecko/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*

!dist/**
!package.json
!readme.md
!tsup.config.ts
53 changes: 53 additions & 0 deletions packages/plugin-gecko/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# `@elizaos/plugin-gecko`

This plugin provides actions and providers for interacting with CoinGecko API, using free version
https://docs.coingecko.com/v3.0.1/reference/introduction

to get the Free Public API access, create your API key here
https://www.coingecko.com/en/developers/dashboard

---

## Configuration

### Default Setup

By default, \*_CoinGecko plugin_ is enabled. To use it, simply add your API key to the `.env` file:

```env
COINGECKO_API_KEY=your-gecko-key-here
```

## Provider

The **Coin Provider** gets the list of all the coins from Gecko API
https://docs.coingecko.com/v3.0.1/reference/coins-list
with its respective id,name and symbol, this can then be used for actions

---

## Actions

### Price

Get the current price and market cap of a coin/token from CoingGecko API, provide ticker or name of currency you want:

**Example usage:**

```env
Get me price of Pendle token
```

### Get Price per address

Get the current price and market cap of a coin/token from CoingGecko API by providing currency address and base platform

**Example usage:**

```env
Get me price for 0x4f9fd6be4a90f2620860d680c0d4d5fb53d1a825 on Base

Get price for HeLp6NuQkmYB4pYWo2zYs22mESHXPQYzXbB8n4V98jwC on Solana
```

---
3 changes: 3 additions & 0 deletions packages/plugin-gecko/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import eslintGlobalConfig from "../../eslint.config.mjs";

export default [...eslintGlobalConfig];
20 changes: 20 additions & 0 deletions packages/plugin-gecko/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@elizaos/plugin-gecko",
"version": "0.1.7-alpha.1",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"dependencies": {
"@elizaos/core": "workspace:*",
"tsup": "8.3.5"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch",
"test": "vitest run",
"lint": "eslint --fix --cache ."
},
"peerDependencies": {
"whatwg-url": "7.1.0"
}
}
204 changes: 204 additions & 0 deletions packages/plugin-gecko/src/actions/price.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import {
Action,
elizaLogger,
IAgentRuntime,
Memory,
HandlerCallback,
State,
composeContext,
generateMessageResponse,
ModelClass,
} from "@elizaos/core";

import { coingeckoProvider } from "../providers/coins";
import type { PriceResponse } from "../types.ts";

export const priceTemplate = `From previous sentence extract only the cryptocurrency name, symbol, or ticker being asked about.
Do not include words like "token", "coin", "price",
unless it's part of the name like in "bitcoin" there is a "coin".
Respond with a JSON markdown block containing only the extracted value:

\`\`\`json
{
"coinName": string | null
}
\`\`\`
`;

function formatMarketCap(marketCap: number): string {
if (marketCap >= 1_000_000_000) {
return `${(marketCap / 1_000_000_000).toFixed(1)} billion`;
} else if (marketCap >= 1_000_000) {
return `${(marketCap / 1_000_000).toFixed(1)} million`;
} else if (marketCap >= 1_000) {
return `${(marketCap / 1_000).toFixed(1)} thousand`;
}
return marketCap.toString();
}

export const getPriceAction: Action = {
name: "GET_COIN_PRICE",
description:
"Get the current USD price and market cap for a specified cryptocurrency using CoinGecko API.",
validate: async (runtime: IAgentRuntime, _message: Memory) => {
elizaLogger.log("Validating runtime for GET_COIN_PRICE...");
return !!(
runtime.getSetting("COINGECKO_API_KEY") ||
process.env.COINGECKO_API_KEY
);
},
handler: async (
runtime: IAgentRuntime,
_message: Memory,
state: State,
_options: any,
callback: HandlerCallback
) => {
elizaLogger.log("Starting GET_COIN_PRICE handler...");

try {
const apiKey =
runtime.getSetting("COINGECKO_API_KEY") ??
process.env.COINGECKO_API_KEY;

// Get the list of supported coins first
const { supportedCoins } = await coingeckoProvider.get(
runtime,
_message
);

// Update the state with current inputs
if (!state) {
state = (await runtime.composeState(_message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}

// Use state replacements but add message at the top of template
const context = composeContext({
state,
template: `${_message.content.text}\n${priceTemplate}`,
});

const priceRequest = await generateMessageResponse({
runtime,
context,
modelClass: ModelClass.SMALL,
});

const result = priceRequest.coinName as string;

if (!result) {
callback(
{
text: "Invalid coin name specified.",
},
[]
);
return;
}

const searchTerm = result.toLowerCase();

// Find all matching coins in our supported coins list
const matchingCoins = supportedCoins.filter(
(c) =>
c.id.includes(searchTerm) ||
c.symbol.includes(searchTerm) ||
c.name.toLowerCase().includes(searchTerm)
);

if (matchingCoins.length === 0) {
callback(
{
text: `Could not find coin "${searchTerm}" in CoinGecko's supported coins list.`,
},
[]
);
return;
}

// If we have multiple matches, we'll need to fetch prices for all of them
const pricePromises = matchingCoins.map(async (coin) => {
const priceUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${coin.id}&vs_currencies=usd&include_market_cap=true`;
const priceResponse = await fetch(priceUrl, {
method: "GET",
headers: {
accept: "application/json",
"x-cg-demo-api-key": apiKey,
},
});

if (!priceResponse.ok) {
return null;
}

const priceData: PriceResponse = await priceResponse.json();
return {
coin,
price: priceData[coin.id]?.usd,
marketCap: priceData[coin.id]?.usd_market_cap,
};
});

const priceResults = await Promise.all(pricePromises);

// Filter out any failed requests and sort by market cap
const validResults = priceResults
.filter(
(result): result is NonNullable<typeof result> =>
result !== null &&
typeof result.price === "number" &&
typeof result.marketCap === "number"
)
.sort((a, b) => b.marketCap - a.marketCap);

if (validResults.length === 0) {
callback(
{
text: `Unable to fetch price data for ${searchTerm}.`,
},
[]
);
return;
}

// Use the coin with the highest market cap
const { coin, price, marketCap } = validResults[0];
const formattedMarketCap = formatMarketCap(marketCap);

// If there were multiple matches, add a note about the selection
const multipleMatchesNote =
validResults.length > 1
? `\n(Selected based on highest market cap among ${validResults.length} matching coins)`
: "";

elizaLogger.log(multipleMatchesNote);

callback(
{
text: `Current price for ${coin.name} (${coin.symbol.toUpperCase()}): ${price.toFixed(2)} USD\nMarket Cap: ${formattedMarketCap} USD`,
},
[]
);
} catch (error) {
elizaLogger.error("Error during price lookup:", error);
callback(
{
text: "Failed to fetch coin price. Please check the logs for more details.",
},
[]
);
}
},
examples: [],
similes: [
"GET_COIN_PRICE",
"FETCH_CRYPTO_PRICE",
"CHECK_TOKEN_PRICE",
"LOOKUP_COIN_VALUE",
"GET_TOKEN_PRICE",
"CHECK_CRYPTO_PRICE",
"FETCH_TOKEN_VALUE",
],
};
Loading
Loading