From 8eaf6c6b264268ca72b3c31b2846e7aa508dc16c Mon Sep 17 00:00:00 2001 From: Theofilus Kharisma Date: Thu, 29 May 2025 08:11:56 +0700 Subject: [PATCH 1/2] feat: add Pond action provider - Add wallet risk assessment functionality - Add token analysis capabilities - Add chain-specific data analysis - Add comprehensive error handling - Add API key configuration support --- .../src/action-providers/pond/README.md | 113 + .../src/action-providers/pond/index.ts | 2 + .../pond/pondActionProvider.test.ts | 833 ++++++++ .../pond/pondActionProvider.ts | 1878 +++++++++++++++++ .../src/action-providers/pond/schemas.ts | 542 +++++ 5 files changed, 3368 insertions(+) create mode 100644 typescript/agentkit/src/action-providers/pond/README.md create mode 100644 typescript/agentkit/src/action-providers/pond/index.ts create mode 100644 typescript/agentkit/src/action-providers/pond/pondActionProvider.test.ts create mode 100644 typescript/agentkit/src/action-providers/pond/pondActionProvider.ts create mode 100644 typescript/agentkit/src/action-providers/pond/schemas.ts diff --git a/typescript/agentkit/src/action-providers/pond/README.md b/typescript/agentkit/src/action-providers/pond/README.md new file mode 100644 index 000000000..df5f9a173 --- /dev/null +++ b/typescript/agentkit/src/action-providers/pond/README.md @@ -0,0 +1,113 @@ +# Pond Action Provider + +This directory contains the Pond action provider implementation, which provides actions to interact with the Pond API for wallet risk assessment, token analysis, and chain-specific data analysis. + +## Getting Started + +To use the Pond Action Provider, you need to obtain the following API keys: + +1. Pond API Key: + - Default key is limited to 10,000 requests + - For higher request limits, contact us through secure channels + +2. Base Dify API Key: + - Required for Base chain data analyst queries + - Used for natural language queries about Base chain data + +3. Ethereum Dify API Key: + - Required for Ethereum chain data analyst queries + - Used for natural language queries about Ethereum chain data + +### Environment Variables + +``` +POND_API_KEY +BASE_DIFY_API_KEY +ETH_DIFY_API_KEY +``` + +Alternatively, you can configure the provider directly during initialization: + +```typescript +import { pondActionProvider } from "@coinbase/agentkit"; + +const provider = pondActionProvider({ + apiKey: "your_pond_api_key", + baseDifyApiKey: "your_base_dify_api_key", + ethDifyApiKey: "your_eth_dify_api_key" +}); +``` + +## Directory Structure + +``` +pond/ +├── constants.ts # API endpoints and other constants +├── pondActionProvider.test.ts # Tests for the provider +├── pondActionProvider.ts # Main provider with Pond API functionality +├── index.ts # Main exports +├── README.md # Documentation +├── schemas.ts # Pond action schemas +└── types.ts # Type definitions +``` + +## Actions + +- `getWalletRiskScore`: Get risk assessment for a wallet + - Supports multiple risk assessment models + - Provides risk score, level, and feature completeness + - Models: OlehRCL (default), 22je0569, Wellspring Praise + +- `getWalletSummary`: Get comprehensive wallet analysis + - DEX trading activity + - Transaction metrics + - Portfolio diversity + - Gas usage analysis + +- `getSybilPrediction`: Detect potential Sybil attacks + - Analyzes wallet behavior patterns + - Identifies multi-account farming activities + +- `getTokenPricePrediction`: Predict token price movements + - Multiple timeframe predictions (1-24 hours) + - Price movement forecasts + - Risk level assessment + +- `getTokenRiskScores`: Get token risk assessment + - Multiple risk assessment models + - Comprehensive token analysis + - Market performance metrics + +- `getTopSolanaMemeCoins`: Analyze Solana meme coins + - Top 10 meme coins analysis + - Price change predictions + - Trading activity metrics + +- `getBaseDataAnalysis`: Query Base chain data + - Natural language queries + - Token analysis + - NFT analysis + - Market analysis + +- `getEthDataAnalysis`: Query Ethereum chain data + - Natural language queries + - Token analysis + - NFT analysis + - Market analysis + +## Rate Limiting + +The default Pond API key is limited to 10,000 requests. For higher request limits, please contact us through secure channels (email or official platform) to obtain an API key with extended capabilities. + +## Error Handling + +The provider includes comprehensive error handling for: +- Missing API keys +- Invalid wallet addresses +- API connection issues +- Response parsing errors +- Server-side errors +- Rate limiting +- Authentication failures + +Errors are returned as formatted strings with relevant details. \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/pond/index.ts b/typescript/agentkit/src/action-providers/pond/index.ts new file mode 100644 index 000000000..ef7b398c6 --- /dev/null +++ b/typescript/agentkit/src/action-providers/pond/index.ts @@ -0,0 +1,2 @@ +export * from "./pondActionProvider"; +export * from "./schemas"; \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/pond/pondActionProvider.test.ts b/typescript/agentkit/src/action-providers/pond/pondActionProvider.test.ts new file mode 100644 index 000000000..54b709b3d --- /dev/null +++ b/typescript/agentkit/src/action-providers/pond/pondActionProvider.test.ts @@ -0,0 +1,833 @@ +import { PondActionProvider } from "./pondActionProvider"; + +// Mock fetch globally +const mockFetch = jest.fn(); +global.fetch = mockFetch; + +describe("PondActionProvider", () => { + const provider = new PondActionProvider({ + apiKey: "pond-api-test", + baseDifyApiKey: "base-dify-test", + ethDifyApiKey: "eth-dify-test" + }); + const testWalletAddress = "0xD558cE26E3e3Ca0fAc12aE116eAd9D7014a42d18"; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should be initialized with default values", () => { + expect(provider).toBeInstanceOf(PondActionProvider); + }); + + it("should support the network", () => { + expect(provider.supportsNetwork()).toBe(true); + }); + + describe("getWalletRiskScore", () => { + const mockRiskScoreResponse = { + code: 200, + data: [ + { + input_key: testWalletAddress.toLowerCase(), + score: 0.8001304268836975, + debug_info: { + feature_update_time: { + "2025-05-11 00:00": "100.0%", + "null": "0.0%" + }, + not_null_feature: "100.0%" + } + } + ] + }; + + const mockRiskScoreResponseNoDebug = { + code: 200, + data: [ + { + input_key: testWalletAddress.toLowerCase(), + score: 0.8001304268836975 + } + ] + }; + + it("should successfully get wallet risk score with default model", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockRiskScoreResponse, + }); + + const result = await provider.getWalletRiskScore({ + walletAddress: testWalletAddress, + }); + + expect(result).toContain("Using Pond wallet risk scoring from OlehRCL"); + expect(result).toContain("Risk Assessment"); + expect(result).toContain("80.01%"); // Converted from 0.8001304268836975 + expect(result).toContain("CRITICAL"); // Risk level for score > 0.8 + expect(result).toContain("100.0%"); // Feature completeness + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: expect.stringContaining(testWalletAddress), + }) + ); + + const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(requestBody.model_id).toBe(40); // Risk score model ID + }); + + it("should successfully get wallet risk score with BASE_22JE0569 model", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockRiskScoreResponseNoDebug, + }); + + const result = await provider.getWalletRiskScore({ + walletAddress: testWalletAddress, + model: 'BASE_22JE0569' + }); + + expect(result).toContain("Using Pond wallet risk scoring from 22je0569"); + expect(result).toContain("Risk Assessment"); + expect(result).toContain("80.01%"); + expect(result).toContain("CRITICAL"); + expect(result).not.toContain("Feature Completeness"); // Debug info is missing + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: expect.stringContaining(testWalletAddress), + }) + ); + + const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(requestBody.model_id).toBe(14); // BASE_22JE0569 model ID + }); + + it("should successfully get wallet risk score with BASE_WELLSPRING model", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockRiskScoreResponse, + }); + + const result = await provider.getWalletRiskScore({ + walletAddress: testWalletAddress, + model: 'BASE_WELLSPRING' + }); + + expect(result).toContain("Using Pond wallet risk scoring from Wellspring Praise"); + expect(result).toContain("Risk Assessment"); + expect(result).toContain("80.01%"); + expect(result).toContain("CRITICAL"); + expect(result).toContain("100.0%"); // Feature completeness + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: expect.stringContaining(testWalletAddress), + }) + ); + + const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(requestBody.model_id).toBe(15); // BASE_WELLSPRING model ID + }); + + it("should handle API errors for risk score", async () => { + const errorMessage = "API Error"; + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => errorMessage, + }); + + const result = await provider.getWalletRiskScore({ + walletAddress: testWalletAddress, + }); + + expect(result).toContain("Error getting wallet risk score"); + expect(result).toContain(errorMessage); + }); + + it("should handle invalid wallet address for risk score", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "Invalid wallet address", + }); + + const result = await provider.getWalletRiskScore({ + walletAddress: "invalid-address", + }); + + expect(result).toContain("Error getting wallet risk score"); + expect(result).toContain("Invalid wallet address"); + }); + }); + + describe("getWalletSummary", () => { + const mockSummaryResponse = { + code: 200, + msg: "Success", + resp_type: 1, + resp_items: [ + { + input_key: testWalletAddress.toLowerCase(), + score: null, + analysis_result: { + BASE_DEX_SWAPS_USER_TOTAL_ACTIONS_COUNT_FOR_180DAYS: 5, + BASE_DEX_SWAPS_USER_TRADING_VOLUME_SUM_FOR_180DAYS: 47789.43, + BASE_DEX_SWAPS_USER_TRADING_VOLUME_AVG_FOR_180DAYS: 1296927.485, + BASE_DEX_SWAPS_USER_TRADING_VOLUME_MEDIAN_FOR_180DAYS: 17, + BASE_DEX_SWAPS_USER_TRADING_PNL_FOR_180DAYS: 43924.335, + BASE_DEX_SWAPS_USER_SOLD_TOKEN_UNIQUE_COUNT_FOR_180DAYS: 6, + BASE_DEX_SWAPS_USER_BOUGHT_TOKEN_UNIQUE_COUNT_FOR_180DAYS: 4, + BASE_TRANSACTIONS_USER_TOTAL_ACTIONS_COUNT_FOR_180DAYS: 458275.86, + BASE_TRANSACTIONS_USER_GAS_FEE_SUM_FOR_180DAYS: 40980.95, + BASE_TRANSACTIONS_USER_GAS_FEE_AVG_FOR_180DAYS: 4, + BASE_TRANSACTIONS_USER_GAS_FEE_SUM_MEDIAN_180DAYS: 46638.91, + }, + candidates: [], + debug_info: { + UPDATED_AT: "2025-05-15T18:00:59.000Z", + }, + }, + ], + }; + + it("should handle different durations correctly", async () => { + const durations = [1, 3, 6, 12]; + const modelIds = [16, 17, 18, 19]; + + for (let i = 0; i < durations.length; i++) { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSummaryResponse, + }); + + const result = await provider.getWalletSummary({ + walletAddress: testWalletAddress, + duration: durations[i], + chain: "BASE", + }); + + // Verify the response contains duration-specific text + expect(result).toContain(`Last ${durations[i]} Month${durations[i] > 1 ? 's' : ''}`); + + // Verify correct model ID was used + const requestBody = JSON.parse(mockFetch.mock.calls[i][1].body); + expect(requestBody.model_id).toBe(modelIds[i]); + + // Verify response contains all expected sections + expect(result).toContain("Based on Pond's BASE chain analytics"); + expect(result).toContain("Activity Overview"); + expect(result).toContain("DEX Trading Summary"); + expect(result).toContain("Portfolio Diversity"); + expect(result).toContain("Gas Usage"); + } + }); + + it("should return wallet summary with mock data", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSummaryResponse, + }); + + const result = await provider.getWalletSummary({ + walletAddress: testWalletAddress, + duration: 6, + chain: "BASE", + }); + + // Verify specific data points from mock response + expect(result).toContain("47,789.43"); // Trading Volume + expect(result).toContain("43,924.34"); // PNL + expect(result).toContain("10"); // Total unique tokens (6 sold + 4 bought) + expect(result).toContain("40980.950000 ETH"); // Gas fees + }); + + it("should handle API errors gracefully", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error", + }); + + const result = await provider.getWalletSummary({ + walletAddress: testWalletAddress, + duration: 6, + chain: "BASE", + }); + + expect(result).toContain("No activity data available for"); + }); + + it("should validate wallet address format", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSummaryResponse, + }); + + const result = await provider.getWalletSummary({ + walletAddress: testWalletAddress, + duration: 6, + chain: "BASE", + }); + + expect(result).not.toContain("Error"); + }); + + it("should handle wallet address without chain", async () => { + const result = await provider.getWalletSummary({ + walletAddress: "0xd42b098abc9c23f39a053701780e20781bd3da34be905d1e9fdab26dab80661e", + duration: 12, + }); + + expect(result).toContain("Please specify which chain you would like to analyze"); + expect(result).toContain("BASE"); + expect(result).toContain("ETH"); + expect(result).toContain("SOLANA"); + }); + + it("should handle invalid chain input", async () => { + const result = await provider.getWalletSummary({ + walletAddress: "0xd42b098abc9c23f39a053701780e20781bd3da34be905d1e9fdab26dab80661e", + duration: 12, + chain: "invalid" as any + }); + + expect(result).toContain("Error getting wallet summary"); + }); + + it("should handle valid chain input", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSummaryResponse, + }); + + const result = await provider.getWalletSummary({ + walletAddress: "0xd42b098abc9c23f39a053701780e20781bd3da34be905d1e9fdab26dab80661e", + duration: 12, + chain: "ETH" + }); + + expect(result).toContain("Based on Pond's ETH chain analytics"); + expect(result).toContain("Last 12 Months"); + }); + + it("should handle invalid duration", async () => { + const result = await provider.getWalletSummary({ + walletAddress: testWalletAddress, + duration: 7, // Invalid duration + chain: "BASE", + }); + + expect(result).toContain("Invalid timeframe: 7 months"); + expect(result).toContain("Please choose one of the following timeframes"); + expect(result).toContain("1 month"); + expect(result).toContain("3 months"); + expect(result).toContain("6 months"); + expect(result).toContain("12 months"); + }); + + it("should handle empty or null response data", async () => { + const emptyResponse = { + code: 200, + msg: "Success", + resp_type: 1, + resp_items: [ + { + input_key: testWalletAddress.toLowerCase(), + score: null, + analysis_result: { + BASE_DEX_SWAPS_USER_TOTAL_ACTIONS_COUNT_FOR_180DAYS: null, + BASE_DEX_SWAPS_USER_TRADING_VOLUME_SUM_FOR_180DAYS: null, + BASE_DEX_SWAPS_USER_TRADING_VOLUME_AVG_FOR_180DAYS: null, + BASE_DEX_SWAPS_USER_TRADING_VOLUME_MEDIAN_FOR_180DAYS: null, + BASE_DEX_SWAPS_USER_TRADING_PNL_FOR_180DAYS: null, + BASE_DEX_SWAPS_USER_SOLD_TOKEN_UNIQUE_COUNT_FOR_180DAYS: null, + BASE_DEX_SWAPS_USER_BOUGHT_TOKEN_UNIQUE_COUNT_FOR_180DAYS: null, + BASE_TRANSACTIONS_USER_TOTAL_ACTIONS_COUNT_FOR_180DAYS: null, + BASE_TRANSACTIONS_USER_GAS_FEE_SUM_FOR_180DAYS: null, + BASE_TRANSACTIONS_USER_GAS_FEE_AVG_FOR_180DAYS: null, + BASE_TRANSACTIONS_USER_GAS_FEE_SUM_MEDIAN_180DAYS: null, + }, + candidates: [], + debug_info: { + UPDATED_AT: "2025-05-15T18:00:59.000Z", + }, + }, + ], + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => emptyResponse, + }); + + const result = await provider.getWalletSummary({ + walletAddress: testWalletAddress, + duration: 6, + chain: "BASE", + }); + + expect(result).toContain("N/A"); // Should show N/A for null values + expect(result).toContain("Activity Level: Low"); // Should default to Low for no activity + expect(result).toContain("0 total"); // Should show 0 for null token counts + }); + }); + + describe("getTopSolanaMemeCoins", () => { + const mockMemeCoinsResponse = { + code: 200, + resp_type: 1, + resp_items: [ + { + input_key: "LZboYF8CPRYiswZFLSQusXEaMMwMxuSA5VtjGPtpump", + score: null, + analysis_result: { + SOLANA_DEX_SWAPS_TOKEN_PRICE_CHANGE_FOR_6HOURS: 0.5, + SOLANA_DEX_SWAPS_TOKEN_TOTAL_SWAP_USD_VOLUME_SUM_FOR_6HOURS: 1000000, + SOLANA_DEX_SWAPS_TOKEN_UNIQUE_BOUGHT_USER_COUNT_FOR_6HOURS: 100, + SOLANA_DEX_SWAPS_TOKEN_UNIQUE_SOLD_USER_COUNT_FOR_6HOURS: 50, + SOLANA_DEX_SWAPS_TOKEN_TOTAL_SWAP_COUNT_FOR_6HOURS: 200, + SOLANA_DEX_SWAPS_TOKEN_USD_VOLUME_NET_FLOW_SUM_FOR_6HOURS: 50000 + }, + debug_info: { + UPDATED_AT: "2025-05-15T18:00:59.000Z" + } + } + ] + }; + + it("should handle invalid timeframe", async () => { + const result = await provider.getTopSolanaMemeCoins({ + timeframe: 8 + }); + + expect(result).toContain("Invalid timeframe: 8 hours"); + expect(result).toContain("Please choose one of the following timeframes: 3, 6, 12, 24 hours"); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("should successfully get meme coins data for valid timeframe", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockMemeCoinsResponse + }); + + const result = await provider.getTopSolanaMemeCoins({ + timeframe: 6 + }); + + expect(result).toContain("Based on Pond's Solana meme coins analytics"); + expect(result).toContain("Last 6 hours"); + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: expect.stringContaining('"model_id":37') // 6 hours model ID + }) + ); + }); + + it("should handle API errors gracefully", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error" + }); + + const result = await provider.getTopSolanaMemeCoins({ + timeframe: 6 + }); + + expect(result).toContain("Error getting top Solana meme coins"); + expect(result).toContain("API Error"); + }); + }); + + describe("getSybilPrediction", () => { + const mockSybilResponse = { + code: 200, + data: [ + { + input_key: testWalletAddress.toLowerCase(), + score: 0.7, + debug_info: { + feature_update_time: { "2025-05-11 00:00": "100.0%", "null": "0.0%" }, + not_null_feature: "100.0%" + } + } + ] + }; + + it("should successfully get sybil prediction", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSybilResponse, + }); + + const result = await provider.getSybilPrediction({ + walletAddress: testWalletAddress, + }); + + expect(result).toContain("Sybil Assessment"); + expect(result).toContain("70.00%"); + expect(result).toContain("LIKELY"); + expect(result).toContain("100.0%"); + }); + + it("should handle API errors for sybil prediction", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error", + }); + + const result = await provider.getSybilPrediction({ + walletAddress: testWalletAddress, + }); + + expect(result).toContain("Error getting Sybil prediction"); + expect(result).toContain("API Error"); + }); + }); + + describe("getTokenPricePrediction", () => { + const mockTokenPriceResponse = { + code: 200, + data: [ + { + input_key: "0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85", + score: 0.12, + debug_info: { + feature_update_time: { "2025-05-11 00:00": "100.0%", "null": "0.0%" }, + not_null_feature: "100.0%" + } + } + ] + }; + + it("should successfully get token price prediction", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockTokenPriceResponse, + }); + + const result = await provider.getTokenPricePrediction({ + tokenAddress: "0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85", + timeframe: 1, + }); + + expect(result).toContain("Price Movement Forecast"); + expect(result).toContain("12.00%"); + expect(result).toContain("VERY HIGH"); + }); + + it("should handle API errors for token price prediction", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error", + }); + + const result = await provider.getTokenPricePrediction({ + tokenAddress: "0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85", + timeframe: 1, + }); + + expect(result).toContain("Error getting price prediction"); + expect(result).toContain("API Error"); + }); + }); + + describe("getTokenRiskScores", () => { + const mockTokenRiskResponse = { + code: 200, + data: [ + { + input_key: "0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85", + score: 0.3, + debug_info: { + feature_update_time: { "2025-05-11 00:00": "100.0%", "null": "0.0%" }, + not_null_feature: "100.0%" + } + } + ] + }; + + it("should successfully get token risk scores", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockTokenRiskResponse, + }); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockTokenRiskResponse, + }); + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockTokenRiskResponse, + }); + + const result = await provider.getTokenRiskScores({ + tokenAddress: "0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85", + }); + + expect(result).toContain("Token Risk Analysis"); + expect(result).toContain("30.00%"); + expect(result).toContain("LOW"); + }); + + it("should handle API errors for token risk scores", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error", + }); + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error", + }); + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error", + }); + + const result = await provider.getTokenRiskScores({ + tokenAddress: "0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85", + }); + + expect(result).toContain("Error getting token risk scores"); + expect(result).toContain("API Error"); + }); + }); + + describe("getPumpFunPricePrediction", () => { + const mockPumpFunResponse = { + code: 200, + data: [ + { + input_key: "14Ak6KegFHLANKALmpjdn1MFW477yvesX8cdzdVEpump", + score: 0.15, + debug_info: { + feature_update_time: { "2025-05-11 00:00": "100.0%", "null": "0.0%" }, + not_null_feature: "100.0%" + } + } + ] + }; + + it("should successfully get PumpFun price prediction", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockPumpFunResponse, + }); + + const result = await provider.getPumpFunPricePrediction({ + tokenAddress: "14Ak6KegFHLANKALmpjdn1MFW477yvesX8cdzdVEpump", + timeframe: 24, + }); + + expect(result).toContain("PumpFun Price Prediction"); + expect(result).toContain("15.00%"); + expect(result).toContain("increase"); + expect(result).toContain("100.0%"); + }); + + it("should handle invalid timeframe", async () => { + const result = await provider.getPumpFunPricePrediction({ + tokenAddress: "14Ak6KegFHLANKALmpjdn1MFW477yvesX8cdzdVEpump", + timeframe: 8, + }); + + expect(result).toContain("Invalid timeframe: 8 hours"); + expect(result).toContain("Available timeframes are: 1, 3, 6, 12, 24 hours"); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("should handle API errors", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error", + }); + + const result = await provider.getPumpFunPricePrediction({ + tokenAddress: "14Ak6KegFHLANKALmpjdn1MFW477yvesX8cdzdVEpump", + timeframe: 24, + }); + + expect(result).toContain("Error getting PumpFun price prediction"); + expect(result).toContain("API Error"); + }); + }); + + describe("getZoraNFTRecommendations", () => { + const mockZoraResponse = { + code: 200, + data: [ + { + input_key: testWalletAddress.toLowerCase(), + score: null, + candidates: [ + { + item_id: "0x1234...5678", + score: 0.85 + }, + { + item_id: "0x8765...4321", + score: 0.75 + } + ], + debug_info: { + feature_update_time: { "2025-05-11 00:00": "100.0%", "null": "0.0%" }, + not_null_feature: "100.0%" + } + } + ] + }; + + it("should successfully get NFT recommendations", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockZoraResponse, + }); + + const result = await provider.getZoraNFTRecommendations({ + walletAddress: testWalletAddress, + }); + + expect(result).toContain("NFT Recommendations"); + expect(result).toContain("85.00%"); + expect(result).toContain("75.00%"); + expect(result).toContain("100.0%"); + }); + + it("should handle empty recommendations", async () => { + const emptyResponse = { + code: 200, + data: [ + { + input_key: testWalletAddress.toLowerCase(), + score: null, + candidates: [], + debug_info: { + feature_update_time: { "2025-05-11 00:00": "100.0%", "null": "0.0%" }, + not_null_feature: "100.0%" + } + } + ] + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => emptyResponse, + }); + + const result = await provider.getZoraNFTRecommendations({ + walletAddress: testWalletAddress, + }); + + expect(result).toContain("No NFT Recommendations Available"); + expect(result).toContain("Based on Pond's NFT recommendation model"); + }); + + it("should handle API errors", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error", + }); + + const result = await provider.getZoraNFTRecommendations({ + walletAddress: testWalletAddress, + }); + + expect(result).toContain("Error getting Zora NFT recommendations"); + expect(result).toContain("API Error"); + }); + }); + + describe("getSecurityAssessment", () => { + const mockSecurityResponse = { + code: 200, + data: [ + { + input_key: testWalletAddress.toLowerCase(), + score: 0.45, + debug_info: { + feature_update_time: { "2025-05-11 00:00": "100.0%", "null": "0.0%" }, + not_null_feature: "100.0%" + } + } + ] + }; + + it("should successfully get security assessment", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSecurityResponse, + }); + + const result = await provider.getSecurityAssessment({ + address: testWalletAddress, + }); + + expect(result).toContain("Security Assessment for 0xD558cE26E3e3Ca0fAc12aE116eAd9D7014a42d18"); + expect(result).toContain("Risk Score: 45.00%"); + expect(result).toContain("Risk Level: MODERATE"); + expect(result).toContain("Data Completeness: 100.0%"); + }); + + it("should handle low data completeness", async () => { + const lowDataResponse = { + code: 200, + data: [ + { + input_key: testWalletAddress.toLowerCase(), + score: 0.45, + debug_info: { + feature_update_time: { "2025-05-11 00:00": "30.0%", "null": "70.0%" }, + not_null_feature: "30.0%" + } + } + ] + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => lowDataResponse, + }); + + const result = await provider.getSecurityAssessment({ + address: testWalletAddress, + }); + + expect(result).toContain("Warning: Limited Data Available"); + expect(result).toContain("Based on Pond's security assessment model"); + expect(result).toContain("45.00%"); + expect(result).toContain("30.0%"); + }); + + it("should handle API errors", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + text: async () => "API Error", + }); + + const result = await provider.getSecurityAssessment({ + address: testWalletAddress, + }); + + expect(result).toContain("Error getting security assessment"); + expect(result).toContain("API Error"); + }); + }); +}); \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/pond/pondActionProvider.ts b/typescript/agentkit/src/action-providers/pond/pondActionProvider.ts new file mode 100644 index 000000000..26d81db38 --- /dev/null +++ b/typescript/agentkit/src/action-providers/pond/pondActionProvider.ts @@ -0,0 +1,1878 @@ +import { ActionProvider } from "../actionProvider"; +import { CreateAction } from "../actionDecorator"; +import { z } from "zod"; +import { + WalletSummarySchema, + WalletRiskScoreSchema, + SybilPredictionSchema, + TokenPricePredictionSchema, + TokenRiskScoreSchema, + TopSolanaMemeCoinsSchema, + BaseDataAnalystSchema, + EthereumDataAnalystSchema, + PumpFunPricePredictionSchema, + ZoraNFTRecommendationSchema, + SecurityModelSchema +} from "./schemas"; + +/** + * Configuration options for the PondActionProvider. + */ +export interface PondActionProviderConfig { + /** + * Pond API Key (limited to 10,000 requests) + */ + apiKey?: string; + + /** + * Base Dify API Key for Base chain data analyst queries + */ + baseDifyApiKey?: string; + + /** + * Ethereum Dify API Key for Ethereum chain data analyst queries + */ + ethDifyApiKey?: string; +} + +/** + * Action provider for interacting with Pond API + */ +export class PondActionProvider extends ActionProvider { + private readonly apiKey: string; + private readonly baseDifyApiKey: string; + private readonly ethDifyApiKey: string; + private readonly API_URL = "https://broker-service.private.cryptopond.xyz/predict"; + private readonly BASE_DIFY_API_URL = "http://100.29.38.105/v1/chat-messages"; + private readonly ETH_DIFY_API_URL = "http://100.29.38.105/v1/chat-messages"; + private static readonly DURATION_MODEL_MAP = { + BASE: { + 1: 16, // 1 month + 3: 17, // 3 months + 6: 18, // 6 months + 12: 19 // 12 months + }, + ETH: { + 1: 20, // 1 month + 3: 21, // 3 months + 6: 22, // 6 months + 12: 23 // 12 months + }, + SOLANA: { + 1: 24, // 1 month + 3: 25, // 3 months + 6: 26, // 6 months + 12: 27 // 12 months + } + }; + private static readonly RISK_SCORE_MODEL_IDS = { + DEFAULT: 40, // Default risk score model + BASE_22JE0569: 14, // Base chain risk assessment by 22je0569 + BASE_WELLSPRING: 15 // Base chain risk assessment by Wellspring Praise + }; + private static readonly SYBIL_MODEL_ID = 2; + private static readonly TOKEN_RISK_SCORE_MODEL_IDS = { + THE_DAY: 32, // 1st place in Token Risk Scoring competition + MR_HEOS: 33, // 2nd place in Token Risk Scoring competition + NILSSON_LIENE: 34 // 3rd place in Token Risk Scoring competition + }; + private static readonly PRICE_PREDICTION_MODEL_MAP = { + 1: 4, // 1 hour prediction + 3: 5, // 3 hour prediction + 6: 6, // 6 hour prediction + 12: 7, // 12 hour prediction + 24: 8 // 24 hour prediction + }; + private static readonly SOLANA_MEME_COINS_MODEL_MAP = { + 3: 36, // 3 hour prediction + 6: 37, // 6 hour prediction + 12: 38, // 12 hour prediction + 24: 39 // 24 hour prediction + }; + private static readonly BASE_DATA_ANALYST_MODEL_ID = 41; // Base data analyst model + + private static readonly TIMEFRAME_SUFFIX_MAP = { + 3: "3HOURS", + 6: "6HOURS", + 12: "12HOURS", + 24: "24HOURS" + }; + + private static readonly VOLATILITY_PREDICTION_MODEL_MAP = { + 3: 28, // 3 hour prediction + 6: 29, // 6 hour prediction + 12: 30, // 12 hour prediction + 24: 31 // 24 hour prediction + }; + + private static readonly PUMPFUN_PREDICTION_MODEL_MAP = { + 1: 9, // 1 hour prediction + 3: 10, // 3 hour prediction + 6: 11, // 6 hour prediction + 12: 12, // 12 hour prediction + 24: 13 // 24 hour prediction + }; + + private static readonly ZORA_NFT_RECOMMENDATION_MODEL_ID = 3; + private static readonly SECURITY_MODEL_ID = 1; + + /** + * Creates an instance of PondActionProvider + * + * @param config - Configuration for the Pond API including API keys + */ + constructor(config: PondActionProviderConfig = {}) { + super("pond", []); + + // Get API keys from config or environment variables + config.apiKey ||= process.env.POND_API_KEY; + config.baseDifyApiKey ||= process.env.BASE_DIFY_API_KEY; + config.ethDifyApiKey ||= process.env.ETH_DIFY_API_KEY; + + if (!config.apiKey) { + throw new Error("POND_API_KEY is not configured. Please provide it in the config or set POND_API_KEY environment variable."); + } + if (!config.baseDifyApiKey) { + throw new Error("BASE_DIFY_API_KEY is not configured. Please provide it in the config or set BASE_DIFY_API_KEY environment variable."); + } + if (!config.ethDifyApiKey) { + throw new Error("ETH_DIFY_API_KEY is not configured. Please provide it in the config or set ETH_DIFY_API_KEY environment variable."); + } + + this.apiKey = config.apiKey; + this.baseDifyApiKey = config.baseDifyApiKey; + this.ethDifyApiKey = config.ethDifyApiKey; + } + + /** + * Gets wallet risk score from Pond API + * + * @param args - Object containing the wallet address to analyze + * @returns A string containing the risk analysis in a formatted way + */ + @CreateAction({ + name: "get_wallet_risk_score", + description: ` +This tool will get a wallet risk score from Pond API. It is based on the wallet's activity and transaction history. NOT TOKEN RISK ASSESMENT +It requires an Ethereum (base) wallet address as input. + +The response will include: +- Risk score (0 to 1, where higher scores indicate higher risk) +- Feature update time information +- Feature completeness percentage + +Available models: +- DEFAULT (40): Standard risk assessment model by OlehRCL +- BASE_22JE0569 (14): Base chain risk assessment by 22je0569 +- BASE_WELLSPRING (15): Base chain risk assessment by Wellspring Praise + +Example wallet address format: 0xD558cE26E3e3Ca0fAc12aE116eAd9D7014a42d18 + +A failure response will return an error message with details. +`, + schema: WalletRiskScoreSchema.input, + }) + async getWalletRiskScore(args: z.infer): Promise { + try { + const modelId = args.model ? PondActionProvider.RISK_SCORE_MODEL_IDS[args.model] : PondActionProvider.RISK_SCORE_MODEL_IDS.DEFAULT; + + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [args.walletAddress], + model_id: modelId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json().catch(() => { + throw new Error("Invalid JSON response from Pond API"); + }); + + const parsedResponse = WalletRiskScoreSchema.response.parse(data); + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result) { + throw new Error("No risk score data available for this wallet address"); + } + + // Handle missing debug info fields gracefully + interface DebugInfo { + feature_update_time?: Record; + not_null_feature?: string; + } + const debugInfo: DebugInfo = result.debug_info || {}; + const featureUpdateTime = debugInfo.feature_update_time || {}; + const notNullFeature = debugInfo.not_null_feature || '100.0'; // Default to 100% if not provided + + // Convert score to percentage and risk level + const riskPercentage = (result.score * 100).toFixed(2); + let riskLevel = "MINIMAL"; + let riskDescription = ""; + + if (result.score > 0.8) { + riskLevel = "CRITICAL"; + riskDescription = "Extremely high-risk behavior detected"; + } else if (result.score > 0.6) { + riskLevel = "HIGH"; + riskDescription = "Significant risk factors present"; + } else if (result.score > 0.4) { + riskLevel = "MODERATE"; + riskDescription = "Some concerning patterns observed"; + } else if (result.score > 0.2) { + riskLevel = "LOW"; + riskDescription = "Minor risk indicators present"; + } else { + riskDescription = "No significant risk factors detected"; + } + + // Get the most recent feature update time + const updateTimes = Object.entries(featureUpdateTime) + .filter(([time]) => time !== "null") + .sort(([a], [b]) => b.localeCompare(a)); + const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; + + // Format the date nicely + const formattedDate = latestUpdate !== "unknown" + ? new Date(latestUpdate).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : "unknown date"; + + const modelAuthor = args.model === 'BASE_22JE0569' ? '22je0569' : + args.model === 'BASE_WELLSPRING' ? 'Wellspring Praise' : + 'OlehRCL'; + + return ` +Using Pond wallet risk scoring from ${modelAuthor} + +Wallet Risk Assessment for ${args.walletAddress}: + +• Risk Score: ${riskPercentage}% +• Risk Level: ${riskLevel} +• Risk Summary: ${riskDescription} +${debugInfo.not_null_feature ? `• Feature Completeness: ${notNullFeature}` : ''} +${latestUpdate !== "unknown" ? `• Last Updated: ${formattedDate}` : ''} + +Note: Risk scores range from 0% (lowest risk) to 100% (highest risk). +${result.score > 0.6 ? `\nWould you like to try the other risk assessment models for comparison? +• Wallet risk scoring from 22je0569 +• Wallet risk scoring from Wellspring Praise` : ''}`; + } catch (error) { + return `Error getting wallet risk score: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets wallet activity summary from Pond API for Base, Ethereum, or Solana chains + * + * @param args - Object containing the wallet address, duration, and chain to analyze + * @returns A string containing the activity summary for the specified chain in a formatted way + */ + @CreateAction({ + name: "get_wallet_summary", + description: ` +This tool will get a basic wallet activity summary from Pond API for Base, Ethereum, or Solana chains. +It is designed for simple numerical metrics and statistics only. For complex analysis or specific token information, use the Base data analyst. + +It requires: +1. A wallet address +2. Duration in months (1, 3, 6, or 12) for the summary period +3. (Optional) Chain to analyze (BASE, ETH, or SOLANA). If not specified, you will be prompted to choose one. + +The response will include ONLY basic numerical metrics: +- DEX trading activity for the specified period: + - Total number of swaps + - Trading volumes (sum, average, median) + - Trading PNL + - Number of unique tokens traded +- Transaction metrics for the specified period: + - Total transaction count + - Gas fees (sum, average, median) + +For any of the following, use the Base data analyst instead: +- Questions about specific tokens +- Analysis of buying/selling patterns +- Token preferences or strategies +- Complex wallet behavior analysis +- Detailed token holdings + +Example wallet address format: 0xD558cE26E3e3Ca0fAc12aE116eAd9D7014a42d18 +Example duration: 6 (for 6 months of data) +Example chain: BASE (for Base chain analysis) + +A failure response will return an error message with details. +`, + schema: WalletSummarySchema.input, + }) + async getWalletSummary(args: z.infer): Promise { + try { + // Check if this is a complex query that should use Base data analyst + const complexQueryKeywords = [ + 'token', 'buy', 'purchase', 'sell', 'holding', 'holdings', + 'prefer', 'strategy', 'pattern', 'behavior', 'analysis', + 'what', 'which', 'how', 'why', 'when', 'where' + ]; + + const isComplexQuery = args.query && complexQueryKeywords.some(keyword => + args.query?.toLowerCase().includes(keyword) + ); + + if (isComplexQuery && (!args.chain || args.chain.toUpperCase() === 'BASE')) { + // Redirect to Base data analyst for complex queries + return this.getBaseDataAnalysis({ + query: args.query || `What is the activity of wallet ${args.walletAddress} on Base?` + }); + } + + // Convert chain to uppercase if provided + if (args.chain) { + args.chain = args.chain.toUpperCase() as 'BASE' | 'ETH' | 'SOLANA'; + } + + // If chain is not specified, return a prompt to choose one + if (!args.chain) { + return `Please specify which chain you would like to analyze: +• BASE - For Base chain activity +• ETH - For Ethereum chain activity +• SOLANA - For Solana chain activity + +You can specify the chain by adding it to your request, for example: +"Get wallet summary for 0xD558cE26E3e3Ca0fAc12aE116eAd9D7014a42d18 for 6 months on BASE"`; + } + + // Validate timeframe + if (![1, 3, 6, 12].includes(args.duration)) { + return `Invalid timeframe: ${args.duration} months. Please choose one of the following timeframes: +• 1 month +• 3 months +• 6 months +• 12 months + +You can specify the timeframe by adding it to your request, for example: +"Get wallet summary for 0xD558cE26E3e3Ca0fAc12aE116eAd9D7014a42d18 for 6 months on ${args.chain}"`; + } + + const modelId = PondActionProvider.DURATION_MODEL_MAP[args.chain][args.duration]; + const result = await this.getChainSummary(args.walletAddress, args.duration, args.chain, modelId); + + if (!result) { + return `No activity data available for ${args.walletAddress} on ${args.chain} chain for the last ${args.duration} month${args.duration > 1 ? 's' : ''}.`; + } + + return `Based on Pond's ${args.chain} chain analytics, here's the wallet activity summary for ${args.walletAddress} (Last ${args.duration} Month${args.duration > 1 ? 's' : ''}):\n\n${result}`; + } catch (error) { + return `Error getting wallet summary: ${error instanceof Error ? error.message : String(error)}`; + } + } + + private async getChainSummary(walletAddress: string, duration: number, chain: 'BASE' | 'ETH' | 'SOLANA', modelId: number): Promise { + try { + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [walletAddress], + model_id: modelId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = WalletSummarySchema.response.parse(data); + + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result || !result.analysis_result) { + return null; + } + + const analysis = result.analysis_result; + const daySuffix = this.getDaySuffixForDuration(duration); + const chainPrefix = chain === 'BASE' ? 'BASE' : chain === 'ETH' ? 'ETH' : 'SOLANA'; + + const tradingVolume = analysis[`${chainPrefix}_DEX_SWAPS_USER_TRADING_VOLUME_SUM_FOR_${daySuffix}`] || 0; + const tradingPnl = analysis[`${chainPrefix}_DEX_SWAPS_USER_TRADING_PNL_FOR_${daySuffix}`] || 0; + const profitability = tradingVolume > 0 ? (tradingPnl / tradingVolume) * 100 : 0; + + // Calculate activity level + const swapsCount = analysis[`${chainPrefix}_DEX_SWAPS_USER_TOTAL_ACTIONS_COUNT_FOR_${daySuffix}`] || 0; + const txCount = analysis[`${chainPrefix}_TRANSACTIONS_USER_TOTAL_ACTIONS_COUNT_FOR_${daySuffix}`] || 0; + let activityLevel = "Low"; + if (txCount > 100) activityLevel = "Very High"; + else if (txCount > 50) activityLevel = "High"; + else if (txCount > 20) activityLevel = "Moderate"; + + // Format currency values + const formatUSD = (value: number | null | undefined) => + value ? value.toLocaleString('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }) : 'N/A'; + + const formatNative = (value: number | null | undefined) => + value ? `${value.toFixed(6)} ${chain === 'ETH' ? 'ETH' : chain === 'SOLANA' ? 'SOL' : 'ETH'}` : 'N/A'; + + return `${chain} Chain Activity: +Activity Overview: +• Activity Level: ${activityLevel} +• Total Transactions: ${txCount || 'N/A'} +• Average Daily Transactions: ${txCount ? (txCount / (duration * 30)).toFixed(1) : 'N/A'} + +DEX Trading Summary: +• Total Swaps: ${swapsCount || 'N/A'} +• Trading Volume: ${formatUSD(tradingVolume)} +• Trading Performance: + - Total PNL: ${formatUSD(tradingPnl)} + - Profitability: ${profitability.toFixed(2)}% + - Average Trade Size: ${formatUSD(analysis[`${chainPrefix}_DEX_SWAPS_USER_TRADING_VOLUME_AVG_FOR_${daySuffix}`])} + - Median Trade Size: ${formatUSD(analysis[`${chainPrefix}_DEX_SWAPS_USER_TRADING_VOLUME_MEDIAN_FOR_${daySuffix}`])} + +Portfolio Diversity: +• Unique Tokens Traded: ${( + (analysis[`${chainPrefix}_DEX_SWAPS_USER_BOUGHT_TOKEN_UNIQUE_COUNT_FOR_${daySuffix}`] || 0) + + (analysis[`${chainPrefix}_DEX_SWAPS_USER_SOLD_TOKEN_UNIQUE_COUNT_FOR_${daySuffix}`] || 0) + )} total + - Tokens Bought: ${analysis[`${chainPrefix}_DEX_SWAPS_USER_BOUGHT_TOKEN_UNIQUE_COUNT_FOR_${daySuffix}`] || 'N/A'} + - Tokens Sold: ${analysis[`${chainPrefix}_DEX_SWAPS_USER_SOLD_TOKEN_UNIQUE_COUNT_FOR_${daySuffix}`] || 'N/A'} + +Gas Usage: +• Total Gas Spent: ${formatNative(analysis[`${chainPrefix}_TRANSACTIONS_USER_GAS_FEE_SUM_FOR_${daySuffix}`])} +• Average Gas per Tx: ${formatNative(analysis[`${chainPrefix}_TRANSACTIONS_USER_GAS_FEE_AVG_FOR_${daySuffix}`])} +• Median Gas per Tx: ${formatNative(analysis[`${chainPrefix}_TRANSACTIONS_USER_GAS_FEE_SUM_MEDIAN_${daySuffix}`])} + +Last Updated: ${new Date(result.debug_info.UPDATED_AT).toLocaleString()}`; + + } catch (error) { + console.error(`Error getting ${chain} chain summary:`, error); + return null; + } + } + + private getDaySuffixForDuration(duration: number): string { + switch (duration) { + case 1: + return "30DAYS"; + case 3: + return "90DAYS"; + case 6: + return "180DAYS"; + case 12: + return "360DAYS"; + default: + throw new Error(`Invalid duration: ${duration}`); + } + } + + /** + * Gets Sybil prediction score for a wallet address from Pond API + * + * @param args - Object containing the wallet address to analyze + * @returns A string containing the Sybil prediction analysis in a formatted way + */ + @CreateAction({ + name: "get_sybil_prediction", + description: ` +This tool will get a Sybil prediction score from Pond API to detect potential multi-account wallets. +It requires an Ethereum wallet address as input. + +The response will include: +- Sybil score (0 to 1, where higher scores indicate higher likelihood of being a Sybil address) +- Feature update time information +- Feature completeness percentage + +Example wallet address format: 0xD558cE26E3e3Ca0fAc12aE116eAd9D7014a42d18 + +A failure response will return an error message with details. +`, + schema: SybilPredictionSchema.input, + }) + async getSybilPrediction(args: z.infer): Promise { + try { + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [args.walletAddress], + model_id: PondActionProvider.SYBIL_MODEL_ID, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = SybilPredictionSchema.response.parse(data); + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result) { + throw new Error("No Sybil prediction data available for this wallet address"); + } + + if (!result.debug_info.not_null_feature || parseFloat(result.debug_info.not_null_feature) < 50) { + return ` +Warning: Limited Data Available + +Based on Pond's Sybil detection model, the analysis for ${args.walletAddress} is incomplete due to insufficient data: +• Data Completeness: ${result.debug_info.not_null_feature || '0'}% +• Sybil Score: ${(result.score * 100).toFixed(2)}% (low confidence) + +Note: This score may not be reliable due to limited available data. +Please check if this is a new or inactive wallet address. +`; + } + + // Convert score to percentage and Sybil likelihood level + const sybilPercentage = (result.score * 100).toFixed(2); + let sybilLevel = "VERY UNLIKELY"; + let sybilDescription = ""; + + if (result.score > 0.8) { + sybilLevel = "VERY LIKELY"; + sybilDescription = "Strong indicators of Sybil behavior detected"; + } else if (result.score > 0.6) { + sybilLevel = "LIKELY"; + sybilDescription = "Significant Sybil patterns observed"; + } else if (result.score > 0.4) { + sybilLevel = "POSSIBLE"; + sybilDescription = "Some Sybil-like patterns present"; + } else if (result.score > 0.2) { + sybilLevel = "UNLIKELY"; + sybilDescription = "Minor Sybil indicators present"; + } else { + sybilDescription = "No significant Sybil behavior detected"; + } + + // Get the most recent feature update time + const updateTimes = Object.entries(result.debug_info.feature_update_time) + .filter(([time]) => time !== "null") + .sort(([a], [b]) => b.localeCompare(a)); + const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; + + // Format the date nicely + const formattedDate = latestUpdate !== "unknown" + ? new Date(latestUpdate).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : "unknown date"; + + return ` +Based on Pond's Sybil detection model (created by Jerry), this wallet has been analyzed: + +Sybil Assessment: +• Sybil Score: ${sybilPercentage}% +• Likelihood: ${sybilLevel} +• Analysis: ${sybilDescription} +• Feature Completeness: ${result.debug_info.not_null_feature} +• Last Updated: ${formattedDate} + +Note: Sybil scores range from 0% (lowest likelihood) to 100% (highest likelihood) of being a multi-account wallet. +`; + + } catch (error) { + return `Error getting Sybil prediction: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets token price prediction from Pond API + * + * @param args - Object containing the token address and timeframe to predict + * @returns A string containing the price prediction analysis in a formatted way + */ + @CreateAction({ + name: "get_token_price_prediction", + description: ` +This tool will get a token price prediction from Pond API for tokens on Base chain. NOT CURRENT PRICE. only use this for future price predictions. +It requires: +1. A Base chain token contract address +2. Timeframe in hours (1, 3, 6, 12, or 24) for the prediction period + +The response will include: +- Predicted price change percentage +- Prediction confidence based on data completeness +- Last feature update time + +Example token address format: 0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85 +Example timeframe: 1 (for 1 hour prediction) + +A failure response will return an error message with details. +`, + schema: TokenPricePredictionSchema.input, + }) + async getTokenPricePrediction(args: z.infer): Promise { + try { + const modelId = PondActionProvider.PRICE_PREDICTION_MODEL_MAP[args.timeframe]; + + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [args.tokenAddress], + model_id: modelId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = TokenPricePredictionSchema.response.parse(data); + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result) { + throw new Error("No price prediction data available for this token"); + } + + if (!result.debug_info.not_null_feature || parseFloat(result.debug_info.not_null_feature) < 50) { + return ` +Warning: Limited Data Available + +Based on Pond's price prediction model, the analysis for ${args.tokenAddress} is incomplete due to insufficient data: +• Data Completeness: ${result.debug_info.not_null_feature || '0'}% +• Predicted Change: ${(result.score * 100).toFixed(2)}% (low confidence) + +Note: This prediction may not be reliable due to limited available data. +Please check if this is a new or illiquid token. +`; + } + + // Convert score to percentage and determine prediction direction + const priceChangePercent = (result.score * 100).toFixed(2); + const direction = result.score >= 0 ? "increase" : "decrease"; + const magnitude = Math.abs(result.score); + let confidenceLevel = "LOW"; + let movementDescription = "minimal"; + + if (magnitude > 0.1) { + confidenceLevel = "VERY HIGH"; + movementDescription = "significant"; + } else if (magnitude > 0.05) { + confidenceLevel = "HIGH"; + movementDescription = "notable"; + } else if (magnitude > 0.02) { + confidenceLevel = "MODERATE"; + movementDescription = "moderate"; + } else if (magnitude > 0.01) { + confidenceLevel = "LOW"; + movementDescription = "slight"; + } + + // Get the most recent feature update time + const updateTimes = Object.entries(result.debug_info.feature_update_time) + .filter(([time]) => time !== "null") + .sort(([a], [b]) => b.localeCompare(a)); + const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; + + // Format the date nicely + const formattedDate = latestUpdate !== "unknown" + ? new Date(latestUpdate).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : "unknown date"; + + return ` +Based on Pond's price prediction model for Base chain tokens: + +Price Movement Forecast: +• Direction: ${direction.toUpperCase()} +• Magnitude: ${movementDescription.toUpperCase()} +• Predicted Change: ${priceChangePercent}% +• Confidence Level: ${confidenceLevel} + +Analysis Details: +• Token Address: ${args.tokenAddress} +• Data Completeness: ${result.debug_info.not_null_feature} +• Last Updated: ${formattedDate} + +Note: This prediction is based on on-chain metrics and historical patterns. +Past performance does not guarantee future results. +`; + + } catch (error) { + return `Error getting price prediction: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets token risk scores from multiple models on Pond API + * + * @param args - Object containing the token address to analyze + * @returns A string containing the risk analysis from all three models in a formatted way + */ + @CreateAction({ + name: "get_token_risk_scores", + description: ` +This tool will get token risk scores from three different models on Pond API for Base chain tokens. +These models were the top 3 winners of Pond's Token Risk Scoring competition for Base chain tokens. + +The response will include risk scores from three different models: +1. The Day's model (1st place winner) - Model ID: 32 +2. Mr. Heos's model (2nd place winner) - Model ID: 33 +3. Nilsson Liene's model (3rd place winner) - Model ID: 34 + +Each model provides: +- Risk score (0 to 1, where higher scores indicate higher risk) +- Feature completeness percentage +- Last update time + +Example token address format: 0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85 + +A failure response will return an error message with details. +`, + schema: TokenRiskScoreSchema.input, + }) + async getTokenRiskScores(args: z.infer): Promise { + try { + const results = await Promise.all( + Object.entries(PondActionProvider.TOKEN_RISK_SCORE_MODEL_IDS).map(async ([modelName, modelId]) => { + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [args.tokenAddress], + model_id: modelId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = TokenRiskScoreSchema.response.parse(data); + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result) { + throw new Error(`No risk score data available for this token from ${modelName}'s model`); + } + + return { + modelName, + result + }; + }) + ); + + // Format the results + const formatRiskLevel = (score: number): { level: string; description: string } => { + if (score > 0.8) { + return { level: "CRITICAL", description: "Extremely high-risk token" }; + } else if (score > 0.6) { + return { level: "HIGH", description: "Significant risk factors present" }; + } else if (score > 0.4) { + return { level: "MODERATE", description: "Some concerning patterns observed" }; + } else if (score > 0.2) { + return { level: "LOW", description: "Minor risk indicators present" }; + } else { + return { level: "MINIMAL", description: "No significant risk factors detected" }; + } + }; + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + const formattedResults = results.map(({ modelName, result }) => { + const riskPercentage = (result.score * 100).toFixed(2); + const { level, description } = formatRiskLevel(result.score); + const updateTimes = Object.entries(result.debug_info.feature_update_time) + .filter(([time]) => time !== "null") + .sort(([a], [b]) => b.localeCompare(a)); + const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; + const formattedDate = latestUpdate !== "unknown" ? formatDate(latestUpdate) : "unknown date"; + + return { + modelName, + riskPercentage, + level, + description, + dataCompleteness: result.debug_info.not_null_feature, + lastUpdated: formattedDate + }; + }); + + return `Based on Pond's token risk assessment models for Base chain tokens: + +Token Risk Analysis for ${args.tokenAddress}: + +${formattedResults.map(result => + `${result.modelName}'s Model (${result.modelName === 'THE_DAY' ? '1st' : result.modelName === 'MR_HEOS' ? '2nd' : '3rd'} Place):\n` + + `• Risk Score: ${result.riskPercentage}%\n` + + `• Risk Level: ${result.level}\n` + + `• Analysis: ${result.description}\n` + + `• Data Completeness: ${result.dataCompleteness}\n` + + `• Last Updated: ${result.lastUpdated}\n\n` +).join('')}Note: Risk scores range from 0% (lowest risk) to 100% (highest risk). +Each model may use different methodologies and data points for risk assessment.`; + + } catch (error) { + return `Error getting token risk scores: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets top 10 Solana meme coins from Pond API + * + * @param args - Object containing the timeframe to analyze + * @returns A string containing the top 10 meme coins analysis in a formatted way + */ + @CreateAction({ + name: "get_top_solana_meme_coins", + description: ` +This tool will get Solana meme coin analytics from Pond API for a set of token addresses. +It requires: +- A timeframe in hours (3, 6, 12, or 24) for the analysis period + +The response will include for each token: +- Price change percentage +- Trading volume and activity +- Number of unique traders +- Market metrics + +Example timeframe: 3 (for 3 hours analysis) +Example addresses: ["abcdefghijklmnopqrstuvwxyz1234567890", ...] (dont ask for address for this action, it will always give top 10 tokens by default) + +A failure response will return an error message with details. +`, + schema: TopSolanaMemeCoinsSchema.input, + }) + async getTopSolanaMemeCoins(args: z.infer): Promise { + // Validate timeframe first + if (!PondActionProvider.SOLANA_MEME_COINS_MODEL_MAP[args.timeframe]) { + const validTimeframes = Object.keys(PondActionProvider.SOLANA_MEME_COINS_MODEL_MAP).join(', '); + return `Invalid timeframe: ${args.timeframe} hours. I will request with available timeframe. Please choose one of the following timeframes: ${validTimeframes} hours on the next request`; + } + + try { + const modelId = PondActionProvider.SOLANA_MEME_COINS_MODEL_MAP[args.timeframe]; + const timeframeSuffix = PondActionProvider.TIMEFRAME_SUFFIX_MAP[args.timeframe]; + + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: args.addresses || ["LZboYF8CPRYiswZFLSQusXEaMMwMxuSA5VtjGPtpump"], + model_id: modelId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = TopSolanaMemeCoinsSchema.response.parse(data); + + // Get the coins data from either data or resp_items + const coins = parsedResponse.data || parsedResponse.resp_items; + if (!coins || coins.length === 0) { + throw new Error("No meme coins data available"); + } + + // Sort tokens by price change descending + const sortedCoins = coins.slice().sort((a, b) => { + const aChange = a.analysis_result[`SOLANA_DEX_SWAPS_TOKEN_PRICE_CHANGE_FOR_${timeframeSuffix}`] || 0; + const bChange = b.analysis_result[`SOLANA_DEX_SWAPS_TOKEN_PRICE_CHANGE_FOR_${timeframeSuffix}`] || 0; + return bChange - aChange; + }); + + // Helper function to format numbers + const formatNumber = (num: number | null | undefined) => { + if (num === null || num === undefined) return 'N/A'; + return num.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + }; + + const formatUSD = (num: number | null | undefined) => { + if (num === null || num === undefined) return 'N/A'; + return `$${num.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 8 + })}`; + }; + + // Prepare explanation for top 3 tokens (show detailed metrics) + const top3 = sortedCoins.slice(0, 3).map((coin, idx) => { + const a = coin.analysis_result; + const priceChange = a[`SOLANA_DEX_SWAPS_TOKEN_PRICE_CHANGE_FOR_${timeframeSuffix}`] || 0; + const volume = a[`SOLANA_DEX_SWAPS_TOKEN_TOTAL_SWAP_USD_VOLUME_SUM_FOR_${timeframeSuffix}`] || 0; + const uniqueTraders = (a[`SOLANA_DEX_SWAPS_TOKEN_UNIQUE_BOUGHT_USER_COUNT_FOR_${timeframeSuffix}`] || 0) + + (a[`SOLANA_DEX_SWAPS_TOKEN_UNIQUE_SOLD_USER_COUNT_FOR_${timeframeSuffix}`] || 0); + const totalSwaps = a[`SOLANA_DEX_SWAPS_TOKEN_TOTAL_SWAP_COUNT_FOR_${timeframeSuffix}`] || 0; + const netFlow = a[`SOLANA_DEX_SWAPS_TOKEN_USD_VOLUME_NET_FLOW_SUM_FOR_${timeframeSuffix}`] || 0; + + return `#${idx + 1} ${coin.input_key} + • Price Change: ${(priceChange * 100).toFixed(8)}% + • Trading Volume: ${formatUSD(volume)} + • Trading Activity: + - Total Swaps: ${formatNumber(totalSwaps)} + - Unique Traders: ${formatNumber(uniqueTraders)} + - Net USD Flow: ${formatUSD(netFlow)}`; + }); + + // For the rest, show price change, volume, and unique traders + const rest = sortedCoins.slice(3).map((coin, idx) => { + const a = coin.analysis_result; + const priceChange = a[`SOLANA_DEX_SWAPS_TOKEN_PRICE_CHANGE_FOR_${timeframeSuffix}`] || 0; + const volume = a[`SOLANA_DEX_SWAPS_TOKEN_TOTAL_SWAP_USD_VOLUME_SUM_FOR_${timeframeSuffix}`] || 0; + const uniqueTraders = (a[`SOLANA_DEX_SWAPS_TOKEN_UNIQUE_BOUGHT_USER_COUNT_FOR_${timeframeSuffix}`] || 0) + + (a[`SOLANA_DEX_SWAPS_TOKEN_UNIQUE_SOLD_USER_COUNT_FOR_${timeframeSuffix}`] || 0); + + return `#${idx + 4} ${coin.input_key} + • Price Change: ${(priceChange * 100).toFixed(8)}% + • Volume: ${formatUSD(volume)} + • Unique Traders: ${formatNumber(uniqueTraders)}`; + }); + + const lastUpdated = coins[0].debug_info.UPDATED_AT; + const formattedDate = new Date(lastUpdated).toLocaleString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + + return `Based on Pond's Solana meme coins analytics (Last ${args.timeframe} hours): + +Top Performers: +${top3.join('\n\n')} + +Other Trending Meme Coins: +${rest.join('\n\n')} + +Last Updated: ${formattedDate} + +Note: +• Price change is percentage over the period +• Trading volume is total DEX trading volume +• Net USD flow represents net buy/sell volume +• Past performance does not guarantee future results`; + + } catch (error) { + return `Error getting top Solana meme coins: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets Base chain data analysis from Pond API + * + * @param args - Object containing the query to analyze + * @returns A string containing the data analysis in a formatted way + */ + @CreateAction({ + name: "get_base_data_analysis", + description: ` +This tool will get Base chain data analysis from Pond API. +It requires a query string as input. + +The response will include: +- Analysis results based on the query +- Data completeness metrics +- Last update timestamp + +Example query: "What are the most popular NFTs on base today?" + +A failure response will return an error message with details. +`, + schema: BaseDataAnalystSchema.input, + }) + async getBaseDataAnalysis(args: z.infer): Promise { + if (!this.baseDifyApiKey) { + return "Error: Base Dify API key is required. Please provide it when initializing PondActionProvider."; + } + + try { + const response = await fetch(this.BASE_DIFY_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.baseDifyApiKey}` + }, + body: JSON.stringify({ + inputs: {}, + query: args.query, + response_mode: "streaming", + conversation_id: "", + user: "abc-123" + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Dify API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + // Handle streaming response + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Failed to get response reader"); + } + + let fullResponse = ""; + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + buffer += chunk; + + // Process complete lines from the buffer + const lines = buffer.split('\n'); + buffer = lines.pop() || ""; // Keep the last incomplete line in the buffer + + for (const line of lines) { + if (line.startsWith('data: ')) { + const jsonStr = line.slice(6); // Remove 'data: ' prefix + if (jsonStr === '[DONE]') continue; + + try { + const data = JSON.parse(jsonStr); + if (data.event === 'message' && data.answer) { + fullResponse += data.answer; + } + } catch { + // Silently skip parsing errors for incomplete JSON + continue; + } + } + } + } + + if (!fullResponse) { + throw new Error("No valid response received from the API"); + } + + return `Using Pond's Base Data Analyst model.\n\n${fullResponse}\n\nLast Updated: ${new Date().toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + })}`; + } catch (error) { + return `Error getting Base data analysis: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets Eth chain data analysis from Pond API + * + * @param args - Object containing the query to analyze + * @returns A string containing the data analysis in a formatted way + */ + @CreateAction({ + name: "get_eth_data_analysis", + description: ` +This tool will get Eth chain data analysis from Pond API. +It requires a query string as input. + +The response will include: +- Analysis results based on the query +- Data completeness metrics +- Last update timestamp + +Example query: "What are the most popular NFTs on Eth today?" + +A failure response will return an error message with details. +`, + schema: EthereumDataAnalystSchema.input, + }) + async getEthDataAnalysis(args: z.infer): Promise { + if (!this.ethDifyApiKey) { + return "Error: Eth Dify API key is required. Please provide it when initializing PondActionProvider."; + } + + try { + const response = await fetch(this.ETH_DIFY_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.ethDifyApiKey}` + }, + body: JSON.stringify({ + inputs: {}, + query: args.query, + response_mode: "streaming", + conversation_id: "", + user: "abc-123" + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Dify API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + // Handle streaming response + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("Failed to get response reader"); + } + + let fullResponse = ""; + const decoder = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + buffer += chunk; + + // Process complete lines from the buffer + const lines = buffer.split('\n'); + buffer = lines.pop() || ""; // Keep the last incomplete line in the buffer + + for (const line of lines) { + if (line.startsWith('data: ')) { + const jsonStr = line.slice(6); // Remove 'data: ' prefix + if (jsonStr === '[DONE]') continue; + + try { + const data = JSON.parse(jsonStr); + if (data.event === 'message' && data.answer) { + fullResponse += data.answer; + } + } catch { + // Silently skip parsing errors for incomplete JSON + continue; + } + } + } + } + + if (!fullResponse) { + throw new Error("No valid response received from the API"); + } + + return `Using Pond's Ethereum Data Analyst model.\n\n${fullResponse}\n\nLast Updated: ${new Date().toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + })}`; + } catch (error) { + return `Error getting Ethereum data analysis: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets token volatility prediction from Pond API + * + * @param args - Object containing the token address and timeframe to predict + * @returns A string containing the volatility prediction analysis in a formatted way + */ + @CreateAction({ + name: "get_token_volatility_prediction", + description: ` +This tool will get a token volatility prediction from Pond API for tokens on Base chain. +It requires: +1. A Base chain token contract address +2. Timeframe in hours (3, 6, 12, or 24) for the prediction period + +The response will include: +- Predicted volatility percentage +- Prediction confidence based on data completeness +- Last feature update time + +Example token address format: 0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85 +Example timeframe: 3 (for 3 hour prediction) + +A failure response will return an error message with details. +`, + schema: TokenPricePredictionSchema.input, + }) + async getTokenVolatilityPrediction(args: z.infer): Promise { + try { + // Handle missing or invalid timeframe + if (!args.timeframe || !PondActionProvider.VOLATILITY_PREDICTION_MODEL_MAP[args.timeframe]) { + const validTimeframes = Object.keys(PondActionProvider.VOLATILITY_PREDICTION_MODEL_MAP).join(', '); + const defaultTimeframe = 24; + const modelId = PondActionProvider.VOLATILITY_PREDICTION_MODEL_MAP[defaultTimeframe]; + + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [args.tokenAddress], + model_id: modelId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = TokenPricePredictionSchema.response.parse(data); + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result) { + throw new Error("No volatility prediction data available for this token"); + } + + if (!result.debug_info.not_null_feature || parseFloat(result.debug_info.not_null_feature) < 50) { + return ` +Warning: Limited Data Available + +Based on Pond's volatility prediction model, the analysis for ${args.tokenAddress} is incomplete due to insufficient data: +• Data Completeness: ${result.debug_info.not_null_feature || '0'}% +• Predicted Volatility: ${(result.score * 100).toFixed(2)}% (low confidence) + +Note: This prediction may not be reliable due to limited available data. +Please check if this is a new or illiquid token. + +Note: The requested timeframe is not available. Available timeframes are: ${validTimeframes} hours. +For the closest prediction to your request, please use one of these timeframes. +`; + } + + // Convert score to percentage + const volatilityPercent = (result.score * 100).toFixed(2); + + // Get the most recent feature update time + const updateTimes = Object.entries(result.debug_info.feature_update_time) + .filter(([time]) => time !== "null") + .sort(([a], [b]) => b.localeCompare(a)); + const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; + + // Format the date nicely + const formattedDate = latestUpdate !== "unknown" + ? new Date(latestUpdate).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : "unknown date"; + + return ` +Volatility Prediction for ${args.tokenAddress} (${defaultTimeframe}h): + +• Predicted Volatility: ${volatilityPercent}% +• Data Completeness: ${result.debug_info.not_null_feature} +• Last Updated: ${formattedDate} + +Note: This prediction is based on on-chain metrics and historical patterns. +Past performance does not guarantee future results. + +Note: The requested timeframe is not available. Available timeframes are: ${validTimeframes} hours. +For the closest prediction to your request, please use one of these timeframes. +`; + } + + const modelId = PondActionProvider.VOLATILITY_PREDICTION_MODEL_MAP[args.timeframe]; + + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [args.tokenAddress], + model_id: modelId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = TokenPricePredictionSchema.response.parse(data); + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result) { + throw new Error("No volatility prediction data available for this token"); + } + + if (!result.debug_info.not_null_feature || parseFloat(result.debug_info.not_null_feature) < 50) { + return ` +Warning: Limited Data Available + +Based on Pond's volatility prediction model, the analysis for ${args.tokenAddress} is incomplete due to insufficient data: +• Data Completeness: ${result.debug_info.not_null_feature || '0'}% +• Predicted Volatility: ${(result.score * 100).toFixed(2)}% (low confidence) + +Note: This prediction may not be reliable due to limited available data. +Please check if this is a new or illiquid token. +`; + } + + // Convert score to percentage + const volatilityPercent = (result.score * 100).toFixed(2); + + // Get the most recent feature update time + const updateTimes = Object.entries(result.debug_info.feature_update_time) + .filter(([time]) => time !== "null") + .sort(([a], [b]) => b.localeCompare(a)); + const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; + + // Format the date nicely + const formattedDate = latestUpdate !== "unknown" + ? new Date(latestUpdate).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : "unknown date"; + + return ` +Volatility Prediction for ${args.tokenAddress} (${args.timeframe}h): + +• Predicted Volatility: ${volatilityPercent}% +• Data Completeness: ${result.debug_info.not_null_feature} +• Last Updated: ${formattedDate} + +Note: This prediction is based on on-chain metrics and historical patterns. +Past performance does not guarantee future results. +`; + + } catch (error) { + return `Error getting volatility prediction: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets PumpFun price prediction from Pond API + * + * @param args - Object containing the token address and timeframe to predict + * @returns A string containing the price prediction analysis in a formatted way + */ + @CreateAction({ + name: "get_pumpfun_price_prediction", + description: ` +This tool will get a PumpFun price prediction from Pond API for tokens on Base chain. +It requires: +1. A Solana token address +2. Timeframe in hours (1, 3, 6, 12, or 24) for the prediction period + +The response will include: +- Predicted price change percentage +- Prediction confidence based on data completeness +- Last feature update time + +Example token address format: 14Ak6KegFHLANKALmpjdn1MFW477yvesX8cdzdVEpump +Example timeframe: 1 (for 1 hour prediction) + +A failure response will return an error message with details. +`, + schema: PumpFunPricePredictionSchema.input, + }) + async getPumpFunPricePrediction(args: z.infer): Promise { + try { + // Handle missing or invalid timeframe + if (!args.timeframe || !PondActionProvider.PUMPFUN_PREDICTION_MODEL_MAP[args.timeframe]) { + const validTimeframes = Object.keys(PondActionProvider.PUMPFUN_PREDICTION_MODEL_MAP).join(', '); + return `Invalid timeframe: ${args.timeframe} hours. Available timeframes are: ${validTimeframes} hours.`; + } + + const modelId = PondActionProvider.PUMPFUN_PREDICTION_MODEL_MAP[args.timeframe]; + + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [args.tokenAddress], + model_id: modelId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = PumpFunPricePredictionSchema.response.parse(data); + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result) { + throw new Error("No price prediction data available for this token"); + } + + if (!result.debug_info.not_null_feature || parseFloat(result.debug_info.not_null_feature) < 50) { + return ` +Warning: Limited Data Available + +Based on Pond's PumpFun price prediction model, the analysis for ${args.tokenAddress} is incomplete due to insufficient data: +• Data Completeness: ${result.debug_info.not_null_feature || '0'}% +• Predicted Change: ${(result.score * 100).toFixed(2)}% (low confidence) + +Note: This prediction may not be reliable due to limited available data. +Please check if this is a new or illiquid token. +`; + } + + // Convert score to percentage + const priceChangePercent = (result.score * 100).toFixed(2); + const direction = result.score >= 0 ? "increase" : "decrease"; + + // Get the most recent feature update time + const updateTimes = Object.entries(result.debug_info.feature_update_time) + .filter(([time]) => time !== "null") + .sort(([a], [b]) => b.localeCompare(a)); + const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; + + // Format the date nicely + const formattedDate = latestUpdate !== "unknown" + ? new Date(latestUpdate).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : "unknown date"; + + return ` +PumpFun Price Prediction for ${args.tokenAddress} (${args.timeframe}h): + +• Predicted Change: ${priceChangePercent}% (${direction}) +• Data Completeness: ${result.debug_info.not_null_feature} +• Last Updated: ${formattedDate} + +Note: This prediction is based on on-chain metrics and historical patterns. +Past performance does not guarantee future results. +`; + + } catch (error) { + return `Error getting PumpFun price prediction: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets Zora NFT recommendations from Pond API + * + * @param args - Object containing the wallet address to get recommendations for + * @returns A string containing the NFT recommendations in a formatted way + */ + @CreateAction({ + name: "get_zora_nft_recommendations", + description: ` +This tool will get Zora NFT recommendations from Pond API based on a wallet's preferences and activity. +It requires an Ethereum wallet address as input. + +The response will include: +- Top NFT recommendations with similarity scores +- Collection addresses and token IDs +- Recommendation confidence based on data completeness + +Example wallet address format: 0x306d676e137736264fb7fbccec280b589dffcbd1 + +A failure response will return an error message with details. +`, + schema: ZoraNFTRecommendationSchema.input, + }) + async getZoraNFTRecommendations(args: z.infer): Promise { + try { + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [args.walletAddress], + model_id: PondActionProvider.ZORA_NFT_RECOMMENDATION_MODEL_ID, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = ZoraNFTRecommendationSchema.response.parse(data); + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result) { + throw new Error("No NFT recommendations available for this wallet address"); + } + + if (!result.candidates || result.candidates.length === 0) { + return ` +No NFT Recommendations Available + +Based on Pond's NFT recommendation model, no recommendations could be generated for ${args.walletAddress}. +This could be due to: +• Limited wallet activity +• No matching preferences found +• Insufficient data for recommendations + +Please try again later or check if this is a new or inactive wallet address. +`; + } + + if (!result.debug_info.not_null_feature || parseFloat(result.debug_info.not_null_feature) < 50) { + return ` +Warning: Limited Data Available + +Based on Pond's NFT recommendation model, the analysis for ${args.walletAddress} is incomplete due to insufficient data: +• Data Completeness: ${result.debug_info.not_null_feature || '0'}% + +Note: These recommendations may not be reliable due to limited available data. +Please check if this is a new or inactive wallet address. +`; + } + + // Format the recommendations + const recommendations = result.candidates + .slice(0, 10) // Get top 10 recommendations + .map((candidate, index) => { + const [collectionAddress, tokenId] = candidate.item_id.split('_'); + const similarityScore = (candidate.score * 100).toFixed(2); + return `${index + 1}. Collection: ${collectionAddress} + • Token ID: ${tokenId} + • Similarity Score: ${similarityScore}%`; + }) + .join('\n\n'); + + // Get the most recent feature update time + const updateTimes = Object.entries(result.debug_info.feature_update_time) + .filter(([time]) => time !== "null") + .sort(([a], [b]) => b.localeCompare(a)); + const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; + + // Format the date nicely + const formattedDate = latestUpdate !== "unknown" + ? new Date(latestUpdate).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : "unknown date"; + + return ` +Based on Pond's Zora NFT recommendation model for ${args.walletAddress}: + +Top NFT Recommendations: +${recommendations} + +Data Quality: +• Data Completeness: ${result.debug_info.not_null_feature} +• Last Updated: ${formattedDate} + +Note: Similarity scores range from 0% to 100%, where higher scores indicate better matches to your preferences. +These recommendations are based on your wallet's activity and preferences on Zora. +`; + + } catch (error) { + return `Error getting Zora NFT recommendations: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Gets security assessment from Pond API + * + * @param args - Object containing the address to analyze + * @returns A string containing the security assessment in a formatted way + */ + @CreateAction({ + name: "get_security_assessment", + description: ` +This tool will get a security assessment from Pond API to identify potential security risks. +It analyzes patterns of behavior to pinpoint malicious addresses, phishing accounts, and vulnerable smart contracts. + +It requires an Ethereum address as input. + +The response will include: +- Security risk score (0 to 1, where higher scores indicate higher risk) +- Risk level categorization +- Feature completeness metrics +- Last update timestamp + +Example address format: 0xd885c064ddaa010d4a1735dcbaa3cd8fe52e127e + +A failure response will return an error message with details. +`, + schema: SecurityModelSchema.input, + }) + async getSecurityAssessment(args: z.infer): Promise { + try { + const response = await fetch(this.API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + req_type: "1", + access_token: this.apiKey, + input_keys: [args.address], + model_id: PondActionProvider.SECURITY_MODEL_ID, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error("Pond API service is currently unavailable. Please try again later."); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } + + const data = await response.json(); + const parsedResponse = SecurityModelSchema.response.parse(data); + const result = parsedResponse.data?.[0] || parsedResponse.resp_items?.[0]; + + if (!result) { + throw new Error("No security assessment data available for this address"); + } + + if (!result.debug_info.not_null_feature || parseFloat(result.debug_info.not_null_feature) < 50) { + return ` +Warning: Limited Data Available + +Based on Pond's security assessment model, the analysis for ${args.address} is incomplete due to insufficient data: +• Data Completeness: ${result.debug_info.not_null_feature || '0'}% +• Risk Score: ${(result.score * 100).toFixed(2)}% (low confidence) + +Note: This assessment may not be reliable due to limited available data. +Please check if this is a new or inactive address. +`; + } + + // Convert score to percentage and determine risk level + const riskPercentage = (result.score * 100).toFixed(2); + let riskLevel = "SAFE"; + let riskDescription = ""; + + if (result.score > 0.8) { + riskLevel = "CRITICAL"; + riskDescription = "Extremely high-risk behavior detected. Strong indicators of malicious activity."; + } else if (result.score > 0.6) { + riskLevel = "HIGH"; + riskDescription = "Significant security risks present. Exercise extreme caution."; + } else if (result.score > 0.4) { + riskLevel = "MODERATE"; + riskDescription = "Some concerning patterns observed. Proceed with caution."; + } else if (result.score > 0.2) { + riskLevel = "LOW"; + riskDescription = "Minor risk indicators present. Standard security measures recommended."; + } else { + riskDescription = "No significant security risks detected. Standard security practices still recommended."; + } + + // Get the most recent feature update time + const updateTimes = Object.entries(result.debug_info.feature_update_time) + .filter(([time]) => time !== "null") + .sort(([a], [b]) => b.localeCompare(a)); + const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; + + // Format the date nicely + const formattedDate = latestUpdate !== "unknown" + ? new Date(latestUpdate).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + : "unknown date"; + + return ` +Security Assessment for ${args.address}: + +• Risk Score: ${riskPercentage}% +• Risk Level: ${riskLevel} +• Assessment: ${riskDescription} +• Data Completeness: ${result.debug_info.not_null_feature} +• Last Updated: ${formattedDate} + +Note: Risk scores range from 0% (lowest risk) to 100% (highest risk). +This assessment is based on behavioral patterns and known security indicators. +`; + + } catch (error) { + return `Error getting security assessment: ${error instanceof Error ? error.message : String(error)}`; + } + } + + /** + * Checks if the provider supports a given network + * + * @returns Always returns true as Pond service supports Ethereum + */ + supportsNetwork(): boolean { + return true; // Pond service supports Ethereum network + } +} + +export const pondActionProvider = (config: PondActionProviderConfig = {}) => + new PondActionProvider(config); \ No newline at end of file diff --git a/typescript/agentkit/src/action-providers/pond/schemas.ts b/typescript/agentkit/src/action-providers/pond/schemas.ts new file mode 100644 index 000000000..f06c3e369 --- /dev/null +++ b/typescript/agentkit/src/action-providers/pond/schemas.ts @@ -0,0 +1,542 @@ +import { z } from "zod"; + +/** + * Schema for wallet summary from Pond + */ +export const WalletSummarySchema = { + input: z + .object({ + walletAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum wallet address format"), + duration: z + .number() + .int() + .min(1) + .max(12) + .refine( + (val) => [1, 3, 6, 12].includes(val), + "Duration must be 1, 3, 6, or 12 months" + ), + chain: z + .enum(['BASE', 'ETH', 'SOLANA']) + .optional() + .describe("The blockchain to analyze. If not specified, you will be prompted to choose one."), + query: z.string().optional().describe("Optional query string to determine if this is a token purchase query") + }) + .strip() + .describe("Instructions for getting wallet activity summary from Pond's analytics platform"), + + response: z.object({ + code: z.number(), + msg: z.string(), + resp_type: z.union([z.string(), z.number()]), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number().nullable(), + analysis_result: z.record(z.any()), + debug_info: z.object({ + UPDATED_AT: z.union([z.string(), z.number()]), + }), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number().nullable(), + analysis_result: z.record(z.any()), + debug_info: z.object({ + UPDATED_AT: z.union([z.string(), z.number()]), + }), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's response", + } + ).describe("Response schema for Pond's wallet activity summary") +}; + +/** + * Schema for wallet risk score from Pond + */ +export const WalletRiskScoreSchema = { + input: z + .object({ + walletAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/) + .describe("The Ethereum wallet address to analyze for risk using Pond's risk model"), + model: z + .enum(['DEFAULT', 'BASE_22JE0569', 'BASE_WELLSPRING']) + .optional() + .describe("The risk assessment model to use. If not specified, the default model will be used."), + }) + .strip() + .describe("Instructions for getting wallet risk score from Pond's risk assessment model"), + + response: z.object({ + code: z.number(), + msg: z.string().optional(), + resp_type: z.number().optional(), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()).optional(), + not_null_feature: z.string().optional(), + }).optional(), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()).optional(), + not_null_feature: z.string().optional(), + }).optional(), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's response", + } + ).describe("Response schema for Pond's wallet risk assessment model") +}; + +/** + * Schema for Sybil prediction from Pond + */ +export const SybilPredictionSchema = { + input: z + .object({ + walletAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/, 'Must be a valid Ethereum address') + .describe("The Ethereum wallet address to analyze using Pond's Sybil detection model"), + }) + .strip() + .describe("Instructions for getting Sybil prediction from Pond's Sybil detection model"), + + response: z.object({ + code: z.number(), + msg: z.string().optional(), + resp_type: z.number().optional(), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's Sybil detection response", + } + ).describe("Response schema for Pond's Sybil detection model") +}; + +/** + * Schema for token price prediction from Pond + */ +export const TokenPricePredictionSchema = { + input: z + .object({ + tokenAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/) + .describe("The Base chain token address to analyze using Pond's price prediction model"), + timeframe: z + .number() + .refine((val) => [1, 3, 6, 12, 24].includes(val), { + message: "Timeframe must be 1, 3, 6, 12, or 24 hours", + }) + .describe("Prediction timeframe in hours for Pond's price forecast (1, 3, 6, 12, or 24)"), + }) + .strip() + .describe("Instructions for getting token price prediction from Pond's price prediction model"), + + response: z.object({ + code: z.number(), + msg: z.string().optional(), + resp_type: z.number().optional(), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's price prediction response", + } + ).describe("Response schema for Pond's token price prediction model") +}; + +/** + * Schema for token risk scoring from Pond + */ +export const TokenRiskScoreSchema = { + input: z.object({ + tokenAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/) + .describe("The Base chain token contract address to analyze"), + }).strip().describe("Instructions for getting token risk scores from Pond's risk assessment models"), + + response: z.object({ + code: z.number(), + msg: z.string().optional(), + resp_type: z.number().optional(), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string(), + UPDATED_AT: z.string().optional() + }), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string(), + UPDATED_AT: z.string().optional() + }), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's response", + } + ).describe("Response schema for Pond's token risk scoring models") +}; + +/** + * Schema for top 10 Solana meme coins from Pond + */ +export const TopSolanaMemeCoinsSchema = { + input: z.object({ + timeframe: z.number(), + addresses: z.array(z.string()).default(["LZboYF8CPRYiswZFLSQusXEaMMwMxuSA5VtjGPtpump"]).optional(), + }), + response: z.object({ + code: z.number(), + message: z.string().optional(), + msg: z.string().optional(), + resp_type: z.union([z.string(), z.number()]), + data: z.array(z.object({ + input_key: z.string(), + score: z.number().nullable().optional(), + analysis_result: z.record(z.any()), + debug_info: z.object({ + UPDATED_AT: z.union([z.string(), z.number()]), + not_null_feature: z.union([z.string(), z.number()]).optional(), + feature_update_time: z.record(z.any()).optional(), + }), + })).optional(), + resp_items: z.array(z.object({ + input_key: z.string(), + score: z.number().nullable().optional(), + analysis_result: z.record(z.any()), + debug_info: z.object({ + UPDATED_AT: z.union([z.string(), z.number()]), + not_null_feature: z.union([z.string(), z.number()]).optional(), + feature_update_time: z.record(z.any()).optional(), + }), + })).optional(), + }) + .refine( + (data) => data.data || data.resp_items, + "Either data or resp_items must be present in Pond's response" + ) + .transform((obj) => ({ + ...obj, + message: obj.message || obj.msg || "" + })), +}; + +/** + * Schema for Base data analyst from Pond + */ +export const BaseDataAnalystSchema = { + input: z.object({ + query: z.string().describe("The query to analyze using Pond's Base data analyst model"), + }).strip().describe("Instructions for getting Base chain data analysis from Pond's data analyst model"), + + response: z.object({ + code: z.number(), + msg: z.string().optional(), + resp_type: z.union([z.string(), z.number()]), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number().nullable().optional(), + analysis_result: z.record(z.any()), + debug_info: z.object({ + UPDATED_AT: z.union([z.string(), z.number()]), + not_null_feature: z.union([z.string(), z.number()]).optional(), + feature_update_time: z.record(z.any()).optional(), + }), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number().nullable().optional(), + analysis_result: z.record(z.any()), + debug_info: z.object({ + UPDATED_AT: z.union([z.string(), z.number()]), + not_null_feature: z.union([z.string(), z.number()]).optional(), + feature_update_time: z.record(z.any()).optional(), + }), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's response", + } + ).describe("Response schema for Pond's Base data analyst model") +}; + +/** + * Schema for Ethereum data analyst from Pond + */ +export const EthereumDataAnalystSchema = { + input: z.object({ + query: z.string().describe("The query to analyze using Pond's Ethereum data analyst model"), + }).strip().describe("Instructions for getting Ethereum chain data analysis from Pond's data analyst model"), + + response: z.object({ + code: z.number(), + msg: z.string().optional(), + resp_type: z.union([z.string(), z.number()]), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number().nullable().optional(), + analysis_result: z.record(z.any()), + debug_info: z.object({ + UPDATED_AT: z.union([z.string(), z.number()]), + not_null_feature: z.union([z.string(), z.number()]).optional(), + feature_update_time: z.record(z.any()).optional(), + }), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number().nullable().optional(), + analysis_result: z.record(z.any()), + debug_info: z.object({ + UPDATED_AT: z.union([z.string(), z.number()]), + not_null_feature: z.union([z.string(), z.number()]).optional(), + feature_update_time: z.record(z.any()).optional(), + }), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's response", + } + ).describe("Response schema for Pond's Ethereum data analyst model") +}; + +/** + * Schema for PumpFun price prediction from Pond + */ +export const PumpFunPricePredictionSchema = { + input: z + .object({ + tokenAddress: z + .string() + .min(32) + .max(44) + .describe("The Solana token address to analyze using Pond's PumpFun price prediction model"), + timeframe: z + .number() + .refine((val) => [1, 3, 6, 12, 24].includes(val), { + message: "Timeframe must be 1, 3, 6, 12, or 24 hours", + }) + .describe("Prediction timeframe in hours for Pond's price forecast (1, 3, 6, 12, or 24)"), + }) + .strip() + .describe("Instructions for getting PumpFun price prediction from Pond's price prediction model"), + + response: z.object({ + code: z.number(), + msg: z.string().optional(), + resp_type: z.number().optional(), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's price prediction response", + } + ).describe("Response schema for Pond's PumpFun price prediction model") +}; + +/** + * Schema for Zora NFT recommendations from Pond + */ +export const ZoraNFTRecommendationSchema = { + input: z + .object({ + walletAddress: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/) + .describe("The Ethereum wallet address to get NFT recommendations for"), + }) + .strip() + .describe("Instructions for getting Zora NFT recommendations from Pond's recommendation model"), + + response: z.object({ + code: z.number(), + msg: z.string().optional(), + resp_type: z.number().optional(), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number().nullable(), + candidates: z.array( + z.object({ + item_id: z.string(), + score: z.number() + }) + ), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number().nullable(), + candidates: z.array( + z.object({ + item_id: z.string(), + score: z.number() + }) + ), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's response", + } + ).describe("Response schema for Pond's Zora NFT recommendation model") +}; + +/** + * Schema for security model from Pond + */ +export const SecurityModelSchema = { + input: z + .object({ + address: z + .string() + .regex(/^0x[a-fA-F0-9]{40}$/) + .describe("The Ethereum address to analyze for security risks"), + }) + .strip() + .describe("Instructions for getting security assessment from Pond's security model"), + + response: z.object({ + code: z.number(), + msg: z.string().optional(), + resp_type: z.number().optional(), + data: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + resp_items: z.array( + z.object({ + input_key: z.string(), + score: z.number(), + debug_info: z.object({ + feature_update_time: z.record(z.string()), + not_null_feature: z.string() + }), + }) + ).optional(), + }).refine( + (data) => data.data !== undefined || data.resp_items !== undefined, + { + message: "Either data or resp_items must be present in Pond's response", + } + ).describe("Response schema for Pond's security assessment model") +}; \ No newline at end of file From 7a86241823563c16ff555fb39fd3c040d77681fe Mon Sep 17 00:00:00 2001 From: Theofilus Kharisma Date: Fri, 30 May 2025 06:21:05 +0700 Subject: [PATCH 2/2] feat: add Pond action provider integration and example usage --- .../agentkit/src/action-providers/index.ts | 1 + .../pond/pondActionProvider.ts | 505 +++--------------- .../examples/langchain-cdp-chatbot/.env-local | 4 + .../examples/langchain-cdp-chatbot/chatbot.ts | 2 + 4 files changed, 85 insertions(+), 427 deletions(-) diff --git a/typescript/agentkit/src/action-providers/index.ts b/typescript/agentkit/src/action-providers/index.ts index 13856ca53..2d2409cb9 100644 --- a/typescript/agentkit/src/action-providers/index.ts +++ b/typescript/agentkit/src/action-providers/index.ts @@ -19,6 +19,7 @@ export * from "./pyth"; export * from "./moonwell"; export * from "./morpho"; export * from "./opensea"; +export * from "./pond"; export * from "./spl"; export * from "./twitter"; export * from "./wallet"; diff --git a/typescript/agentkit/src/action-providers/pond/pondActionProvider.ts b/typescript/agentkit/src/action-providers/pond/pondActionProvider.ts index 26d81db38..4ce6d06fd 100644 --- a/typescript/agentkit/src/action-providers/pond/pondActionProvider.ts +++ b/typescript/agentkit/src/action-providers/pond/pondActionProvider.ts @@ -35,6 +35,32 @@ export interface PondActionProviderConfig { ethDifyApiKey?: string; } +// Add at the top, after imports +async function handlePondApiError(response: Response, service: string = "Pond API"): Promise { + if (!response.ok) { + const errorText = await response.text(); + if (response.status === 429) { + throw new Error("Rate limit exceeded. Please try again later."); + } else if (response.status === 401) { + throw new Error("Invalid API key or authentication failed."); + } else if (response.status >= 500) { + throw new Error(`${service} service is currently unavailable. Please try again later.`); + } + throw new Error(`API Error (${response.status}): ${errorText}`); + } +} + +function formatDate(dateStr: string | number): string { + if (!dateStr || dateStr === "unknown") return "unknown date"; + return new Date(dateStr).toLocaleDateString('en-US', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +} + /** * Action provider for interacting with Pond API */ @@ -89,7 +115,7 @@ export class PondActionProvider extends ActionProvider { 12: 38, // 12 hour prediction 24: 39 // 24 hour prediction }; - private static readonly BASE_DATA_ANALYST_MODEL_ID = 41; // Base data analyst model + private static readonly TIMEFRAME_SUFFIX_MAP = { 3: "3HOURS", @@ -153,7 +179,7 @@ export class PondActionProvider extends ActionProvider { @CreateAction({ name: "get_wallet_risk_score", description: ` -This tool will get a wallet risk score from Pond API. It is based on the wallet's activity and transaction history. NOT TOKEN RISK ASSESMENT +This tool will get a wallet risk score from Pond API. It is based on the wallet's activity and transaction history. NOT TOKEN RISSESMENT It requires an Ethereum (base) wallet address as input. The response will include: @@ -162,7 +188,7 @@ The response will include: - Feature completeness percentage Available models: -- DEFAULT (40): Standard risk assessment model by OlehRCL +- DEFAULT (40): Wallet risk assessment model by OlehRCL - BASE_22JE0569 (14): Base chain risk assessment by 22je0569 - BASE_WELLSPRING (15): Base chain risk assessment by Wellspring Praise @@ -189,17 +215,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json().catch(() => { throw new Error("Invalid JSON response from Pond API"); @@ -223,42 +239,13 @@ A failure response will return an error message with details. // Convert score to percentage and risk level const riskPercentage = (result.score * 100).toFixed(2); - let riskLevel = "MINIMAL"; - let riskDescription = ""; - if (result.score > 0.8) { - riskLevel = "CRITICAL"; - riskDescription = "Extremely high-risk behavior detected"; - } else if (result.score > 0.6) { - riskLevel = "HIGH"; - riskDescription = "Significant risk factors present"; - } else if (result.score > 0.4) { - riskLevel = "MODERATE"; - riskDescription = "Some concerning patterns observed"; - } else if (result.score > 0.2) { - riskLevel = "LOW"; - riskDescription = "Minor risk indicators present"; - } else { - riskDescription = "No significant risk factors detected"; - } - // Get the most recent feature update time const updateTimes = Object.entries(featureUpdateTime) .filter(([time]) => time !== "null") .sort(([a], [b]) => b.localeCompare(a)); const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; - // Format the date nicely - const formattedDate = latestUpdate !== "unknown" - ? new Date(latestUpdate).toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - : "unknown date"; - const modelAuthor = args.model === 'BASE_22JE0569' ? '22je0569' : args.model === 'BASE_WELLSPRING' ? 'Wellspring Praise' : 'OlehRCL'; @@ -269,15 +256,8 @@ Using Pond wallet risk scoring from ${modelAuthor} Wallet Risk Assessment for ${args.walletAddress}: • Risk Score: ${riskPercentage}% -• Risk Level: ${riskLevel} -• Risk Summary: ${riskDescription} ${debugInfo.not_null_feature ? `• Feature Completeness: ${notNullFeature}` : ''} -${latestUpdate !== "unknown" ? `• Last Updated: ${formattedDate}` : ''} - -Note: Risk scores range from 0% (lowest risk) to 100% (highest risk). -${result.score > 0.6 ? `\nWould you like to try the other risk assessment models for comparison? -• Wallet risk scoring from 22je0569 -• Wallet risk scoring from Wellspring Praise` : ''}`; +• Last Updated: ${formatDate(latestUpdate)}`; } catch (error) { return `Error getting wallet risk score: ${error instanceof Error ? error.message : String(error)}`; } @@ -293,7 +273,7 @@ ${result.score > 0.6 ? `\nWould you like to try the other risk assessment models name: "get_wallet_summary", description: ` This tool will get a basic wallet activity summary from Pond API for Base, Ethereum, or Solana chains. -It is designed for simple numerical metrics and statistics only. For complex analysis or specific token information, use the Base data analyst. +It is designed for simple numerical metrics and statistics only. For complex analysis or specific token bought,sold, traded, use the Base/ethereum data analyst. It requires: 1. A wallet address @@ -401,17 +381,7 @@ You can specify the timeframe by adding it to your request, for example: }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = WalletSummarySchema.response.parse(data); @@ -478,7 +448,7 @@ Gas Usage: • Average Gas per Tx: ${formatNative(analysis[`${chainPrefix}_TRANSACTIONS_USER_GAS_FEE_AVG_FOR_${daySuffix}`])} • Median Gas per Tx: ${formatNative(analysis[`${chainPrefix}_TRANSACTIONS_USER_GAS_FEE_SUM_MEDIAN_${daySuffix}`])} -Last Updated: ${new Date(result.debug_info.UPDATED_AT).toLocaleString()}`; +Last Updated: ${formatDate(result.debug_info.UPDATED_AT)}`; } catch (error) { console.error(`Error getting ${chain} chain summary:`, error); @@ -539,17 +509,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = SybilPredictionSchema.response.parse(data); @@ -574,54 +534,20 @@ Please check if this is a new or inactive wallet address. // Convert score to percentage and Sybil likelihood level const sybilPercentage = (result.score * 100).toFixed(2); - let sybilLevel = "VERY UNLIKELY"; - let sybilDescription = ""; - - if (result.score > 0.8) { - sybilLevel = "VERY LIKELY"; - sybilDescription = "Strong indicators of Sybil behavior detected"; - } else if (result.score > 0.6) { - sybilLevel = "LIKELY"; - sybilDescription = "Significant Sybil patterns observed"; - } else if (result.score > 0.4) { - sybilLevel = "POSSIBLE"; - sybilDescription = "Some Sybil-like patterns present"; - } else if (result.score > 0.2) { - sybilLevel = "UNLIKELY"; - sybilDescription = "Minor Sybil indicators present"; - } else { - sybilDescription = "No significant Sybil behavior detected"; - } - + // Get the most recent feature update time const updateTimes = Object.entries(result.debug_info.feature_update_time) .filter(([time]) => time !== "null") .sort(([a], [b]) => b.localeCompare(a)); const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; - // Format the date nicely - const formattedDate = latestUpdate !== "unknown" - ? new Date(latestUpdate).toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - : "unknown date"; - return ` Based on Pond's Sybil detection model (created by Jerry), this wallet has been analyzed: Sybil Assessment: • Sybil Score: ${sybilPercentage}% -• Likelihood: ${sybilLevel} -• Analysis: ${sybilDescription} • Feature Completeness: ${result.debug_info.not_null_feature} -• Last Updated: ${formattedDate} - -Note: Sybil scores range from 0% (lowest likelihood) to 100% (highest likelihood) of being a multi-account wallet. -`; +• Last Updated: ${formatDate(latestUpdate)}`; } catch (error) { return `Error getting Sybil prediction: ${error instanceof Error ? error.message : String(error)}`; @@ -671,17 +597,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = TokenPricePredictionSchema.response.parse(data); @@ -699,66 +615,28 @@ Based on Pond's price prediction model, the analysis for ${args.tokenAddress} is • Data Completeness: ${result.debug_info.not_null_feature || '0'}% • Predicted Change: ${(result.score * 100).toFixed(2)}% (low confidence) -Note: This prediction may not be reliable due to limited available data. -Please check if this is a new or illiquid token. -`; +Note: This prediction is based on on-chain metrics and historical patterns. +Past performance does not guarantee future results.`; } - // Convert score to percentage and determine prediction direction + // Convert score to percentage const priceChangePercent = (result.score * 100).toFixed(2); const direction = result.score >= 0 ? "increase" : "decrease"; - const magnitude = Math.abs(result.score); - let confidenceLevel = "LOW"; - let movementDescription = "minimal"; - - if (magnitude > 0.1) { - confidenceLevel = "VERY HIGH"; - movementDescription = "significant"; - } else if (magnitude > 0.05) { - confidenceLevel = "HIGH"; - movementDescription = "notable"; - } else if (magnitude > 0.02) { - confidenceLevel = "MODERATE"; - movementDescription = "moderate"; - } else if (magnitude > 0.01) { - confidenceLevel = "LOW"; - movementDescription = "slight"; - } - + // Get the most recent feature update time const updateTimes = Object.entries(result.debug_info.feature_update_time) .filter(([time]) => time !== "null") .sort(([a], [b]) => b.localeCompare(a)); const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; - // Format the date nicely - const formattedDate = latestUpdate !== "unknown" - ? new Date(latestUpdate).toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - : "unknown date"; - return ` Based on Pond's price prediction model for Base chain tokens: Price Movement Forecast: -• Direction: ${direction.toUpperCase()} -• Magnitude: ${movementDescription.toUpperCase()} +• Direction: ${direction} • Predicted Change: ${priceChangePercent}% -• Confidence Level: ${confidenceLevel} - -Analysis Details: -• Token Address: ${args.tokenAddress} • Data Completeness: ${result.debug_info.not_null_feature} -• Last Updated: ${formattedDate} - -Note: This prediction is based on on-chain metrics and historical patterns. -Past performance does not guarantee future results. -`; +• Last Updated: ${formatDate(latestUpdate)}`; } catch (error) { return `Error getting price prediction: ${error instanceof Error ? error.message : String(error)}`; @@ -810,17 +688,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = TokenRiskScoreSchema.response.parse(data); @@ -838,46 +706,18 @@ A failure response will return an error message with details. ); // Format the results - const formatRiskLevel = (score: number): { level: string; description: string } => { - if (score > 0.8) { - return { level: "CRITICAL", description: "Extremely high-risk token" }; - } else if (score > 0.6) { - return { level: "HIGH", description: "Significant risk factors present" }; - } else if (score > 0.4) { - return { level: "MODERATE", description: "Some concerning patterns observed" }; - } else if (score > 0.2) { - return { level: "LOW", description: "Minor risk indicators present" }; - } else { - return { level: "MINIMAL", description: "No significant risk factors detected" }; - } - }; - - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - }; - const formattedResults = results.map(({ modelName, result }) => { const riskPercentage = (result.score * 100).toFixed(2); - const { level, description } = formatRiskLevel(result.score); const updateTimes = Object.entries(result.debug_info.feature_update_time) .filter(([time]) => time !== "null") .sort(([a], [b]) => b.localeCompare(a)); const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; - const formattedDate = latestUpdate !== "unknown" ? formatDate(latestUpdate) : "unknown date"; return { modelName, riskPercentage, - level, - description, dataCompleteness: result.debug_info.not_null_feature, - lastUpdated: formattedDate + lastUpdated: formatDate(latestUpdate) }; }); @@ -888,8 +728,6 @@ Token Risk Analysis for ${args.tokenAddress}: ${formattedResults.map(result => `${result.modelName}'s Model (${result.modelName === 'THE_DAY' ? '1st' : result.modelName === 'MR_HEOS' ? '2nd' : '3rd'} Place):\n` + `• Risk Score: ${result.riskPercentage}%\n` + - `• Risk Level: ${result.level}\n` + - `• Analysis: ${result.description}\n` + `• Data Completeness: ${result.dataCompleteness}\n` + `• Last Updated: ${result.lastUpdated}\n\n` ).join('')}Note: Risk scores range from 0% (lowest risk) to 100% (highest risk). @@ -950,17 +788,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = TopSolanaMemeCoinsSchema.response.parse(data); @@ -1029,14 +857,6 @@ A failure response will return an error message with details. }); const lastUpdated = coins[0].debug_info.UPDATED_AT; - const formattedDate = new Date(lastUpdated).toLocaleString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); return `Based on Pond's Solana meme coins analytics (Last ${args.timeframe} hours): @@ -1046,7 +866,7 @@ ${top3.join('\n\n')} Other Trending Meme Coins: ${rest.join('\n\n')} -Last Updated: ${formattedDate} +Last Updated: ${formatDate(lastUpdated)} Note: • Price change is percentage over the period @@ -1103,17 +923,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Dify API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response, "Dify API"); // Handle streaming response const reader = response.body?.getReader(); @@ -1158,13 +968,7 @@ A failure response will return an error message with details. throw new Error("No valid response received from the API"); } - return `Using Pond's Base Data Analyst model.\n\n${fullResponse}\n\nLast Updated: ${new Date().toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - })}`; + return `Using Pond's Base Data Analyst model.\n\n${fullResponse}\n\nLast Updated: ${formatDate(new Date().toISOString())}`; } catch (error) { return `Error getting Base data analysis: ${error instanceof Error ? error.message : String(error)}`; } @@ -1179,15 +983,16 @@ A failure response will return an error message with details. @CreateAction({ name: "get_eth_data_analysis", description: ` -This tool will get Eth chain data analysis from Pond API. +This tool will answer complex Ethereum data-based question from Pond API. It requires a query string as input. The response will include: -- Analysis results based on the query +- Analysis results based on the query (already LLM interpreted and user friendly) - Data completeness metrics - Last update timestamp -Example query: "What are the most popular NFTs on Eth today?" +Example query: "What are the most popular NFTs on Eth today?", "What are the top trending tokens in the last 12 hours on Ethereum?" + A failure response will return an error message with details. `, @@ -1214,17 +1019,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Dify API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response, "Dify API"); // Handle streaming response const reader = response.body?.getReader(); @@ -1269,13 +1064,7 @@ A failure response will return an error message with details. throw new Error("No valid response received from the API"); } - return `Using Pond's Ethereum Data Analyst model.\n\n${fullResponse}\n\nLast Updated: ${new Date().toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - })}`; + return `Using Pond's Ethereum Data Analyst model.\n\n${fullResponse}\n\nLast Updated: ${formatDate(new Date().toISOString())}`; } catch (error) { return `Error getting Ethereum data analysis: ${error instanceof Error ? error.message : String(error)}`; } @@ -1328,17 +1117,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = TokenPricePredictionSchema.response.parse(data); @@ -1356,8 +1135,8 @@ Based on Pond's volatility prediction model, the analysis for ${args.tokenAddres • Data Completeness: ${result.debug_info.not_null_feature || '0'}% • Predicted Volatility: ${(result.score * 100).toFixed(2)}% (low confidence) -Note: This prediction may not be reliable due to limited available data. -Please check if this is a new or illiquid token. +Note: This prediction is based on on-chain metrics and historical patterns. +Past performance does not guarantee future results. Note: The requested timeframe is not available. Available timeframes are: ${validTimeframes} hours. For the closest prediction to your request, please use one of these timeframes. @@ -1373,30 +1152,15 @@ For the closest prediction to your request, please use one of these timeframes. .sort(([a], [b]) => b.localeCompare(a)); const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; - // Format the date nicely - const formattedDate = latestUpdate !== "unknown" - ? new Date(latestUpdate).toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - : "unknown date"; - return ` Volatility Prediction for ${args.tokenAddress} (${defaultTimeframe}h): • Predicted Volatility: ${volatilityPercent}% • Data Completeness: ${result.debug_info.not_null_feature} -• Last Updated: ${formattedDate} +• Last Updated: ${formatDate(latestUpdate)} Note: This prediction is based on on-chain metrics and historical patterns. -Past performance does not guarantee future results. - -Note: The requested timeframe is not available. Available timeframes are: ${validTimeframes} hours. -For the closest prediction to your request, please use one of these timeframes. -`; +Past performance does not guarantee future results.`; } const modelId = PondActionProvider.VOLATILITY_PREDICTION_MODEL_MAP[args.timeframe]; @@ -1414,17 +1178,7 @@ For the closest prediction to your request, please use one of these timeframes. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = TokenPricePredictionSchema.response.parse(data); @@ -1442,9 +1196,8 @@ Based on Pond's volatility prediction model, the analysis for ${args.tokenAddres • Data Completeness: ${result.debug_info.not_null_feature || '0'}% • Predicted Volatility: ${(result.score * 100).toFixed(2)}% (low confidence) -Note: This prediction may not be reliable due to limited available data. -Please check if this is a new or illiquid token. -`; +Note: This prediction is based on on-chain metrics and historical patterns. +Past performance does not guarantee future results.`; } // Convert score to percentage @@ -1456,27 +1209,15 @@ Please check if this is a new or illiquid token. .sort(([a], [b]) => b.localeCompare(a)); const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; - // Format the date nicely - const formattedDate = latestUpdate !== "unknown" - ? new Date(latestUpdate).toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - : "unknown date"; - return ` Volatility Prediction for ${args.tokenAddress} (${args.timeframe}h): • Predicted Volatility: ${volatilityPercent}% • Data Completeness: ${result.debug_info.not_null_feature} -• Last Updated: ${formattedDate} +• Last Updated: ${formatDate(latestUpdate)} Note: This prediction is based on on-chain metrics and historical patterns. -Past performance does not guarantee future results. -`; +Past performance does not guarantee future results.`; } catch (error) { return `Error getting volatility prediction: ${error instanceof Error ? error.message : String(error)}`; @@ -1532,17 +1273,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = PumpFunPricePredictionSchema.response.parse(data); @@ -1560,9 +1291,8 @@ Based on Pond's PumpFun price prediction model, the analysis for ${args.tokenAdd • Data Completeness: ${result.debug_info.not_null_feature || '0'}% • Predicted Change: ${(result.score * 100).toFixed(2)}% (low confidence) -Note: This prediction may not be reliable due to limited available data. -Please check if this is a new or illiquid token. -`; +Note: This prediction is based on on-chain metrics and historical patterns. +Past performance does not guarantee future results.`; } // Convert score to percentage @@ -1575,27 +1305,15 @@ Please check if this is a new or illiquid token. .sort(([a], [b]) => b.localeCompare(a)); const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; - // Format the date nicely - const formattedDate = latestUpdate !== "unknown" - ? new Date(latestUpdate).toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - : "unknown date"; - return ` PumpFun Price Prediction for ${args.tokenAddress} (${args.timeframe}h): • Predicted Change: ${priceChangePercent}% (${direction}) • Data Completeness: ${result.debug_info.not_null_feature} -• Last Updated: ${formattedDate} +• Last Updated: ${formatDate(latestUpdate)} Note: This prediction is based on on-chain metrics and historical patterns. -Past performance does not guarantee future results. -`; +Past performance does not guarantee future results.`; } catch (error) { return `Error getting PumpFun price prediction: ${error instanceof Error ? error.message : String(error)}`; @@ -1640,17 +1358,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = ZoraNFTRecommendationSchema.response.parse(data); @@ -1704,17 +1412,6 @@ Please check if this is a new or inactive wallet address. .sort(([a], [b]) => b.localeCompare(a)); const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; - // Format the date nicely - const formattedDate = latestUpdate !== "unknown" - ? new Date(latestUpdate).toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - : "unknown date"; - return ` Based on Pond's Zora NFT recommendation model for ${args.walletAddress}: @@ -1723,11 +1420,10 @@ ${recommendations} Data Quality: • Data Completeness: ${result.debug_info.not_null_feature} -• Last Updated: ${formattedDate} +• Last Updated: ${formatDate(latestUpdate)} Note: Similarity scores range from 0% to 100%, where higher scores indicate better matches to your preferences. -These recommendations are based on your wallet's activity and preferences on Zora. -`; +These recommendations are based on your wallet's activity and preferences on Zora.`; } catch (error) { return `Error getting Zora NFT recommendations: ${error instanceof Error ? error.message : String(error)}`; @@ -1775,17 +1471,7 @@ A failure response will return an error message with details. }), }); - if (!response.ok) { - const errorText = await response.text(); - if (response.status === 429) { - throw new Error("Rate limit exceeded. Please try again later."); - } else if (response.status === 401) { - throw new Error("Invalid API key or authentication failed."); - } else if (response.status >= 500) { - throw new Error("Pond API service is currently unavailable. Please try again later."); - } - throw new Error(`API Error (${response.status}): ${errorText}`); - } + await handlePondApiError(response); const data = await response.json(); const parsedResponse = SecurityModelSchema.response.parse(data); @@ -1803,61 +1489,26 @@ Based on Pond's security assessment model, the analysis for ${args.address} is i • Data Completeness: ${result.debug_info.not_null_feature || '0'}% • Risk Score: ${(result.score * 100).toFixed(2)}% (low confidence) -Note: This assessment may not be reliable due to limited available data. +Note: This assessment is based on behavioral patterns and known security indicators. Please check if this is a new or inactive address. `; } // Convert score to percentage and determine risk level const riskPercentage = (result.score * 100).toFixed(2); - let riskLevel = "SAFE"; - let riskDescription = ""; - - if (result.score > 0.8) { - riskLevel = "CRITICAL"; - riskDescription = "Extremely high-risk behavior detected. Strong indicators of malicious activity."; - } else if (result.score > 0.6) { - riskLevel = "HIGH"; - riskDescription = "Significant security risks present. Exercise extreme caution."; - } else if (result.score > 0.4) { - riskLevel = "MODERATE"; - riskDescription = "Some concerning patterns observed. Proceed with caution."; - } else if (result.score > 0.2) { - riskLevel = "LOW"; - riskDescription = "Minor risk indicators present. Standard security measures recommended."; - } else { - riskDescription = "No significant security risks detected. Standard security practices still recommended."; - } - + // Get the most recent feature update time const updateTimes = Object.entries(result.debug_info.feature_update_time) .filter(([time]) => time !== "null") .sort(([a], [b]) => b.localeCompare(a)); const latestUpdate = updateTimes[0] ? updateTimes[0][0] : "unknown"; - // Format the date nicely - const formattedDate = latestUpdate !== "unknown" - ? new Date(latestUpdate).toLocaleDateString('en-US', { - day: 'numeric', - month: 'long', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }) - : "unknown date"; - return ` Security Assessment for ${args.address}: • Risk Score: ${riskPercentage}% -• Risk Level: ${riskLevel} -• Assessment: ${riskDescription} • Data Completeness: ${result.debug_info.not_null_feature} -• Last Updated: ${formattedDate} - -Note: Risk scores range from 0% (lowest risk) to 100% (highest risk). -This assessment is based on behavioral patterns and known security indicators. -`; +• Last Updated: ${formatDate(latestUpdate)}`; } catch (error) { return `Error getting security assessment: ${error instanceof Error ? error.message : String(error)}`; diff --git a/typescript/examples/langchain-cdp-chatbot/.env-local b/typescript/examples/langchain-cdp-chatbot/.env-local index 2e36cd9d9..ca7465230 100644 --- a/typescript/examples/langchain-cdp-chatbot/.env-local +++ b/typescript/examples/langchain-cdp-chatbot/.env-local @@ -1,3 +1,7 @@ +CDP_WALLET_SECRET= OPENAI_API_KEY= CDP_API_KEY_NAME= CDP_API_KEY_PRIVATE_KEY= +BASE_DIFY_API_KEY= +ETH_DIFY_API_KEY= +POND_API_KEY=sk-00009796-3jxfa7fyTjCkjvAw8Dka \ No newline at end of file diff --git a/typescript/examples/langchain-cdp-chatbot/chatbot.ts b/typescript/examples/langchain-cdp-chatbot/chatbot.ts index 182c82517..deec70260 100644 --- a/typescript/examples/langchain-cdp-chatbot/chatbot.ts +++ b/typescript/examples/langchain-cdp-chatbot/chatbot.ts @@ -10,6 +10,7 @@ import { pythActionProvider, openseaActionProvider, alloraActionProvider, + pondActionProvider, } from "@coinbase/agentkit"; import { getLangChainTools } from "@coinbase/agentkit-langchain"; import { HumanMessage } from "@langchain/core/messages"; @@ -122,6 +123,7 @@ async function initializeAgent() { ] : []), alloraActionProvider(), + pondActionProvider(), ], });