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: birdeye provider to support all possible evm symbols #1366

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@elizaos/plugin-multiversx": "workspace:*",
"@elizaos/plugin-near": "workspace:*",
"@elizaos/plugin-zksync-era": "workspace:*",
"@elizaos/plugin-birdeye": "workspace:*",
"readline": "1.3.0",
"ws": "8.18.0",
"yargs": "17.7.2"
Expand Down
2 changes: 2 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { suiPlugin } from "@elizaos/plugin-sui";
import { TEEMode, teePlugin } from "@elizaos/plugin-tee";
import { tonPlugin } from "@elizaos/plugin-ton";
import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era";
import { birdeyePlugin } from "@elizaos/plugin-birdeye";
import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
Expand Down Expand Up @@ -571,6 +572,7 @@ export async function createAgent(
getSecret(character, "TON_PRIVATE_KEY") ? tonPlugin : null,
getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null,
getSecret(character, "STORY_PRIVATE_KEY") ? storyPlugin : null,
getSecret(character, "BIRDEYE_API_KEY") ? birdeyePlugin : null,
].filter(Boolean),
providers: [],
actions: [],
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-birdeye/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*

!dist/**
!package.json
!readme.md
!tsup.config.ts
25 changes: 25 additions & 0 deletions packages/plugin-birdeye/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Birdeye plugin for Eliza

Basic API wrapper for <https://birdeye.so> inside our AI agent, almost readonly API only.
For more information please refer to their [docs](https://docs.birdeye.so/reference/get_defi-tokenlist)

## What you need?

```
BIRDEYE_API_KEY=zzz-some-secret
BIRDEYE_WALLET_ADDR=your porfolio profile address
```

## Integrate with other plugin

```js
// init your provider with custom support map
const provider = new BirdeyeProvider(runtime.cacheManager, {
'WETH': '0xs0000000001231231',
...
})

const price = await provider.fetchTokenPriceBySymbol('WETH')

// further action based on this
```
3 changes: 3 additions & 0 deletions packages/plugin-birdeye/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];
19 changes: 19 additions & 0 deletions packages/plugin-birdeye/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@elizaos/plugin-birdeye",
"version": "0.1.7-alpha.1",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"dependencies": {
"@elizaos/core": "workspace:*",
"node-cache": "5.1.2",
"tsup": "8.3.5",
"vitest": "2.1.4"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch",
"lint": "eslint --fix --cache .",
"test": "vitest run"
}
}
189 changes: 189 additions & 0 deletions packages/plugin-birdeye/src/actions/report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import {
Action,
ActionExample,
composeContext,
elizaLogger,
generateText,
ModelClass,
type IAgentRuntime,
type Memory,
type State
} from "@elizaos/core";
import { BirdeyeProvider } from "../providers/birdeye";

const extractTokenSymbolTemplate = `Given the recent message below:

{{recentMessages}}

Extract the 1 latest information about the requested token report:
- Input token symbol
- Extra about this symbol

When the symbol is specified in all lower case, such as btc, eth, sol..., we should convert it into wellknown symbol.
E.g. btc instead of BTC, sol instead of SOL.

But when we see them in mixed form, such as SOl, DOl, eTH we should keep the original and notice user instead.
When in doubt, specify the concern in the message field.

Respond exactly a JSON object containing only the extracted values, no extra description or message needed.
Use null for any values that cannot be determined. The result should be a valid JSON object with the following schema:
{
"symbol": string | null,
"message": string | null,
}

Examples:
Message: "Tell me about BTC"
Response: {
"symbol": "BTC",
"message": null
}

Message: "Do you know about SOl."
Response:
{
"symbol": "SOl",
"message": "Please note that the symbol is not in the standard format."
}
`;


const formatTokenReport = (data) => {
let output = `**Token Security and Trade Report**\n`;
output += `Token symbol: ${data.symbol}\n`
output += `Token Address: ${data.tokenAddress}\n\n`;

output += `**Ownership Distribution:**\n`;
output += `- Owner Balance: ${data.security.ownerBalance}\n`;
output += `- Creator Balance: ${data.security.creatorBalance}\n`;
output += `- Owner Percentage: ${data.security.ownerPercentage}%\n`;
output += `- Creator Percentage: ${data.security.creatorPercentage}%\n`;
output += `- Top 10 Holders Balance: ${data.security.top10HolderBalance}\n`;
output += `- Top 10 Holders Percentage: ${data.security.top10HolderPercent}%\n\n`;

// Trade Data
output += `**Trade Data:**\n`;
output += `- Holders: ${data.volume.holder}\n`;
output += `- Unique Wallets (24h): ${data.volume.unique_wallet_24h}\n`;
output += `- Price Change (24h): ${data.volume.price_change_24h_percent}%\n`;
output += `- Price Change (12h): ${data.volume.price_change_12h_percent}%\n`;
output += `- Volume (24h USD): $${data.volume.volume_24h_usd}\n`;
output += `- Current Price: $${data.volume.price}\n\n`;

return output
}

const extractTokenSymbol = async (
runtime: IAgentRuntime,
message: Memory,
state: State,
options: any,
callback?: any) => {
const context = composeContext({
state,
template: extractTokenSymbolTemplate
})

const response = await generateText({
runtime,
context,
modelClass: ModelClass.LARGE,
})

try {
const regex = new RegExp(/\{(.+)\}/gms);
const normalized = response && regex.exec(response)?.[0]
elizaLogger.debug('Normalized data', normalized)
return normalized && JSON.parse(normalized)
} catch {
callback?.({text: response})
return true
}
}

export const reportToken = {
name: "REPORT_TOKEN",
similes: ["CHECK_TOKEN", "REVIEW_TOKEN", "TOKEN_DETAILS"],
description: "Check background data for a given token",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
options: any,
callback?: any
) => {
try {
const params = await extractTokenSymbol(
runtime,
message,
state,
options,
callback
)

elizaLogger.debug('Params', params)

if(!params?.symbol) {
callback?.({text: "I need a token symbol to begin"})
return true
}

if(params?.message) {
callback?.({text: `I need more clarification to continue, ${params.message}`})
return true
}

const symbol = params?.symbol
elizaLogger.log('Fetching birdeye data', symbol)
const provider = new BirdeyeProvider(runtime.cacheManager)

const [tokenAddress, security, volume] = await Promise.all([
provider.getTokenAddress(symbol),
provider.fetchTokenSecurityBySymbol(symbol),
provider.fetchTokenTradeDataBySymbol(symbol),
]);

elizaLogger.log('Fetching birdeye done')
const msg = formatTokenReport({
symbol,
tokenAddress,
security: security.data,
volume: volume.data
})
callback?.({text: msg})
return true
} catch (error) {
console.error("Error in reportToken handler:", error.message);
callback?.({ text: `Error: ${error.message}` });
return false;
}
},
validate: async (runtime: IAgentRuntime, message: Memory) => {
return true;
},
examples: [
[
{
user: "user",
content: {
text: "Tell me what you know about SOL",
action: "CHECK_TOKEN",
},
},
{
user: "user",
content: {
text: "Do you know about SOL",
action: "TOKEN_DETAILS",
},
},
{
user: "user",
content: {
text: "Tell me about WETH",
action: "REVIEW_TOKEN",
},
},
],
] as ActionExample[][],
} as Action;
35 changes: 35 additions & 0 deletions packages/plugin-birdeye/src/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { IAgentRuntime } from "@elizaos/core";
import { z } from "zod";

export const birdeyeEnvSchema = z.object({
BIRDEYE_API_KEY: z.string().min(1, "Birdeye API key is required"),
});

export type BirdeyeConfig = z.infer<typeof birdeyeEnvSchema>;

export async function validateBirdeyeConfig(
runtime: IAgentRuntime
): Promise<BirdeyeConfig> {
try {
const config = {
BIRDEYE_API_KEY:
runtime.getSetting("BIRDEYE_API_KEY") ||
process.env.BIRDEYE_API_KEY,
BIRDEYE_WALLET_ADDR:
runtime.getSetting("BIRDEYE_WALLET_ADDR") ||
process.env.BIRDEYE_WALLET_ADDR,
};

return birdeyeEnvSchema.parse(config);
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors
.map((err) => `${err.path.join(".")}: ${err.message}`)
.join("\n");
throw new Error(
`Birdeye configuration validation failed:\n${errorMessages}`
);
}
throw error;
}
}
15 changes: 15 additions & 0 deletions packages/plugin-birdeye/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Plugin } from "@elizaos/core";

import { birdeyeProvider, BirdeyeProvider } from "./providers/birdeye";
import { reportToken } from "./actions/report";

export { BirdeyeProvider };

export const birdeyePlugin: Plugin = {
name: "birdeye",
description: "Birdeye Plugin for Eliza",
providers: [birdeyeProvider],
actions: [reportToken]
};

export default birdeyePlugin;
Loading
Loading