Skip to content

Commit 27bd9c3

Browse files
committed
add external router path and new tweet generation
1 parent 2790821 commit 27bd9c3

File tree

4 files changed

+197
-14
lines changed

4 files changed

+197
-14
lines changed

packages/client-coinbase/src/index.ts

+101-12
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import {
55
Memory,
66
Content,
77
HandlerCallback,
8-
stringToUuid
8+
stringToUuid,
9+
composeContext,
10+
generateText,
11+
ModelClass
912
} from "@elizaos/core";
1013
import { postTweet } from "@elizaos/plugin-twitter";
1114
import express from "express";
@@ -17,7 +20,6 @@ export class CoinbaseClient implements Client {
1720
private port: number;
1821

1922
constructor(runtime: IAgentRuntime) {
20-
2123
this.runtime = runtime;
2224
this.server = express();
2325
this.port = Number(runtime.getSetting("COINBASE_WEBHOOK_PORT")) || 3001;
@@ -37,6 +39,17 @@ export class CoinbaseClient implements Client {
3739
private setupWebhookEndpoint() {
3840
this.server.use(express.json());
3941

42+
// Add CORS middleware to allow external requests
43+
this.server.use((req, res, next) => {
44+
res.header('Access-Control-Allow-Origin', '*');
45+
res.header('Access-Control-Allow-Methods', 'POST');
46+
res.header('Access-Control-Allow-Headers', 'Content-Type');
47+
if (req.method === 'OPTIONS') {
48+
return res.sendStatus(200);
49+
}
50+
next();
51+
});
52+
4053
// Add webhook validation middleware
4154
const validateWebhook = (req: express.Request, res: express.Response, next: express.NextFunction) => {
4255
const event = req.body as WebhookEvent;
@@ -51,20 +64,26 @@ export class CoinbaseClient implements Client {
5164
next();
5265
};
5366

67+
// Add health check endpoint
68+
this.server.get('/health', (req, res) => {
69+
res.status(200).json({ status: 'ok' });
70+
});
71+
72+
// Main webhook endpoint
5473
this.server.post("/webhook", validateWebhook, async (req, res) => {
5574
try {
5675
const event = req.body as WebhookEvent;
5776
await this.handleWebhookEvent(event);
58-
res.status(200).send("OK");
77+
res.status(200).json({ status: "success" });
5978
} catch (error) {
6079
elizaLogger.error("Error processing webhook:", error);
61-
res.status(500).send("Internal Server Error");
80+
res.status(500).json({ error: "Internal Server Error" });
6281
}
6382
});
6483

6584
return new Promise<void>((resolve, reject) => {
6685
try {
67-
this.server.listen(this.port, () => {
86+
this.server.listen(this.port, '0.0.0.0', () => {
6887
elizaLogger.info(`Webhook server listening on port ${this.port}`);
6988
resolve();
7089
});
@@ -74,12 +93,86 @@ export class CoinbaseClient implements Client {
7493
});
7594
}
7695

96+
private async generateTweetContent(event: WebhookEvent, _tradeAmount: number, formattedTimestamp: string): Promise<string> {
97+
try {
98+
const roomId = stringToUuid("coinbase-trading");
99+
const amount = Number(this.runtime.getSetting('COINBASE_TRADING_AMOUNT')) ?? 1;
100+
101+
const tradeTweetTemplate = `
102+
# Task
103+
Create an engaging and unique tweet announcing a Coinbase trade. Be creative but professional.
104+
105+
Trade details:
106+
- ${event.event.toUpperCase()} order for ${event.ticker}
107+
- Trading amount: $${amount.toFixed(2)}
108+
- Current price: $${Number(event.price).toFixed(2)}
109+
- Time: ${formattedTimestamp}
110+
111+
Requirements:
112+
1. Must be under 180 characters
113+
2. Use 1-2 relevant emojis
114+
3. No hashtags
115+
4. Vary the wording each time to keep it fresh and engaging
116+
5. Can mention market conditions, timing, or strategy when relevant
117+
6. Keep it professional but conversational
118+
7. Include the key information: action, amount, ticker, and price
119+
120+
Example variations for buys:
121+
"📈 Just added $1,000 of BTC to the portfolio at $50,000.00"
122+
"🎯 Strategic BTC purchase: $1,000 at $50,000.00"
123+
124+
Example variations for sells:
125+
"💫 Executed BTC position: Sold $1,000 at $52,000.00"
126+
"📊 Strategic exit: Released $1,000 of BTC at $52,000.00"
127+
128+
Generate only the tweet text, no commentary or markdown.`;
129+
130+
const context = composeContext({
131+
template: tradeTweetTemplate,
132+
state: {
133+
event: event.event.toUpperCase(),
134+
ticker: event.ticker,
135+
amount: `${amount.toFixed(2)}`,
136+
price: `${Number(event.price).toFixed(2)}`,
137+
timestamp: formattedTimestamp,
138+
bio: '',
139+
lore: '',
140+
messageDirections: '',
141+
postDirections: '',
142+
persona: '',
143+
personality: '',
144+
role: '',
145+
scenario: '',
146+
roomId,
147+
actors: '',
148+
recentMessages: '',
149+
recentMessagesData: []
150+
}
151+
});
152+
153+
const tweetContent = await generateText({
154+
runtime: this.runtime,
155+
context,
156+
modelClass: ModelClass.SMALL,
157+
});
158+
159+
const trimmedContent = tweetContent.trim();
160+
return trimmedContent.length > 180 ? trimmedContent.substring(0, 177) + "..." : trimmedContent;
161+
162+
} catch (error) {
163+
elizaLogger.error("Error generating tweet content:", error);
164+
const amount = Number(this.runtime.getSetting('COINBASE_TRADING_AMOUNT')) ?? 1;
165+
const fallbackTweet = `🚀 ${event.event.toUpperCase()}: $${amount.toFixed(2)} of ${event.ticker} at $${Number(event.price).toFixed(2)}`;
166+
return fallbackTweet;
167+
}
168+
}
169+
77170
private async handleWebhookEvent(event: WebhookEvent) {
78171
const roomId = stringToUuid("coinbase-trading");
79172
await this.runtime.ensureRoomExists(roomId);
80173
await this.runtime.ensureParticipantInRoom(this.runtime.agentId, roomId);
81174

82-
const amount = this.runtime.getSetting('COINBASE_TRADING_AMOUNT') ?? 1;
175+
const amount = Number(this.runtime.getSetting('COINBASE_TRADING_AMOUNT')) ?? 1;
83176
const memory: Memory = {
84177
id: stringToUuid(`coinbase-${event.timestamp}`),
85178
userId: this.runtime.agentId,
@@ -118,13 +211,9 @@ export class CoinbaseClient implements Client {
118211
timeZoneName: 'short'
119212
}).format(new Date(event.timestamp));
120213

121-
const tweetContent = `🚀 ${event.event.toUpperCase()} for ${event.ticker}!
122-
Amount: $${amount}.
123-
Price: $${event.price}.
124-
Time: ${formattedTimestamp} 🌀`;
125-
126214
try {
127-
elizaLogger.info("Tweet content:", tweetContent);
215+
const tweetContent = await this.generateTweetContent(event, amount, formattedTimestamp);
216+
elizaLogger.info("Generated tweet content:", tweetContent);
128217
const response = await postTweet(tweetContent);
129218
elizaLogger.info("Tweet response:", response);
130219
} catch (error) {

packages/client-direct/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"cors": "2.8.5",
3030
"discord.js": "14.16.3",
3131
"express": "4.21.1",
32-
"multer": "1.4.5-lts.1"
32+
"multer": "1.4.5-lts.1",
33+
"@elizaos/client-coinbase": "workspace:*"
3334
},
3435
"devDependencies": {
3536
"tsup": "8.3.5",

packages/client-direct/src/api.ts

+87
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { REST, Routes } from "discord.js";
1313
import { DirectClient } from ".";
1414
import { stringToUuid } from "@elizaos/core";
15+
import { WebhookEvent } from "@elizaos/client-coinbase";
1516

1617
export function createApiRouter(
1718
agents: Map<string, AgentRuntime>,
@@ -28,6 +29,49 @@ export function createApiRouter(
2829
})
2930
);
3031

32+
router.get("/webhook/coinbase/health", (req, res) => {
33+
elizaLogger.info("Health check received");
34+
res.status(200).json({ status: "ok" });
35+
});
36+
37+
router.post("/webhook/coinbase/:agentId", async (req, res) => {
38+
elizaLogger.info("Webhook received for agent:", req.params.agentId);
39+
const agentId = req.params.agentId;
40+
const runtime = agents.get(agentId);
41+
42+
if (!runtime) {
43+
res.status(404).json({ error: "Agent not found" });
44+
return;
45+
}
46+
47+
// Validate the webhook payload
48+
const event = req.body as WebhookEvent;
49+
if (!event.event || !event.ticker || !event.timestamp || !event.price) {
50+
res.status(400).json({ error: "Invalid webhook payload" });
51+
return;
52+
}
53+
if (event.event !== 'buy' && event.event !== 'sell') {
54+
res.status(400).json({ error: "Invalid event type" });
55+
return;
56+
}
57+
58+
try {
59+
// Access the coinbase client through the runtime
60+
const coinbaseClient = runtime.clients.coinbase as any;
61+
if (!coinbaseClient) {
62+
res.status(400).json({ error: "Coinbase client not initialized for this agent" });
63+
return;
64+
}
65+
66+
// Forward the webhook event to the client's handleWebhookEvent method
67+
await coinbaseClient.handleWebhookEvent(event);
68+
res.status(200).json({ status: "success" });
69+
} catch (error) {
70+
elizaLogger.error("Error processing Coinbase webhook:", error);
71+
res.status(500).json({ error: "Internal Server Error" });
72+
}
73+
});
74+
3175
router.get("/", (req, res) => {
3276
res.send("Welcome, this is the REST API!");
3377
});
@@ -183,5 +227,48 @@ export function createApiRouter(
183227
}
184228
});
185229

230+
// Add Coinbase webhook forwarding endpoint
231+
router.post("/webhook/coinbase/:agentId", async (req, res) => {
232+
const agentId = req.params.agentId;
233+
const runtime = agents.get(agentId);
234+
235+
if (!runtime) {
236+
res.status(404).json({ error: "Agent not found" });
237+
return;
238+
}
239+
240+
// Validate the webhook payload
241+
const event = req.body as WebhookEvent;
242+
if (!event.event || !event.ticker || !event.timestamp || !event.price) {
243+
res.status(400).json({ error: "Invalid webhook payload" });
244+
return;
245+
}
246+
if (event.event !== 'buy' && event.event !== 'sell') {
247+
res.status(400).json({ error: "Invalid event type" });
248+
return;
249+
}
250+
251+
try {
252+
// Access the coinbase client through the runtime
253+
const coinbaseClient = runtime.clients.coinbase as any;
254+
if (!coinbaseClient) {
255+
res.status(400).json({ error: "Coinbase client not initialized for this agent" });
256+
return;
257+
}
258+
259+
// Forward the webhook event to the client's handleWebhookEvent method
260+
await coinbaseClient.handleWebhookEvent(event);
261+
res.status(200).json({ status: "success" });
262+
} catch (error) {
263+
elizaLogger.error("Error processing Coinbase webhook:", error);
264+
res.status(500).json({ error: "Internal Server Error" });
265+
}
266+
});
267+
268+
// Add health check endpoint for Coinbase webhook
269+
router.get("/webhook/coinbase/health", (req, res) => {
270+
res.status(200).json({ status: "ok" });
271+
});
272+
186273
return router;
187274
}

pnpm-lock.yaml

+7-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)