From 5f7a4e9cdf8c7fa01cc1e162e40779919de80106 Mon Sep 17 00:00:00 2001 From: bijan Date: Tue, 11 Mar 2025 10:17:33 -0400 Subject: [PATCH 1/2] feat(ts): messari action provider (#549) --- typescript/.changeset/ninety-gifts-know.md | 5 + .../agentkit/src/action-providers/index.ts | 1 + .../src/action-providers/messari/README.md | 103 ++++++++ .../src/action-providers/messari/index.ts | 2 + .../messari/messariActionProvider.test.ts | 200 ++++++++++++++++ .../messari/messariActionProvider.ts | 226 ++++++++++++++++++ .../src/action-providers/messari/schemas.ts | 14 ++ 7 files changed, 551 insertions(+) create mode 100644 typescript/.changeset/ninety-gifts-know.md create mode 100644 typescript/agentkit/src/action-providers/messari/README.md create mode 100644 typescript/agentkit/src/action-providers/messari/index.ts create mode 100644 typescript/agentkit/src/action-providers/messari/messariActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/messari/messariActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/messari/schemas.ts diff --git a/typescript/.changeset/ninety-gifts-know.md b/typescript/.changeset/ninety-gifts-know.md new file mode 100644 index 000000000..8607e82f4 --- /dev/null +++ b/typescript/.changeset/ninety-gifts-know.md @@ -0,0 +1,5 @@ +--- +"@coinbase/agentkit": patch +--- + +Add a new Messari action provider that enables AI agents to query the Messari AI toolkit for crypto market research data. diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 0ad07d22d..53a97c7a0 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -13,6 +13,7 @@ export * from "./erc20"; export * from "./erc721"; export * from "./farcaster"; export * from "./jupiter"; +export * from "./messari"; export * from "./pyth"; export * from "./moonwell"; export * from "./morpho"; diff --git a/typescript/agentkit/src/action-providers/messari/README.md b/typescript/agentkit/src/action-providers/messari/README.md new file mode 100644 index 000000000..dffeb3314 --- /dev/null +++ b/typescript/agentkit/src/action-providers/messari/README.md @@ -0,0 +1,103 @@ +# Messari Action Provider + +The Messari Action Provider enables AI agents to query the [Messari AI toolkit](https://messari.io/) for crypto market research data. This provider allows agents to ask research questions about market data, statistics, rankings, historical trends, and information about specific protocols, tokens, or platforms. + +## Getting an API Key + +To use the Messari Action Provider, you need to obtain a Messari API key by following these steps: + +1. Sign up for a Messari account at [messari.io](https://messari.io/) +2. After signing up, navigate to [messari.io/account/api](https://messari.io/account/api) +3. Generate your API key from the account dashboard + +For more detailed information about authentication, refer to the [Messari API Authentication documentation](https://docs.messari.io/reference/authentication). + +Different subscription tiers provide different levels of access to the API. See the [Rate Limiting](#rate-limiting) section for details. + +## Configuration + +Once you have your Messari API key, you can configure the provider in two ways: + +### 1. Environment Variable + +Set the `MESSARI_API_KEY` environment variable: + +```bash +MESSARI_API_KEY=your_messari_api_key +``` + +### 2. Direct Configuration + +Pass the API key directly when initializing the provider: + +```typescript +import { messariActionProvider } from "@coinbase/agentkit"; + +const provider = messariActionProvider({ + apiKey: "your_messari_api_key", +}); +``` + +## Rate Limiting + +The Messari API has rate limits based on your subscription tier: + +| Subscription Tier | Daily Request Limit | +|-------------------|---------------------| +| Free (Unpaid) | 2 requests per day | +| Lite | 10 requests per day | +| Pro | 20 requests per day | +| Enterprise | 50 requests per day | + +If you need more than 50 requests per day, you can contact Messari's sales team to discuss a custom credit allocation system for your specific needs. + +## Actions + +### `research_question` + +This action allows the agent to query the Messari AI toolkit with a research question about crypto markets, protocols, or tokens. + +#### Input Schema + +| Parameter | Type | Description | +|-----------|--------|--------------------------------------------------------------| +| question | string | The research question about crypto markets, protocols, or tokens | + +#### Example Usage + +```typescript +import { AgentKit, messariActionProvider } from "@coinbase/agentkit"; +import { getLangChainTools } from "@coinbase/agentkit-langchain"; +import { createReactAgent } from "@langchain/langgraph/prebuilt"; +import { ChatOpenAI } from "@langchain/openai"; + +// Initialize AgentKit with the Messari action provider +const agentkit = await AgentKit.from({ + actionProviders: [messariActionProvider()], +}); + +// Get LangChain tools from AgentKit +const tools = await getLangChainTools(agentkit); + +// Create a LangChain agent with the tools +const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); +const agent = createReactAgent({ + llm, + tools, +}); + +// The agent can now use the Messari research_question action +// Example prompt: "What is the current price of Ethereum?" +``` + +#### Example Response + +``` +Messari Research Results: + +Ethereum (ETH) has shown strong performance over the past month with a 15% price increase. The current price is approximately $3,500, up from $3,000 at the beginning of the month. Trading volume has also increased by 20% in the same period. +``` + +## Network Support + +The Messari Action Provider is network-agnostic, meaning it supports all networks. The research capabilities are not tied to any specific blockchain network. \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/messari/index.ts b/typescript/agentkit/src/action-providers/messari/index.ts new file mode 100644 index 000000000..d01209430 --- /dev/null +++ b/typescript/agentkit/src/action-providers/messari/index.ts @@ -0,0 +1,2 @@ +export * from "./messariActionProvider"; +export * from "./schemas"; diff --git a/typescript/agentkit/src/action-providers/messari/messariActionProvider.test.ts b/typescript/agentkit/src/action-providers/messari/messariActionProvider.test.ts new file mode 100644 index 000000000..c78aa811a --- /dev/null +++ b/typescript/agentkit/src/action-providers/messari/messariActionProvider.test.ts @@ -0,0 +1,200 @@ +import { messariActionProvider, MessariActionProvider } from "./messariActionProvider"; + +const MOCK_API_KEY = "messari-test-key"; + +// Sample response for the research question action +const MOCK_RESEARCH_RESPONSE = { + data: { + messages: [ + { + role: "assistant", + content: + "Ethereum (ETH) has shown strong performance over the past month with a 15% price increase. The current price is approximately $3,500, up from $3,000 at the beginning of the month. Trading volume has also increased by 20% in the same period.", + }, + ], + }, +}; + +// Sample error response in Messari format +const MOCK_ERROR_RESPONSE = { + error: "Internal server error, please try again. If the problem persists, please contact support", + data: null, +}; + +describe("MessariActionProvider", () => { + let provider: MessariActionProvider; + + beforeEach(() => { + process.env.MESSARI_API_KEY = MOCK_API_KEY; + provider = messariActionProvider({ apiKey: MOCK_API_KEY }); + jest.restoreAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + delete process.env.MESSARI_API_KEY; + }); + + describe("constructor", () => { + it("should initialize with API key from constructor", () => { + const customProvider = messariActionProvider({ apiKey: "custom-key" }); + expect(customProvider["apiKey"]).toBe("custom-key"); + }); + + it("should initialize with API key from environment variable", () => { + process.env.MESSARI_API_KEY = "env-key"; + const envProvider = messariActionProvider(); + expect(envProvider["apiKey"]).toBe("env-key"); + }); + + it("should throw error if API key is not provided", () => { + delete process.env.MESSARI_API_KEY; + expect(() => messariActionProvider()).toThrow("MESSARI_API_KEY is not configured."); + }); + }); + + describe("researchQuestion", () => { + it("should successfully fetch research results", async () => { + const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => MOCK_RESEARCH_RESPONSE, + } as Response); + + const question = "What is the current price of Ethereum?"; + const response = await provider.researchQuestion({ question }); + + // Verify the API was called with the correct parameters + expect(fetchMock).toHaveBeenCalled(); + const [url, options] = fetchMock.mock.calls[0]; + + // Check URL + expect(url).toBe("https://api.messari.io/ai/v1/chat/completions"); + + // Check request options + expect(options).toEqual( + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + "Content-Type": "application/json", + "x-messari-api-key": MOCK_API_KEY, + }), + body: JSON.stringify({ + messages: [ + { + role: "user", + content: question, + }, + ], + }), + }), + ); + + // Check response formatting + expect(response).toContain("Messari Research Results:"); + expect(response).toContain(MOCK_RESEARCH_RESPONSE.data.messages[0].content); + }); + + it("should handle non-ok response with structured error format", async () => { + const errorResponseText = JSON.stringify(MOCK_ERROR_RESPONSE); + + jest.spyOn(global, "fetch").mockResolvedValue({ + ok: false, + status: 500, + statusText: "Internal Server Error", + text: async () => errorResponseText, + } as Response); + + const response = await provider.researchQuestion({ + question: "What is the current price of Bitcoin?", + }); + + // Should use the structured error message from the response + expect(response).toContain("Messari API Error: Internal server error"); + expect(response).not.toContain("500"); // Should not include technical details when we have a structured error + }); + + it("should handle non-ok response with non-JSON error format", async () => { + const plainTextError = "Rate limit exceeded"; + + jest.spyOn(global, "fetch").mockResolvedValue({ + ok: false, + status: 429, + statusText: "Too Many Requests", + text: async () => plainTextError, + } as Response); + + const response = await provider.researchQuestion({ + question: "What is the current price of Bitcoin?", + }); + + // Should fall back to detailed error format + expect(response).toContain("Messari API Error:"); + expect(response).toContain("429"); + expect(response).toContain("Too Many Requests"); + expect(response).toContain(plainTextError); + }); + + it("should handle JSON parsing error in successful response", async () => { + jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => { + throw new Error("Invalid JSON"); + }, + } as unknown as Response); + + const response = await provider.researchQuestion({ + question: "What is the market cap of Solana?", + }); + + expect(response).toContain("Unexpected error: Failed to parse API response"); + expect(response).toContain("Invalid JSON"); + }); + + it("should handle invalid response format", async () => { + jest.spyOn(global, "fetch").mockResolvedValue({ + ok: true, + json: async () => ({ data: { messages: [] } }), // Empty messages array + } as Response); + + const response = await provider.researchQuestion({ + question: "What is the market cap of Solana?", + }); + + expect(response).toContain( + "Unexpected error: Received invalid response format from Messari API", + ); + }); + + it("should handle fetch error", async () => { + const error = new Error("Network error"); + jest.spyOn(global, "fetch").mockRejectedValue(error); + + const response = await provider.researchQuestion({ + question: "What is the market cap of Solana?", + }); + + expect(response).toContain("Unexpected error: Network error"); + }); + + it("should handle string error with JSON content", async () => { + // This simulates a case where an error might be stringified JSON + const stringifiedError = JSON.stringify(MOCK_ERROR_RESPONSE); + jest.spyOn(global, "fetch").mockRejectedValue(stringifiedError); + + const response = await provider.researchQuestion({ + question: "What is the market cap of Solana?", + }); + + // Should parse the JSON string and extract the error message + expect(response).toContain("Messari API Error: Internal server error"); + }); + }); + + describe("supportsNetwork", () => { + it("should always return true as research is network-agnostic", () => { + expect(provider.supportsNetwork({ protocolFamily: "evm" })).toBe(true); + expect(provider.supportsNetwork({ protocolFamily: "solana" })).toBe(true); + expect(provider.supportsNetwork({ protocolFamily: "unknown" })).toBe(true); + }); + }); +}); diff --git a/typescript/agentkit/src/action-providers/messari/messariActionProvider.ts b/typescript/agentkit/src/action-providers/messari/messariActionProvider.ts new file mode 100644 index 000000000..3d956bb2a --- /dev/null +++ b/typescript/agentkit/src/action-providers/messari/messariActionProvider.ts @@ -0,0 +1,226 @@ +import { z } from "zod"; +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { Network } from "../../network"; +import { MessariResearchQuestionSchema } from "./schemas"; + +/** + * Configuration options for the MessariActionProvider. + */ +export interface MessariActionProviderConfig { + /** + * Messari API Key + */ + apiKey?: string; +} + +/** + * Types for API responses and errors + */ +interface MessariAPIResponse { + data: { + messages: Array<{ + content: string; + role: string; + }>; + }; +} + +interface MessariErrorResponse { + error?: string; + data?: null | unknown; +} + +interface MessariError extends Error { + status?: number; + statusText?: string; + responseText?: string; + errorResponse?: { + error?: string; + data?: null | unknown; + }; +} + +/** + * MessariActionProvider is an action provider for Messari AI toolkit interactions. + * It enables AI agents to ask research questions about crypto markets, protocols, and tokens. + * + * @augments ActionProvider + */ +export class MessariActionProvider extends ActionProvider { + private readonly apiKey: string; + private readonly baseUrl = "https://api.messari.io/ai/v1"; + + /** + * Constructor for the MessariActionProvider class. + * + * @param config - The configuration options for the MessariActionProvider + */ + constructor(config: MessariActionProviderConfig = {}) { + super("messari", []); + + config.apiKey ||= process.env.MESSARI_API_KEY; + + if (!config.apiKey) { + throw new Error("MESSARI_API_KEY is not configured."); + } + + this.apiKey = config.apiKey; + } + + /** + * Makes a request to the Messari AI API with a research question + * + * @param args - The arguments containing the research question + * @returns A string containing the research results or an error message + */ + @CreateAction({ + name: "research_question", + description: ` +This tool queries Messari AI for comprehensive crypto research across these datasets: +1. News/Content - Latest crypto news, blogs, podcasts +2. Exchanges - CEX/DEX volumes, market share, assets listed +3. Onchain Data - Active addresses, transaction fees, total transactions. +4. Token Unlocks - Upcoming supply unlocks, vesting schedules, and token emission details +5. Market Data - Asset prices, trading volume, market cap, TVL, and historical performance +6. Fundraising - Investment data, funding rounds, venture capital activity. +7. Protocol Research - Technical analysis of how protocols work, tokenomics, and yield mechanisms +8. Social Data - Twitter followers and Reddit subscribers metrics, growth trends + +Examples: "Which DEXs have the highest trading volume this month?", "When is Arbitrum's next major token unlock?", "How does Morpho generate yield for users?", "Which cryptocurrency has gained the most Twitter followers in 2023?", "What did Vitalik Buterin say about rollups in his recent blog posts?" + `, + schema: MessariResearchQuestionSchema, + }) + async researchQuestion(args: z.infer): Promise { + try { + // Make API request + const response = await fetch(`${this.baseUrl}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-messari-api-key": this.apiKey, + }, + body: JSON.stringify({ + messages: [ + { + role: "user", + content: args.question, + }, + ], + }), + }); + if (!response.ok) { + throw await this.createMessariError(response); + } + + // Parse and validate response + let data: MessariAPIResponse; + try { + data = (await response.json()) as MessariAPIResponse; + } catch (jsonError) { + throw new Error( + `Failed to parse API response: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`, + ); + } + if (!data.data?.messages?.[0]?.content) { + throw new Error("Received invalid response format from Messari API"); + } + + const result = data.data.messages[0].content; + return `Messari Research Results:\n\n${result}`; + } catch (error: unknown) { + if (error instanceof Error && "responseText" in error) { + return this.formatMessariApiError(error as MessariError); + } + + return this.formatGenericError(error); + } + } + + /** + * Checks if the action provider supports the given network. + * Messari research is network-agnostic, so it supports all networks. + * + * @param _ - The network to check + * @returns Always returns true as Messari research is network-agnostic + */ + supportsNetwork(_: Network): boolean { + return true; // Messari research is network-agnostic + } + + /** + * Creates a MessariError from an HTTP response + * + * @param response - The fetch Response object + * @returns A MessariError with response details + */ + private async createMessariError(response: Response): Promise { + const error = new Error( + `Messari API returned ${response.status} ${response.statusText}`, + ) as MessariError; + error.status = response.status; + error.statusText = response.statusText; + + const responseText = await response.text(); + error.responseText = responseText; + try { + const errorJson = JSON.parse(responseText) as MessariErrorResponse; + error.errorResponse = errorJson; + } catch { + // If parsing fails, just use the raw text + } + + return error; + } + + /** + * Formats error details for API errors + * + * @param error - The MessariError to format + * @returns Formatted error message + */ + private formatMessariApiError(error: MessariError): string { + if (error.errorResponse?.error) { + return `Messari API Error: ${error.errorResponse.error}`; + } + + const errorDetails = { + status: error.status, + statusText: error.statusText, + responseText: error.responseText, + message: error.message, + }; + return `Messari API Error: ${JSON.stringify(errorDetails, null, 2)}`; + } + + /** + * Formats generic errors + * + * @param error - The error to format + * @returns Formatted error message + */ + private formatGenericError(error: unknown): string { + // Check if this might be a JSON string containing an error message + if (typeof error === "string") { + try { + const parsedError = JSON.parse(error) as MessariErrorResponse; + if (parsedError.error) { + return `Messari API Error: ${parsedError.error}`; + } + } catch { + // Not valid JSON, continue with normal handling + } + } + + return `Unexpected error: ${error instanceof Error ? error.message : String(error)}`; + } +} + +/** + * Factory function to create a new MessariActionProvider instance. + * + * @param config - The configuration options for the MessariActionProvider + * @returns A new instance of MessariActionProvider + */ +export const messariActionProvider = (config: MessariActionProviderConfig = {}) => + new MessariActionProvider(config); diff --git a/typescript/agentkit/src/action-providers/messari/schemas.ts b/typescript/agentkit/src/action-providers/messari/schemas.ts new file mode 100644 index 000000000..24f17fd69 --- /dev/null +++ b/typescript/agentkit/src/action-providers/messari/schemas.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +/** + * Input schema for submitting a research question to Messari AI. + */ +export const MessariResearchQuestionSchema = z + .object({ + question: z + .string() + .min(1, "Research question is required.") + .describe("The research question about crypto markets, protocols, or tokens"), + }) + .strip() + .describe("Input schema for submitting a research question to Messari AI"); From c6b0013be3633b8db21dfd4246c2e57c81d804ac Mon Sep 17 00:00:00 2001 From: Christopher Gerber Date: Thu, 27 Mar 2025 10:37:26 -0700 Subject: [PATCH 2/2] feat(ts): messari action provider refinements (#619) --- typescript/agentkit/README.md | 9 + .../src/action-providers/messari/README.md | 188 +++++++++++++----- .../src/action-providers/messari/constants.ts | 19 ++ .../src/action-providers/messari/index.ts | 3 + .../messari/messariActionProvider.test.ts | 17 +- .../messari/messariActionProvider.ts | 118 +---------- .../src/action-providers/messari/types.ts | 44 ++++ .../src/action-providers/messari/utils.ts | 68 +++++++ 8 files changed, 292 insertions(+), 174 deletions(-) create mode 100644 typescript/agentkit/src/action-providers/messari/constants.ts create mode 100644 typescript/agentkit/src/action-providers/messari/types.ts create mode 100644 typescript/agentkit/src/action-providers/messari/utils.ts diff --git a/typescript/agentkit/README.md b/typescript/agentkit/README.md index c55d23f5f..b717c4049 100644 --- a/typescript/agentkit/README.md +++ b/typescript/agentkit/README.md @@ -265,6 +265,15 @@ const agent = createReactAgent({
+Messari + + + + + +
research_questionQueries Messari AI for comprehensive crypto research across news, market data, protocol information, and more.
+
+
Morpho diff --git a/typescript/agentkit/src/action-providers/messari/README.md b/typescript/agentkit/src/action-providers/messari/README.md index dffeb3314..05c6f8270 100644 --- a/typescript/agentkit/src/action-providers/messari/README.md +++ b/typescript/agentkit/src/action-providers/messari/README.md @@ -1,34 +1,24 @@ # Messari Action Provider -The Messari Action Provider enables AI agents to query the [Messari AI toolkit](https://messari.io/) for crypto market research data. This provider allows agents to ask research questions about market data, statistics, rankings, historical trends, and information about specific protocols, tokens, or platforms. +This directory contains the Messari action provider implementation, which provides actions to interact with the Messari AI toolkit for fetching DeFi research, market information, and protocol details. -## Getting an API Key +## Getting Started To use the Messari Action Provider, you need to obtain a Messari API key by following these steps: -1. Sign up for a Messari account at [messari.io](https://messari.io/) -2. After signing up, navigate to [messari.io/account/api](https://messari.io/account/api) +1. Sign up for a [Messari account](https://messari.io/) +2. Navigate to [messari.io/account/api](https://messari.io/account/api) 3. Generate your API key from the account dashboard For more detailed information about authentication, refer to the [Messari API Authentication documentation](https://docs.messari.io/reference/authentication). -Different subscription tiers provide different levels of access to the API. See the [Rate Limiting](#rate-limiting) section for details. +### Environment Variables -## Configuration - -Once you have your Messari API key, you can configure the provider in two ways: - -### 1. Environment Variable - -Set the `MESSARI_API_KEY` environment variable: - -```bash -MESSARI_API_KEY=your_messari_api_key +``` +MESSARI_API_KEY ``` -### 2. Direct Configuration - -Pass the API key directly when initializing the provider: +Alternatively you can configure the provider directly during initializing: ```typescript import { messariActionProvider } from "@coinbase/agentkit"; @@ -38,6 +28,28 @@ const provider = messariActionProvider({ }); ``` +## Directory Structure + +``` +messari/ +├── constants.ts # API endpoints and other constants +├── messariActionProvider.test.ts # Tests for the provider +├── messariActionProvider.ts # Main provider with Messari API functionality +├── index.ts # Main exports +├── README.md # Documentation +├── schemas.ts # Messari action schemas +├── types.ts # Type definitions +└── utils.ts # Utility functions +``` + +## Actions + +- `research_question`: Query the Messari AI toolkit with a research question + - Submit natural language questions about crypto markets, protocols, or tokens + - Returns detailed research information compiled from Messari's data sources + - Handles various question types including market data, tokenomics, and project analysis + - Returns formatted results or descriptive error messages + ## Rate Limiting The Messari API has rate limits based on your subscription tier: @@ -49,55 +61,129 @@ The Messari API has rate limits based on your subscription tier: | Pro | 20 requests per day | | Enterprise | 50 requests per day | -If you need more than 50 requests per day, you can contact Messari's sales team to discuss a custom credit allocation system for your specific needs. +Contact Messari's sales team for custom rate limits above 50 requests per day. -## Actions +## Examples + +### Token Market Data -### `research_question` +*What is the current price of Ethereum?* -This action allows the agent to query the Messari AI toolkit with a research question about crypto markets, protocols, or tokens. +
+Tool Output -#### Input Schema +``` +Messari Research Results: -| Parameter | Type | Description | -|-----------|--------|--------------------------------------------------------------| -| question | string | The research question about crypto markets, protocols, or tokens | +As of March 26, 2025, the current metrics for Ethereum (ETH) across major cryptocurrency exchanges are as follows: -#### Example Usage +- **Current Price**: $2,012.99 +- **24-hour Price Change**: + - **Absolute Value**: -$55.31 + - **Percentage**: -2.67% +- **Current Market Capitalization**: $242,471,578,768 +- **24-hour Trading Volume**: $7,011,385,937.27 +- **Circulating Supply**: 120,639,295.2 ETH -```typescript -import { AgentKit, messariActionProvider } from "@coinbase/agentkit"; -import { getLangChainTools } from "@coinbase/agentkit-langchain"; -import { createReactAgent } from "@langchain/langgraph/prebuilt"; -import { ChatOpenAI } from "@langchain/openai"; - -// Initialize AgentKit with the Messari action provider -const agentkit = await AgentKit.from({ - actionProviders: [messariActionProvider()], -}); +The data is based on the latest available information from our asset data sources as of the specified date. +``` +
-// Get LangChain tools from AgentKit -const tools = await getLangChainTools(agentkit); +
+Agent Output -// Create a LangChain agent with the tools -const llm = new ChatOpenAI({ model: "gpt-4o-mini" }); -const agent = createReactAgent({ - llm, - tools, -}); +``` +As of March 26, 2025, the current price of Ethereum (ETH) is **$2,012.99**. Here are some additional metrics: -// The agent can now use the Messari research_question action -// Example prompt: "What is the current price of Ethereum?" +- **24-hour Price Change**: + - Absolute Value: -$55.31 + - Percentage: -2.67% +- **Current Market Capitalization**: $242,471,578,768 +- **24-hour Trading Volume**: $7,011,385,937.27 +- **Circulating Supply**: 120,639,295.2 ETH + +If you need more information or assistance, feel free to ask! ``` +
+ +### Protocol News + +*Tell me about the recent news regarding the Hyperliquid token vault HLP that caused some commotion?* -#### Example Response +
+Tool Output ``` Messari Research Results: -Ethereum (ETH) has shown strong performance over the past month with a 15% price increase. The current price is approximately $3,500, up from $3,000 at the beginning of the month. Trading volume has also increased by 20% in the same period. +Over the past few months, Hyperliquid's HLP vault has faced significant challenges due to a few critical incidents. Here's a detailed overview: + +### Recent Events and Incidents + +1. **Exploit Incident**: + - **Date**: Around March 11-12, 2025. + - **Details**: A significant exploit occurred where a whale managed to manipulate the liquidity mechanics, resulting in a $4 million loss for Hyperliquid's HLP vault. The whale executed a $340 million Ethereum position and, through strategic margin withdrawals, triggered a forced liquidation. Although initially mistaken for a hack, the event was confirmed not to be a protocol exploit. + - **Platform Response**: Hyperliquid implemented changes to its margin requirements and reduced the maximum leverage limits on Bitcoin and Ethereum to prevent similar incidents in the future [News Sources]. + +2. **Price Impact**: + - **HYPE Token**: The HYPE token associated with the platform dropped significantly by around 8.5% immediately following the incident, reflecting investor anxiety and loss of confidence. Although it recovered slightly, it's been noted that there has been a decline of about 44% over the month even before the incident [Price Impact]. + +### Function of HLP in Hyperliquid Ecosystem + +- **Role**: The HLP (Hyperliquidity Provider) vault is a crucial component of Hyperliquid, supporting high-throughput financial operations. It allows users to deposit USDC in exchange for potential profit sharing, effectively underwriting a portion of the platform's trading risk. +- **Performance**: Despite the recent setback, the HLP vault had maintained a strong risk-adjusted performance with an average annual return of 12.07%, continuing to be a high-performing asset on a volatility-adjusted basis. + +### Governance and Parameter Changes + +- **Governance Response**: To mitigate future risks, Hyperliquid's governance team has quickly responded with parameter changes, including increasing maintenance margins and lowering leverage caps to ensure a more stable operational environment [Governance Sources]. + +### Community and User Sentiment + +- **Concerns and Discussions**: The losses triggered debates over the inherent risks associated with active liquidity management models like HLP, compared to passive models. Discussions have focused on the need for robust risk management strategies within DeFi platforms [Community Discussions]. + +### Credibility of Sources + +The information is gathered from credible news articles published on established DeFi and cryptocurrency media outlets. The citations used for these details are among recognized sources that specialize in blockchain and financial technology reporting. + +Overall, the series of incidents has negatively impacted Hyperliquid's market perception and investor confidence in the short term, though the platform has taken decisive steps to address vulnerabilities and reassure its user base. ``` +
+ +
+Agent Output + +``` +The recent news surrounding Hyperliquid's HLP (Hyperliquidity Provider) vault highlights a significant incident that stirred commotion in the community: + +### Key Events + +1. **Exploit Incident**: + - **Date**: Around March 11-12, 2025. + - **Details**: A whale manipulated liquidity mechanics, leading to a $4 million loss for the HLP vault. The manipulative action involved a $340 million Ethereum position and strategically timed margin withdrawals that caused forced liquidations. This event was initially feared as a protocol exploit but was later confirmed not to be so. + - **Response**: In reaction to the incident, Hyperliquid adjusted its margin requirements and lowered maximum leverage limits on Ethereum and Bitcoin to mitigate future risks. + +2. **Price Impact**: + - Following the incident, the HYPE token experienced an immediate dip of approximately 8.5%. Over the month leading up to the incident, the token had already seen a decline of about 44%, reflecting broader concerns among investors. + +### HLP Vault's Role +The HLP vault is critical within Hyperliquid's ecosystem, allowing users to deposit USDC in exchange for potential profit sharing, thus underwriting a portion of the liquidity risk associated with trading on the platform. Despite recent setbacks, it had a solid average annual return of 12.07%. + +### Governance Actions +The governance team promptly responded to the situation by enhancing the platform's risk parameters, including increasing maintenance margins and reducing leverage thresholds to stabilize operations. + +### Community Sentiment +The incident has sparked discussions within the community about the risks associated with active liquidity management models and the necessity for robust risk management strategies in DeFi platforms. + +These events have adversely affected Hyperliquid's standing in the market, though the platform has taken proactive steps to address vulnerabilities and restore confidence among its users. +``` +
+ +## Adding New Actions + +To add new Messari actions: -## Network Support +1. Define your schema in `schemas.ts` +2. Implement your action in `messariActionProvider.ts` +3. Add corresponding tests in `messariActionProvider.test.ts` -The Messari Action Provider is network-agnostic, meaning it supports all networks. The research capabilities are not tied to any specific blockchain network. \ No newline at end of file +The provider is network-agnostic and can be used with any blockchain network. \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/messari/constants.ts b/typescript/agentkit/src/action-providers/messari/constants.ts new file mode 100644 index 000000000..da15c5a89 --- /dev/null +++ b/typescript/agentkit/src/action-providers/messari/constants.ts @@ -0,0 +1,19 @@ +/** + * Base URL for the Messari API + */ +export const MESSARI_BASE_URL = "https://api.messari.io/ai/v1"; + +/** + * Default error message when API key is missing + */ +export const API_KEY_MISSING_ERROR = "MESSARI_API_KEY is not configured."; + +/** + * Rate limits by subscription tier + */ +export const RATE_LIMITS = { + FREE: "2 requests per day", + LITE: "10 requests per day", + PRO: "20 requests per day", + ENTERPRISE: "50 requests per day", +}; diff --git a/typescript/agentkit/src/action-providers/messari/index.ts b/typescript/agentkit/src/action-providers/messari/index.ts index d01209430..5f8ca50ba 100644 --- a/typescript/agentkit/src/action-providers/messari/index.ts +++ b/typescript/agentkit/src/action-providers/messari/index.ts @@ -1,2 +1,5 @@ export * from "./messariActionProvider"; export * from "./schemas"; +export * from "./types"; +export * from "./constants"; +export * from "./utils"; diff --git a/typescript/agentkit/src/action-providers/messari/messariActionProvider.test.ts b/typescript/agentkit/src/action-providers/messari/messariActionProvider.test.ts index c78aa811a..64b695814 100644 --- a/typescript/agentkit/src/action-providers/messari/messariActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/messari/messariActionProvider.test.ts @@ -1,8 +1,8 @@ import { messariActionProvider, MessariActionProvider } from "./messariActionProvider"; +import { API_KEY_MISSING_ERROR } from "./constants"; const MOCK_API_KEY = "messari-test-key"; -// Sample response for the research question action const MOCK_RESEARCH_RESPONSE = { data: { messages: [ @@ -15,7 +15,6 @@ const MOCK_RESEARCH_RESPONSE = { }, }; -// Sample error response in Messari format const MOCK_ERROR_RESPONSE = { error: "Internal server error, please try again. If the problem persists, please contact support", data: null, @@ -49,7 +48,7 @@ describe("MessariActionProvider", () => { it("should throw error if API key is not provided", () => { delete process.env.MESSARI_API_KEY; - expect(() => messariActionProvider()).toThrow("MESSARI_API_KEY is not configured."); + expect(() => messariActionProvider()).toThrow(API_KEY_MISSING_ERROR); }); }); @@ -63,14 +62,11 @@ describe("MessariActionProvider", () => { const question = "What is the current price of Ethereum?"; const response = await provider.researchQuestion({ question }); - // Verify the API was called with the correct parameters expect(fetchMock).toHaveBeenCalled(); const [url, options] = fetchMock.mock.calls[0]; - // Check URL expect(url).toBe("https://api.messari.io/ai/v1/chat/completions"); - // Check request options expect(options).toEqual( expect.objectContaining({ method: "POST", @@ -89,7 +85,6 @@ describe("MessariActionProvider", () => { }), ); - // Check response formatting expect(response).toContain("Messari Research Results:"); expect(response).toContain(MOCK_RESEARCH_RESPONSE.data.messages[0].content); }); @@ -108,9 +103,8 @@ describe("MessariActionProvider", () => { question: "What is the current price of Bitcoin?", }); - // Should use the structured error message from the response expect(response).toContain("Messari API Error: Internal server error"); - expect(response).not.toContain("500"); // Should not include technical details when we have a structured error + expect(response).not.toContain("500"); }); it("should handle non-ok response with non-JSON error format", async () => { @@ -127,7 +121,6 @@ describe("MessariActionProvider", () => { question: "What is the current price of Bitcoin?", }); - // Should fall back to detailed error format expect(response).toContain("Messari API Error:"); expect(response).toContain("429"); expect(response).toContain("Too Many Requests"); @@ -153,7 +146,7 @@ describe("MessariActionProvider", () => { it("should handle invalid response format", async () => { jest.spyOn(global, "fetch").mockResolvedValue({ ok: true, - json: async () => ({ data: { messages: [] } }), // Empty messages array + json: async () => ({ data: { messages: [] } }), } as Response); const response = await provider.researchQuestion({ @@ -177,7 +170,6 @@ describe("MessariActionProvider", () => { }); it("should handle string error with JSON content", async () => { - // This simulates a case where an error might be stringified JSON const stringifiedError = JSON.stringify(MOCK_ERROR_RESPONSE); jest.spyOn(global, "fetch").mockRejectedValue(stringifiedError); @@ -185,7 +177,6 @@ describe("MessariActionProvider", () => { question: "What is the market cap of Solana?", }); - // Should parse the JSON string and extract the error message expect(response).toContain("Messari API Error: Internal server error"); }); }); diff --git a/typescript/agentkit/src/action-providers/messari/messariActionProvider.ts b/typescript/agentkit/src/action-providers/messari/messariActionProvider.ts index 3d956bb2a..c87be5d13 100644 --- a/typescript/agentkit/src/action-providers/messari/messariActionProvider.ts +++ b/typescript/agentkit/src/action-providers/messari/messariActionProvider.ts @@ -3,43 +3,9 @@ import { ActionProvider } from "../actionProvider"; import { CreateAction } from "../actionDecorator"; import { Network } from "../../network"; import { MessariResearchQuestionSchema } from "./schemas"; - -/** - * Configuration options for the MessariActionProvider. - */ -export interface MessariActionProviderConfig { - /** - * Messari API Key - */ - apiKey?: string; -} - -/** - * Types for API responses and errors - */ -interface MessariAPIResponse { - data: { - messages: Array<{ - content: string; - role: string; - }>; - }; -} - -interface MessariErrorResponse { - error?: string; - data?: null | unknown; -} - -interface MessariError extends Error { - status?: number; - statusText?: string; - responseText?: string; - errorResponse?: { - error?: string; - data?: null | unknown; - }; -} +import { MESSARI_BASE_URL, API_KEY_MISSING_ERROR } from "./constants"; +import { MessariActionProviderConfig, MessariAPIResponse, MessariError } from "./types"; +import { createMessariError, formatMessariApiError, formatGenericError } from "./utils"; /** * MessariActionProvider is an action provider for Messari AI toolkit interactions. @@ -49,7 +15,6 @@ interface MessariError extends Error { */ export class MessariActionProvider extends ActionProvider { private readonly apiKey: string; - private readonly baseUrl = "https://api.messari.io/ai/v1"; /** * Constructor for the MessariActionProvider class. @@ -62,7 +27,7 @@ export class MessariActionProvider extends ActionProvider { config.apiKey ||= process.env.MESSARI_API_KEY; if (!config.apiKey) { - throw new Error("MESSARI_API_KEY is not configured."); + throw new Error(API_KEY_MISSING_ERROR); } this.apiKey = config.apiKey; @@ -94,7 +59,7 @@ Examples: "Which DEXs have the highest trading volume this month?", "When is Arb async researchQuestion(args: z.infer): Promise { try { // Make API request - const response = await fetch(`${this.baseUrl}/chat/completions`, { + const response = await fetch(`${MESSARI_BASE_URL}/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", @@ -110,7 +75,7 @@ Examples: "Which DEXs have the highest trading volume this month?", "When is Arb }), }); if (!response.ok) { - throw await this.createMessariError(response); + throw await createMessariError(response); } // Parse and validate response @@ -130,10 +95,10 @@ Examples: "Which DEXs have the highest trading volume this month?", "When is Arb return `Messari Research Results:\n\n${result}`; } catch (error: unknown) { if (error instanceof Error && "responseText" in error) { - return this.formatMessariApiError(error as MessariError); + return formatMessariApiError(error as MessariError); } - return this.formatGenericError(error); + return formatGenericError(error); } } @@ -147,73 +112,6 @@ Examples: "Which DEXs have the highest trading volume this month?", "When is Arb supportsNetwork(_: Network): boolean { return true; // Messari research is network-agnostic } - - /** - * Creates a MessariError from an HTTP response - * - * @param response - The fetch Response object - * @returns A MessariError with response details - */ - private async createMessariError(response: Response): Promise { - const error = new Error( - `Messari API returned ${response.status} ${response.statusText}`, - ) as MessariError; - error.status = response.status; - error.statusText = response.statusText; - - const responseText = await response.text(); - error.responseText = responseText; - try { - const errorJson = JSON.parse(responseText) as MessariErrorResponse; - error.errorResponse = errorJson; - } catch { - // If parsing fails, just use the raw text - } - - return error; - } - - /** - * Formats error details for API errors - * - * @param error - The MessariError to format - * @returns Formatted error message - */ - private formatMessariApiError(error: MessariError): string { - if (error.errorResponse?.error) { - return `Messari API Error: ${error.errorResponse.error}`; - } - - const errorDetails = { - status: error.status, - statusText: error.statusText, - responseText: error.responseText, - message: error.message, - }; - return `Messari API Error: ${JSON.stringify(errorDetails, null, 2)}`; - } - - /** - * Formats generic errors - * - * @param error - The error to format - * @returns Formatted error message - */ - private formatGenericError(error: unknown): string { - // Check if this might be a JSON string containing an error message - if (typeof error === "string") { - try { - const parsedError = JSON.parse(error) as MessariErrorResponse; - if (parsedError.error) { - return `Messari API Error: ${parsedError.error}`; - } - } catch { - // Not valid JSON, continue with normal handling - } - } - - return `Unexpected error: ${error instanceof Error ? error.message : String(error)}`; - } } /** diff --git a/typescript/agentkit/src/action-providers/messari/types.ts b/typescript/agentkit/src/action-providers/messari/types.ts new file mode 100644 index 000000000..264baa960 --- /dev/null +++ b/typescript/agentkit/src/action-providers/messari/types.ts @@ -0,0 +1,44 @@ +/** + * Configuration options for the MessariActionProvider. + */ +export interface MessariActionProviderConfig { + /** + * Messari API Key + */ + apiKey?: string; +} + +/** + * Message format in Messari API responses + */ +export interface MessariMessage { + content: string; + role: string; +} + +/** + * Response format from Messari API + */ +export interface MessariAPIResponse { + data: { + messages: MessariMessage[]; + }; +} + +/** + * Error response format from Messari API + */ +export interface MessariErrorResponse { + error?: string; + data?: null | unknown; +} + +/** + * Extended Error interface for Messari API errors + */ +export interface MessariError extends Error { + status?: number; + statusText?: string; + responseText?: string; + errorResponse?: MessariErrorResponse; +} diff --git a/typescript/agentkit/src/action-providers/messari/utils.ts b/typescript/agentkit/src/action-providers/messari/utils.ts new file mode 100644 index 000000000..b21542ec3 --- /dev/null +++ b/typescript/agentkit/src/action-providers/messari/utils.ts @@ -0,0 +1,68 @@ +import { MessariError, MessariErrorResponse } from "./types"; + +/** + * Creates a MessariError from an HTTP response + * + * @param response - The fetch Response object + * @returns A MessariError with response details + */ +export async function createMessariError(response: Response): Promise { + const error = new Error( + `Messari API returned ${response.status} ${response.statusText}`, + ) as MessariError; + error.status = response.status; + error.statusText = response.statusText; + + const responseText = await response.text(); + error.responseText = responseText; + try { + const errorJson = JSON.parse(responseText) as MessariErrorResponse; + error.errorResponse = errorJson; + } catch { + // If parsing fails, just use the raw text + } + + return error; +} + +/** + * Formats error details for API errors + * + * @param error - The MessariError to format + * @returns Formatted error message + */ +export function formatMessariApiError(error: MessariError): string { + if (error.errorResponse?.error) { + return `Messari API Error: ${error.errorResponse.error}`; + } + + const errorDetails = { + status: error.status, + statusText: error.statusText, + responseText: error.responseText, + message: error.message, + }; + return `Messari API Error: ${JSON.stringify(errorDetails, null, 2)}`; +} + +/** + * Formats generic errors + * + * @param error - The error to format + * @returns Formatted error message + */ +export function formatGenericError(error: unknown): string { + // Check if this might be a JSON string containing an error message + if (typeof error === "string") { + try { + const parsedError = JSON.parse(error) as MessariErrorResponse; + if (parsedError.error) { + return `Messari API Error: ${parsedError.error}`; + } + } catch { + // Not valid JSON, continue with normal handling + } + } + + return `Unexpected error: ${error instanceof Error ? error.message : String(error)}`; +}