From 27bd9c3e0c251ac25ecafaa46ec21ae6f922ba95 Mon Sep 17 00:00:00 2001 From: 369d <0x369d@gmail.com> Date: Thu, 9 Jan 2025 21:08:43 +0000 Subject: [PATCH] add external router path and new tweet generation --- packages/client-coinbase/src/index.ts | 113 +++++++++++++++++++++++--- packages/client-direct/package.json | 3 +- packages/client-direct/src/api.ts | 87 ++++++++++++++++++++ pnpm-lock.yaml | 8 +- 4 files changed, 197 insertions(+), 14 deletions(-) diff --git a/packages/client-coinbase/src/index.ts b/packages/client-coinbase/src/index.ts index 4cea4cbea2a..b37fe4c8f3a 100644 --- a/packages/client-coinbase/src/index.ts +++ b/packages/client-coinbase/src/index.ts @@ -5,7 +5,10 @@ import { Memory, Content, HandlerCallback, - stringToUuid + stringToUuid, + composeContext, + generateText, + ModelClass } from "@elizaos/core"; import { postTweet } from "@elizaos/plugin-twitter"; import express from "express"; @@ -17,7 +20,6 @@ export class CoinbaseClient implements Client { private port: number; constructor(runtime: IAgentRuntime) { - this.runtime = runtime; this.server = express(); this.port = Number(runtime.getSetting("COINBASE_WEBHOOK_PORT")) || 3001; @@ -37,6 +39,17 @@ export class CoinbaseClient implements Client { private setupWebhookEndpoint() { this.server.use(express.json()); + // Add CORS middleware to allow external requests + this.server.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'POST'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + if (req.method === 'OPTIONS') { + return res.sendStatus(200); + } + next(); + }); + // Add webhook validation middleware const validateWebhook = (req: express.Request, res: express.Response, next: express.NextFunction) => { const event = req.body as WebhookEvent; @@ -51,20 +64,26 @@ export class CoinbaseClient implements Client { next(); }; + // Add health check endpoint + this.server.get('/health', (req, res) => { + res.status(200).json({ status: 'ok' }); + }); + + // Main webhook endpoint this.server.post("/webhook", validateWebhook, async (req, res) => { try { const event = req.body as WebhookEvent; await this.handleWebhookEvent(event); - res.status(200).send("OK"); + res.status(200).json({ status: "success" }); } catch (error) { elizaLogger.error("Error processing webhook:", error); - res.status(500).send("Internal Server Error"); + res.status(500).json({ error: "Internal Server Error" }); } }); return new Promise((resolve, reject) => { try { - this.server.listen(this.port, () => { + this.server.listen(this.port, '0.0.0.0', () => { elizaLogger.info(`Webhook server listening on port ${this.port}`); resolve(); }); @@ -74,12 +93,86 @@ export class CoinbaseClient implements Client { }); } + private async generateTweetContent(event: WebhookEvent, _tradeAmount: number, formattedTimestamp: string): Promise { + try { + const roomId = stringToUuid("coinbase-trading"); + const amount = Number(this.runtime.getSetting('COINBASE_TRADING_AMOUNT')) ?? 1; + + const tradeTweetTemplate = ` +# Task +Create an engaging and unique tweet announcing a Coinbase trade. Be creative but professional. + +Trade details: +- ${event.event.toUpperCase()} order for ${event.ticker} +- Trading amount: $${amount.toFixed(2)} +- Current price: $${Number(event.price).toFixed(2)} +- Time: ${formattedTimestamp} + +Requirements: +1. Must be under 180 characters +2. Use 1-2 relevant emojis +3. No hashtags +4. Vary the wording each time to keep it fresh and engaging +5. Can mention market conditions, timing, or strategy when relevant +6. Keep it professional but conversational +7. Include the key information: action, amount, ticker, and price + +Example variations for buys: +"📈 Just added $1,000 of BTC to the portfolio at $50,000.00" +"🎯 Strategic BTC purchase: $1,000 at $50,000.00" + +Example variations for sells: +"💫 Executed BTC position: Sold $1,000 at $52,000.00" +"📊 Strategic exit: Released $1,000 of BTC at $52,000.00" + +Generate only the tweet text, no commentary or markdown.`; + + const context = composeContext({ + template: tradeTweetTemplate, + state: { + event: event.event.toUpperCase(), + ticker: event.ticker, + amount: `${amount.toFixed(2)}`, + price: `${Number(event.price).toFixed(2)}`, + timestamp: formattedTimestamp, + bio: '', + lore: '', + messageDirections: '', + postDirections: '', + persona: '', + personality: '', + role: '', + scenario: '', + roomId, + actors: '', + recentMessages: '', + recentMessagesData: [] + } + }); + + const tweetContent = await generateText({ + runtime: this.runtime, + context, + modelClass: ModelClass.SMALL, + }); + + const trimmedContent = tweetContent.trim(); + return trimmedContent.length > 180 ? trimmedContent.substring(0, 177) + "..." : trimmedContent; + + } catch (error) { + elizaLogger.error("Error generating tweet content:", error); + const amount = Number(this.runtime.getSetting('COINBASE_TRADING_AMOUNT')) ?? 1; + const fallbackTweet = `🚀 ${event.event.toUpperCase()}: $${amount.toFixed(2)} of ${event.ticker} at $${Number(event.price).toFixed(2)}`; + return fallbackTweet; + } + } + private async handleWebhookEvent(event: WebhookEvent) { const roomId = stringToUuid("coinbase-trading"); await this.runtime.ensureRoomExists(roomId); await this.runtime.ensureParticipantInRoom(this.runtime.agentId, roomId); - const amount = this.runtime.getSetting('COINBASE_TRADING_AMOUNT') ?? 1; + const amount = Number(this.runtime.getSetting('COINBASE_TRADING_AMOUNT')) ?? 1; const memory: Memory = { id: stringToUuid(`coinbase-${event.timestamp}`), userId: this.runtime.agentId, @@ -118,13 +211,9 @@ export class CoinbaseClient implements Client { timeZoneName: 'short' }).format(new Date(event.timestamp)); - const tweetContent = `🚀 ${event.event.toUpperCase()} for ${event.ticker}! -Amount: $${amount}. -Price: $${event.price}. -Time: ${formattedTimestamp} 🌀`; - try { - elizaLogger.info("Tweet content:", tweetContent); + const tweetContent = await this.generateTweetContent(event, amount, formattedTimestamp); + elizaLogger.info("Generated tweet content:", tweetContent); const response = await postTweet(tweetContent); elizaLogger.info("Tweet response:", response); } catch (error) { diff --git a/packages/client-direct/package.json b/packages/client-direct/package.json index 84e4ebbc201..6d26066a741 100644 --- a/packages/client-direct/package.json +++ b/packages/client-direct/package.json @@ -29,7 +29,8 @@ "cors": "2.8.5", "discord.js": "14.16.3", "express": "4.21.1", - "multer": "1.4.5-lts.1" + "multer": "1.4.5-lts.1", + "@elizaos/client-coinbase": "workspace:*" }, "devDependencies": { "tsup": "8.3.5", diff --git a/packages/client-direct/src/api.ts b/packages/client-direct/src/api.ts index 6d5ac569f5e..3021d9faccc 100644 --- a/packages/client-direct/src/api.ts +++ b/packages/client-direct/src/api.ts @@ -12,6 +12,7 @@ import { import { REST, Routes } from "discord.js"; import { DirectClient } from "."; import { stringToUuid } from "@elizaos/core"; +import { WebhookEvent } from "@elizaos/client-coinbase"; export function createApiRouter( agents: Map, @@ -28,6 +29,49 @@ export function createApiRouter( }) ); + router.get("/webhook/coinbase/health", (req, res) => { + elizaLogger.info("Health check received"); + res.status(200).json({ status: "ok" }); + }); + + router.post("/webhook/coinbase/:agentId", async (req, res) => { + elizaLogger.info("Webhook received for agent:", req.params.agentId); + const agentId = req.params.agentId; + const runtime = agents.get(agentId); + + if (!runtime) { + res.status(404).json({ error: "Agent not found" }); + return; + } + + // Validate the webhook payload + const event = req.body as WebhookEvent; + if (!event.event || !event.ticker || !event.timestamp || !event.price) { + res.status(400).json({ error: "Invalid webhook payload" }); + return; + } + if (event.event !== 'buy' && event.event !== 'sell') { + res.status(400).json({ error: "Invalid event type" }); + return; + } + + try { + // Access the coinbase client through the runtime + const coinbaseClient = runtime.clients.coinbase as any; + if (!coinbaseClient) { + res.status(400).json({ error: "Coinbase client not initialized for this agent" }); + return; + } + + // Forward the webhook event to the client's handleWebhookEvent method + await coinbaseClient.handleWebhookEvent(event); + res.status(200).json({ status: "success" }); + } catch (error) { + elizaLogger.error("Error processing Coinbase webhook:", error); + res.status(500).json({ error: "Internal Server Error" }); + } + }); + router.get("/", (req, res) => { res.send("Welcome, this is the REST API!"); }); @@ -183,5 +227,48 @@ export function createApiRouter( } }); + // Add Coinbase webhook forwarding endpoint + router.post("/webhook/coinbase/:agentId", async (req, res) => { + const agentId = req.params.agentId; + const runtime = agents.get(agentId); + + if (!runtime) { + res.status(404).json({ error: "Agent not found" }); + return; + } + + // Validate the webhook payload + const event = req.body as WebhookEvent; + if (!event.event || !event.ticker || !event.timestamp || !event.price) { + res.status(400).json({ error: "Invalid webhook payload" }); + return; + } + if (event.event !== 'buy' && event.event !== 'sell') { + res.status(400).json({ error: "Invalid event type" }); + return; + } + + try { + // Access the coinbase client through the runtime + const coinbaseClient = runtime.clients.coinbase as any; + if (!coinbaseClient) { + res.status(400).json({ error: "Coinbase client not initialized for this agent" }); + return; + } + + // Forward the webhook event to the client's handleWebhookEvent method + await coinbaseClient.handleWebhookEvent(event); + res.status(200).json({ status: "success" }); + } catch (error) { + elizaLogger.error("Error processing Coinbase webhook:", error); + res.status(500).json({ error: "Internal Server Error" }); + } + }); + + // Add health check endpoint for Coinbase webhook + router.get("/webhook/coinbase/health", (req, res) => { + res.status(200).json({ status: "ok" }); + }); + return router; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99be458c327..9e23509d502 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -604,12 +604,18 @@ importers: packages/client-direct: dependencies: + '@elizaos/client-coinbase': + specifier: workspace:* + version: link:../client-coinbase '@elizaos/core': specifier: workspace:* version: link:../core '@elizaos/plugin-image-generation': specifier: workspace:* version: link:../plugin-image-generation + '@elizaos/plugin-twitter': + specifier: workspace:* + version: link:../plugin-twitter '@types/body-parser': specifier: 1.19.5 version: 1.19.5 @@ -33164,7 +33170,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.3.4 + debug: 4.4.0(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: