@elizaos/plugin-social-alpha 2.0.3-beta.6 → 2.0.3-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/clients.d.ts +354 -0
  2. package/dist/clients.d.ts.map +1 -0
  3. package/dist/clients.js +670 -0
  4. package/dist/clients.js.map +1 -0
  5. package/dist/config.d.ts +144 -0
  6. package/dist/config.d.ts.map +1 -0
  7. package/dist/config.js +122 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/events.d.ts +5 -0
  10. package/dist/events.d.ts.map +1 -0
  11. package/dist/events.js +426 -0
  12. package/dist/events.js.map +1 -0
  13. package/dist/frontend/LeaderboardView.helpers.d.ts +6 -0
  14. package/dist/frontend/LeaderboardView.helpers.d.ts.map +1 -0
  15. package/dist/frontend/LeaderboardView.helpers.js +59 -0
  16. package/dist/frontend/LeaderboardView.helpers.js.map +1 -0
  17. package/dist/frontend/SocialAlphaSpatialView.d.ts +52 -0
  18. package/dist/frontend/SocialAlphaSpatialView.d.ts.map +1 -0
  19. package/dist/frontend/SocialAlphaSpatialView.js +72 -0
  20. package/dist/frontend/SocialAlphaSpatialView.js.map +1 -0
  21. package/dist/frontend/SocialAlphaView.d.ts +35 -0
  22. package/dist/frontend/SocialAlphaView.d.ts.map +1 -0
  23. package/dist/frontend/SocialAlphaView.js +125 -0
  24. package/dist/frontend/SocialAlphaView.js.map +1 -0
  25. package/dist/index.d.ts +24 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +73 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mockPriceService.d.ts +22 -0
  30. package/dist/mockPriceService.d.ts.map +1 -0
  31. package/dist/mockPriceService.js +21 -0
  32. package/dist/mockPriceService.js.map +1 -0
  33. package/dist/providers/socialAlphaProvider.d.ts +15 -0
  34. package/dist/providers/socialAlphaProvider.d.ts.map +1 -0
  35. package/dist/providers/socialAlphaProvider.js +261 -0
  36. package/dist/providers/socialAlphaProvider.js.map +1 -0
  37. package/dist/register-terminal-view.d.ts +15 -0
  38. package/dist/register-terminal-view.d.ts.map +1 -0
  39. package/dist/register-terminal-view.js +21 -0
  40. package/dist/register-terminal-view.js.map +1 -0
  41. package/dist/register.d.ts +10 -0
  42. package/dist/register.d.ts.map +1 -0
  43. package/dist/register.js +5 -0
  44. package/dist/register.js.map +1 -0
  45. package/dist/reports.d.ts +57 -0
  46. package/dist/reports.d.ts.map +1 -0
  47. package/dist/reports.js +455 -0
  48. package/dist/reports.js.map +1 -0
  49. package/dist/routes.d.ts +3 -0
  50. package/dist/routes.d.ts.map +1 -0
  51. package/dist/routes.js +59 -0
  52. package/dist/routes.js.map +1 -0
  53. package/dist/schemas.d.ts +151 -0
  54. package/dist/schemas.d.ts.map +1 -0
  55. package/dist/schemas.js +258 -0
  56. package/dist/schemas.js.map +1 -0
  57. package/dist/service.d.ts +306 -0
  58. package/dist/service.d.ts.map +1 -0
  59. package/dist/service.js +3078 -0
  60. package/dist/service.js.map +1 -0
  61. package/dist/services/balancedTrustScoreCalculator.d.ts +61 -0
  62. package/dist/services/balancedTrustScoreCalculator.d.ts.map +1 -0
  63. package/dist/services/balancedTrustScoreCalculator.js +207 -0
  64. package/dist/services/balancedTrustScoreCalculator.js.map +1 -0
  65. package/dist/services/historicalPriceService.d.ts +59 -0
  66. package/dist/services/historicalPriceService.d.ts.map +1 -0
  67. package/dist/services/historicalPriceService.js +291 -0
  68. package/dist/services/historicalPriceService.js.map +1 -0
  69. package/dist/services/index.d.ts +12 -0
  70. package/dist/services/index.d.ts.map +1 -0
  71. package/dist/services/index.js +17 -0
  72. package/dist/services/index.js.map +1 -0
  73. package/dist/services/priceEnrichmentService.d.ts +109 -0
  74. package/dist/services/priceEnrichmentService.d.ts.map +1 -0
  75. package/dist/services/priceEnrichmentService.js +780 -0
  76. package/dist/services/priceEnrichmentService.js.map +1 -0
  77. package/dist/services/simulationActorsV2.d.ts +54 -0
  78. package/dist/services/simulationActorsV2.d.ts.map +1 -0
  79. package/dist/services/simulationActorsV2.js +362 -0
  80. package/dist/services/simulationActorsV2.js.map +1 -0
  81. package/dist/services/simulationRunner.d.ts +113 -0
  82. package/dist/services/simulationRunner.d.ts.map +1 -0
  83. package/dist/services/simulationRunner.js +771 -0
  84. package/dist/services/simulationRunner.js.map +1 -0
  85. package/dist/services/tokenSimulationService.d.ts +34 -0
  86. package/dist/services/tokenSimulationService.d.ts.map +1 -0
  87. package/dist/services/tokenSimulationService.js +297 -0
  88. package/dist/services/tokenSimulationService.js.map +1 -0
  89. package/dist/services/trustScoreOptimizer.d.ts +110 -0
  90. package/dist/services/trustScoreOptimizer.d.ts.map +1 -0
  91. package/dist/services/trustScoreOptimizer.js +635 -0
  92. package/dist/services/trustScoreOptimizer.js.map +1 -0
  93. package/dist/simulationActors.d.ts +35 -0
  94. package/dist/simulationActors.d.ts.map +1 -0
  95. package/dist/simulationActors.js +160 -0
  96. package/dist/simulationActors.js.map +1 -0
  97. package/dist/social-alpha-view-bundle.d.ts +2 -0
  98. package/dist/social-alpha-view-bundle.d.ts.map +1 -0
  99. package/dist/social-alpha-view-bundle.js +5 -0
  100. package/dist/social-alpha-view-bundle.js.map +1 -0
  101. package/dist/types.d.ts +937 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/types.js +46 -0
  104. package/dist/types.js.map +1 -0
  105. package/dist/views/brand/background/clouds_background.jpg +0 -0
  106. package/dist/views/brand/banners/eliza_banner.svg +20 -0
  107. package/dist/views/brand/banners/elizacloud_banner.svg +20 -0
  108. package/dist/views/brand/banners/elizaos_banner.svg +20 -0
  109. package/dist/views/brand/concepts/billboard_concept_1200.jpg +0 -0
  110. package/dist/views/brand/concepts/chibi_usb_concept_900.jpg +0 -0
  111. package/dist/views/brand/concepts/concept_minipc_900.jpg +0 -0
  112. package/dist/views/brand/concepts/concept_phone_800.jpg +0 -0
  113. package/dist/views/brand/concepts/concept_usbdrive_900.jpg +0 -0
  114. package/dist/views/brand/favicons/android-chrome-192x192.png +0 -0
  115. package/dist/views/brand/favicons/android-chrome-512x512.png +0 -0
  116. package/dist/views/brand/favicons/apple-touch-icon.png +0 -0
  117. package/dist/views/brand/favicons/favicon-16x16.png +0 -0
  118. package/dist/views/brand/favicons/favicon-32x32.png +0 -0
  119. package/dist/views/brand/favicons/favicon.ico +0 -0
  120. package/dist/views/brand/favicons/favicon.svg +17 -0
  121. package/dist/views/brand/logos/elizaOS_text_black.svg +3 -0
  122. package/dist/views/brand/logos/elizaOS_text_white.svg +3 -0
  123. package/dist/views/brand/logos/eliza_logotext.svg +26 -0
  124. package/dist/views/brand/logos/eliza_logotext_black.svg +26 -0
  125. package/dist/views/brand/logos/eliza_text_black.svg +3 -0
  126. package/dist/views/brand/logos/eliza_text_white.svg +3 -0
  127. package/dist/views/brand/logos/elizacloud_logotext.svg +26 -0
  128. package/dist/views/brand/logos/elizacloud_logotext_black.svg +26 -0
  129. package/dist/views/brand/logos/elizacloud_text_black.svg +3 -0
  130. package/dist/views/brand/logos/elizacloud_text_white.svg +3 -0
  131. package/dist/views/brand/logos/elizaos_logotext.svg +26 -0
  132. package/dist/views/brand/logos/elizaos_logotext_black.svg +26 -0
  133. package/dist/views/brand/logos/logo_blue_blackbg.svg +18 -0
  134. package/dist/views/brand/logos/logo_blue_nobg.svg +17 -0
  135. package/dist/views/brand/logos/logo_orange_blackbg.svg +18 -0
  136. package/dist/views/brand/logos/logo_orange_nobg.svg +17 -0
  137. package/dist/views/brand/logos/logo_white_blackbg.svg +25 -0
  138. package/dist/views/brand/logos/logo_white_bluebg.svg +25 -0
  139. package/dist/views/brand/logos/logo_white_graybg.svg +18 -0
  140. package/dist/views/brand/logos/logo_white_nobg.svg +24 -0
  141. package/dist/views/brand/logos/logo_white_orangebg.svg +25 -0
  142. package/dist/views/brand/ogembeds/eliza_ogembed.png +0 -0
  143. package/dist/views/brand/ogembeds/eliza_ogembed.svg +20 -0
  144. package/dist/views/brand/ogembeds/elizacloud_ogembed.png +0 -0
  145. package/dist/views/brand/ogembeds/elizacloud_ogembed.svg +20 -0
  146. package/dist/views/brand/ogembeds/elizaos_ogembed.png +0 -0
  147. package/dist/views/brand/ogembeds/elizaos_ogembed.svg +20 -0
  148. package/dist/views/bundle.js +268 -0
  149. package/dist/views/bundle.js.map +1 -0
  150. package/dist/views/site.webmanifest +19 -0
  151. package/package.json +5 -5
@@ -0,0 +1,3078 @@
1
+ import {
2
+ asUUID,
3
+ ChannelType,
4
+ logger as coreLogger,
5
+ createUniqueUuid,
6
+ ModelType,
7
+ Service
8
+ } from "@elizaos/core";
9
+ function toJsonRecord(value) {
10
+ return JSON.parse(JSON.stringify(value ?? {}));
11
+ }
12
+ function logValue(value) {
13
+ if (typeof value === "string") return value;
14
+ if (value instanceof Error) return value.stack ?? value.message;
15
+ try {
16
+ return JSON.stringify(value);
17
+ } catch {
18
+ return String(value);
19
+ }
20
+ }
21
+ const logger = {
22
+ debug: (...args) => coreLogger.debug(args.map(logValue).join(" ")),
23
+ info: (...args) => coreLogger.info(args.map(logValue).join(" ")),
24
+ warn: (...args) => coreLogger.warn(args.map(logValue).join(" ")),
25
+ error: (...args) => coreLogger.error(args.map(logValue).join(" "))
26
+ };
27
+ import { v4 as uuidv4 } from "uuid";
28
+ import { BirdeyeClient, DexscreenerClient, HeliusClient } from "./clients.js";
29
+ import {
30
+ DEFAULT_TRADING_CONFIG,
31
+ getConvictionMultiplier,
32
+ getLiquidityMultiplier,
33
+ getMarketCapMultiplier,
34
+ getVolumeMultiplier,
35
+ TRUST_LEADERBOARD_WORLD_SEED
36
+ } from "./config.js";
37
+ import { formatFullReport } from "./reports.js";
38
+ import { BalancedTrustScoreCalculator } from "./services/balancedTrustScoreCalculator.js";
39
+ import {
40
+ Conviction,
41
+ RecommendationType,
42
+ ServiceType,
43
+ SupportedChain,
44
+ TRUST_MARKETPLACE_COMPONENT_TYPE,
45
+ TransactionType
46
+ } from "./types.js";
47
+ function parseRecommendationExtraction(response) {
48
+ const parsed = parseJsonObject(
49
+ response
50
+ );
51
+ if (!parsed?.recommendations || !Array.isArray(parsed.recommendations)) {
52
+ return null;
53
+ }
54
+ return {
55
+ recommendations: parsed.recommendations.map((rec) => ({
56
+ ...rec,
57
+ messageIndex: typeof rec.messageIndex === "number" ? rec.messageIndex : Number.parseInt(String(rec.messageIndex ?? "0"), 10),
58
+ isCall: rec.isCall === true || String(rec.isCall).toLowerCase() === "true"
59
+ }))
60
+ };
61
+ }
62
+ function parseJsonObject(value) {
63
+ try {
64
+ const parsed = JSON.parse(value.trim());
65
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ function asContentObject(value, requiredStringKeys) {
71
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
72
+ return null;
73
+ }
74
+ const record = value;
75
+ for (const key of requiredStringKeys) {
76
+ if (typeof record[key] !== "string") {
77
+ return null;
78
+ }
79
+ }
80
+ return value;
81
+ }
82
+ function asTransaction(value) {
83
+ return asContentObject(value, ["positionId", "tokenAddress"]);
84
+ }
85
+ class CommunityInvestorService extends Service {
86
+ static serviceType = ServiceType.COMMUNITY_INVESTOR;
87
+ capabilityDescription = "Manages community-driven investment trust scores and recommendations.";
88
+ // Client instances
89
+ birdeyeClient;
90
+ dexscreenerClient;
91
+ heliusClient = null;
92
+ // Configuration
93
+ tradingConfig;
94
+ apiKeys = {};
95
+ balancedTrustCalculator;
96
+ // Constants can be defined here or loaded from config/settings
97
+ POSITIVE_TRADE_THRESHOLD = 10;
98
+ // Trust score above this might trigger a trade
99
+ NEUTRAL_MARGIN = 5;
100
+ // Trust scores within +/- this from 0 are neutral
101
+ RECENCY_WEIGHT_MONTHS = 6;
102
+ USER_TRADE_COOLDOWN_HOURS = 12;
103
+ METRIC_REFRESH_INTERVAL = 24 * 60 * 60 * 1e3;
104
+ // 1 day to re-evaluate metrics
105
+ // Add this property to the class
106
+ userRegistry = /* @__PURE__ */ new Set();
107
+ componentWorldId;
108
+ componentRoomId;
109
+ // This will be the same as componentWorldId
110
+ constructor(runtime) {
111
+ if (!runtime) {
112
+ throw new Error("CommunityInvestorService requires an agent runtime");
113
+ }
114
+ super(runtime);
115
+ this.runtime = runtime;
116
+ this.balancedTrustCalculator = new BalancedTrustScoreCalculator();
117
+ this.componentWorldId = createUniqueUuid(
118
+ runtime,
119
+ TRUST_LEADERBOARD_WORLD_SEED
120
+ );
121
+ this.componentRoomId = this.componentWorldId;
122
+ logger.info(
123
+ `[CommunityInvestorService] Using Component World/Room ID: ${this.componentWorldId}`
124
+ );
125
+ this.ensurePluginComponentContext();
126
+ this.birdeyeClient = BirdeyeClient.createFromRuntime(runtime);
127
+ this.dexscreenerClient = DexscreenerClient.createFromRuntime(runtime);
128
+ try {
129
+ this.heliusClient = HeliusClient.createFromRuntime(runtime);
130
+ } catch (error) {
131
+ logger.warn(
132
+ "Failed to initialize Helius client, holder data will be limited:",
133
+ error
134
+ );
135
+ }
136
+ this.tradingConfig = DEFAULT_TRADING_CONFIG;
137
+ this.initialize(runtime);
138
+ this.registerTaskWorkers(runtime);
139
+ }
140
+ static async start(runtime) {
141
+ const service = new CommunityInvestorService(runtime);
142
+ return service;
143
+ }
144
+ static async stop(runtime) {
145
+ const service = runtime.getService("trading");
146
+ if (service) {
147
+ await service.stop?.();
148
+ }
149
+ }
150
+ async stop() {
151
+ return Promise.resolve();
152
+ }
153
+ /**
154
+ * Process a buy signal from an entity
155
+ */
156
+ async processBuySignal(buySignal, entity) {
157
+ logger.debug("processing buy signal", buySignal, entity);
158
+ try {
159
+ if (!entity.id) {
160
+ logger.error("Entity ID is required for processing buy signal");
161
+ return null;
162
+ }
163
+ const tokenPerformance = await this.getOrFetchTokenPerformance(
164
+ buySignal.tokenAddress,
165
+ buySignal.chain || this.tradingConfig.defaultChain
166
+ );
167
+ if (!tokenPerformance) {
168
+ logger.error(`Token not found: ${buySignal.tokenAddress}`);
169
+ return null;
170
+ }
171
+ if (!this.validateToken(tokenPerformance)) {
172
+ logger.error(`Token failed validation: ${buySignal.tokenAddress}`);
173
+ return null;
174
+ }
175
+ const recommendation = await this.createTokenRecommendation(
176
+ entity.id,
177
+ tokenPerformance,
178
+ buySignal.conviction || Conviction.MEDIUM,
179
+ RecommendationType.BUY
180
+ );
181
+ if (!recommendation) {
182
+ logger.error(
183
+ `Failed to create recommendation for token: ${buySignal.tokenAddress}`
184
+ );
185
+ return null;
186
+ }
187
+ const buyAmount = this.calculateBuyAmount(
188
+ entity,
189
+ buySignal.conviction || Conviction.MEDIUM,
190
+ tokenPerformance
191
+ );
192
+ const position = await this.createPosition(
193
+ recommendation.id,
194
+ entity.id,
195
+ buySignal.tokenAddress,
196
+ buySignal.walletAddress || "simulation",
197
+ buyAmount,
198
+ tokenPerformance.price?.toString() || "0",
199
+ buySignal.isSimulation || this.tradingConfig.forceSimulation
200
+ );
201
+ if (!position) {
202
+ logger.error(
203
+ `Failed to create position for token: ${buySignal.tokenAddress}`
204
+ );
205
+ return null;
206
+ }
207
+ await this.recordTransaction(
208
+ position.id,
209
+ buySignal.tokenAddress,
210
+ TransactionType.BUY,
211
+ buyAmount,
212
+ tokenPerformance.price || 0,
213
+ position.isSimulation
214
+ );
215
+ return position;
216
+ } catch (error) {
217
+ logger.error("Error processing buy signal:", error);
218
+ return null;
219
+ }
220
+ }
221
+ /**
222
+ * Process a sell signal for an existing position
223
+ */
224
+ async processSellSignal(positionId, _sellRecommenderId) {
225
+ try {
226
+ logger.debug("processing sell signal", positionId, _sellRecommenderId);
227
+ const position = await this.getPosition(positionId);
228
+ if (!position) {
229
+ logger.error(`Position not found: ${positionId}`);
230
+ return false;
231
+ }
232
+ if (position.closedAt) {
233
+ logger.error(`Position already closed: ${positionId}`);
234
+ return false;
235
+ }
236
+ const tokenPerformance = await this.getOrFetchTokenPerformance(
237
+ position.tokenAddress,
238
+ position.chain
239
+ );
240
+ if (!tokenPerformance) {
241
+ logger.error(`Token not found: ${position.tokenAddress}`);
242
+ return false;
243
+ }
244
+ const initialPrice = Number.parseFloat(position.initialPrice);
245
+ const currentPrice = tokenPerformance.price || 0;
246
+ const priceChange = initialPrice > 0 ? (currentPrice - initialPrice) / initialPrice : 0;
247
+ const updatedPosition = {
248
+ ...position,
249
+ currentPrice: currentPrice.toString(),
250
+ closedAt: /* @__PURE__ */ new Date()
251
+ };
252
+ await this.storePosition(updatedPosition);
253
+ await this.recordTransaction(
254
+ position.id,
255
+ position.tokenAddress,
256
+ TransactionType.SELL,
257
+ BigInt(position.amount),
258
+ currentPrice,
259
+ position.isSimulation
260
+ );
261
+ await this.updateRecommenderMetrics(position.entityId, priceChange * 100);
262
+ return true;
263
+ } catch (error) {
264
+ logger.error("Error processing sell signal:", error);
265
+ return false;
266
+ }
267
+ }
268
+ /**
269
+ * Handle a recommendation from a entity
270
+ */
271
+ async handleRecommendation(entity, recommendation) {
272
+ try {
273
+ logger.debug("handling recommendation", entity, recommendation);
274
+ if (!entity.id) {
275
+ logger.error("Entity ID is required for handling recommendation");
276
+ return null;
277
+ }
278
+ const tokenPerformance = await this.getOrFetchTokenPerformance(
279
+ recommendation.tokenAddress,
280
+ recommendation.chain
281
+ );
282
+ if (!tokenPerformance) {
283
+ logger.error(`Token not found: ${recommendation.tokenAddress}`);
284
+ return null;
285
+ }
286
+ const tokenRecommendation = await this.createTokenRecommendation(
287
+ entity.id,
288
+ tokenPerformance,
289
+ recommendation.conviction,
290
+ recommendation.type
291
+ );
292
+ if (!tokenRecommendation) {
293
+ logger.error(
294
+ `Failed to create recommendation for token: ${recommendation.tokenAddress}`
295
+ );
296
+ return null;
297
+ }
298
+ if (recommendation.type === RecommendationType.BUY) {
299
+ const buyAmount = this.calculateBuyAmount(
300
+ entity,
301
+ recommendation.conviction,
302
+ tokenPerformance
303
+ );
304
+ const position = await this.createPosition(
305
+ tokenRecommendation.id,
306
+ entity.id,
307
+ recommendation.tokenAddress,
308
+ "simulation",
309
+ // Use simulation wallet by default
310
+ buyAmount,
311
+ tokenPerformance.price?.toString() || "0",
312
+ true
313
+ // Simulation by default
314
+ );
315
+ if (!position) {
316
+ logger.error(
317
+ `Failed to create position for token: ${recommendation.tokenAddress}`
318
+ );
319
+ return null;
320
+ }
321
+ await this.recordTransaction(
322
+ position.id,
323
+ recommendation.tokenAddress,
324
+ TransactionType.BUY,
325
+ buyAmount,
326
+ tokenPerformance.price || 0,
327
+ true
328
+ // Simulation by default
329
+ );
330
+ return position;
331
+ }
332
+ return null;
333
+ } catch (error) {
334
+ logger.error("Error handling recommendation:", error);
335
+ return null;
336
+ }
337
+ }
338
+ /**
339
+ * Check if a wallet is registered for a chain
340
+ */
341
+ hasWallet(chain) {
342
+ logger.debug("hasWallet", chain);
343
+ return chain.toLowerCase() === "solana";
344
+ }
345
+ // ===================== TOKEN PROVIDER METHODS =====================
346
+ /**
347
+ * Get token overview data
348
+ */
349
+ async getTokenOverview(chain, tokenAddress, forceRefresh = false) {
350
+ try {
351
+ logger.debug("getting token overview", chain, tokenAddress, forceRefresh);
352
+ if (!forceRefresh) {
353
+ const cacheKey = `token:${chain}:${tokenAddress}:overview`;
354
+ const cachedData = await this.runtime.getCache(cacheKey);
355
+ if (cachedData) {
356
+ return cachedData;
357
+ }
358
+ const tokenPerformance = await this.getTokenPerformance(
359
+ tokenAddress,
360
+ chain
361
+ );
362
+ if (tokenPerformance) {
363
+ const tokenData = {
364
+ chain: tokenPerformance.chain || chain,
365
+ address: tokenPerformance.address || tokenAddress,
366
+ name: tokenPerformance.name || "",
367
+ symbol: tokenPerformance.symbol || "",
368
+ decimals: tokenPerformance.decimals || 0,
369
+ metadata: tokenPerformance.metadata || {},
370
+ price: tokenPerformance.price || 0,
371
+ priceUsd: tokenPerformance.price?.toString() || "0",
372
+ price24hChange: tokenPerformance.price24hChange || 0,
373
+ marketCap: tokenPerformance.currentMarketCap || 0,
374
+ liquidityUsd: tokenPerformance.liquidity || 0,
375
+ volume24h: tokenPerformance.volume || 0,
376
+ volume24hChange: tokenPerformance.volume24hChange || 0,
377
+ trades: tokenPerformance.trades || 0,
378
+ trades24hChange: tokenPerformance.trades24hChange || 0,
379
+ uniqueWallet24h: 0,
380
+ // Would need to be fetched
381
+ uniqueWallet24hChange: 0,
382
+ // Would need to be fetched
383
+ holders: tokenPerformance.holders || 0
384
+ };
385
+ await this.runtime.setCache(
386
+ cacheKey,
387
+ tokenData
388
+ );
389
+ return tokenData;
390
+ }
391
+ }
392
+ if (chain.toLowerCase() === "solana") {
393
+ const [dexScreenerData, birdeyeData] = await Promise.all([
394
+ this.dexscreenerClient.searchForHighestLiquidityPair(
395
+ tokenAddress,
396
+ chain,
397
+ {
398
+ expires: "5m"
399
+ }
400
+ ),
401
+ this.birdeyeClient.fetchTokenOverview(
402
+ tokenAddress,
403
+ { expires: "5m" },
404
+ forceRefresh
405
+ )
406
+ ]);
407
+ const tokenData = {
408
+ chain,
409
+ address: tokenAddress,
410
+ name: birdeyeData?.name || dexScreenerData?.baseToken?.name || "",
411
+ symbol: birdeyeData?.symbol || dexScreenerData?.baseToken?.symbol || "",
412
+ decimals: birdeyeData?.decimals || 9,
413
+ // Default for Solana tokens
414
+ metadata: {
415
+ logoURI: birdeyeData?.logoURI || "",
416
+ pairAddress: dexScreenerData?.pairAddress || "",
417
+ dexId: dexScreenerData?.dexId || ""
418
+ },
419
+ price: Number.parseFloat(dexScreenerData?.priceUsd || "0"),
420
+ priceUsd: dexScreenerData?.priceUsd || "0",
421
+ price24hChange: dexScreenerData?.priceChange?.h24 || 0,
422
+ marketCap: dexScreenerData?.marketCap || 0,
423
+ liquidityUsd: dexScreenerData?.liquidity?.usd || 0,
424
+ volume24h: dexScreenerData?.volume?.h24 || 0,
425
+ volume24hChange: 0,
426
+ // Need to calculate from historical data
427
+ trades: 0,
428
+ // Would need additional data
429
+ trades24hChange: 0,
430
+ // Would need additional data
431
+ uniqueWallet24h: 0,
432
+ // Would need additional data
433
+ uniqueWallet24hChange: 0,
434
+ // Would need additional data
435
+ holders: 0
436
+ };
437
+ const cacheKey = `token:${chain}:${tokenAddress}:overview`;
438
+ await this.runtime.setCache(
439
+ cacheKey,
440
+ tokenData
441
+ );
442
+ return tokenData;
443
+ }
444
+ throw new Error(`Chain ${chain} not supported`);
445
+ } catch (error) {
446
+ logger.error(`Error fetching token overview for ${tokenAddress}:`, error);
447
+ throw error;
448
+ }
449
+ }
450
+ /**
451
+ * Resolve a ticker to a token address
452
+ */
453
+ async resolveTicker(ticker, chain = SupportedChain.SOLANA, contextMessages) {
454
+ logger.debug(
455
+ `[CommunityInvestorService] Attempting to resolve ticker "${ticker}" on chain ${chain}`
456
+ );
457
+ const cleanTicker = ticker.startsWith("$") ? ticker.substring(1).toUpperCase() : ticker.toUpperCase();
458
+ if (contextMessages) {
459
+ for (const msg of contextMessages.slice().reverse()) {
460
+ if (msg.content?.text?.includes(ticker) && msg.content.text.length > ticker.length + 5) {
461
+ const potentialAddressParts = msg.content.text.split(/[\s(),]+/);
462
+ for (const part of potentialAddressParts) {
463
+ if (chain === SupportedChain.SOLANA && part.length >= 32 && part.length <= 44 && /^[a-zA-Z0-9]+$/.test(part)) {
464
+ logger.info(
465
+ `[CommunityInvestorService] Found potential Solana address ${part} for ticker ${ticker} in context.`
466
+ );
467
+ return {
468
+ address: part,
469
+ chain: SupportedChain.SOLANA,
470
+ ticker: cleanTicker
471
+ };
472
+ }
473
+ if ((chain === SupportedChain.ETHEREUM || chain === SupportedChain.BASE) && part.length === 42 && part.toLowerCase().startsWith("0x") && /^0x[a-fA-F0-9]{40}$/.test(part)) {
474
+ logger.info(
475
+ `[CommunityInvestorService] Found potential Ethereum/Base address ${part} for ticker ${ticker} in context.`
476
+ );
477
+ return {
478
+ address: part,
479
+ chain,
480
+ ticker: cleanTicker
481
+ };
482
+ }
483
+ }
484
+ }
485
+ }
486
+ }
487
+ if (chain === SupportedChain.SOLANA) {
488
+ const knownSolanaTokens = {
489
+ SOL: "So11111111111111111111111111111111111111112",
490
+ USDC: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
491
+ USDT: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
492
+ WIF: "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzL7WDb43cuQu2",
493
+ BONK: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
494
+ JUP: "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
495
+ RAY: "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",
496
+ ORCA: "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",
497
+ SRM: "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",
498
+ FTT: "AGFEad2et2ZJif9jaGpdMixQqvW5i81aBdvKe7PHNfz3"
499
+ };
500
+ if (knownSolanaTokens[cleanTicker]) {
501
+ return {
502
+ address: knownSolanaTokens[cleanTicker],
503
+ chain: SupportedChain.SOLANA,
504
+ ticker: cleanTicker
505
+ };
506
+ }
507
+ try {
508
+ const searchResults = await this.dexscreenerClient.search(cleanTicker, {
509
+ expires: "5m"
510
+ });
511
+ if (searchResults?.pairs && searchResults.pairs.length > 0) {
512
+ const bestPair = searchResults.pairs.filter(
513
+ (pair) => pair.baseToken.symbol.toUpperCase() === cleanTicker
514
+ ).sort(
515
+ (a, b) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0)
516
+ )[0];
517
+ if (bestPair) {
518
+ logger.info(
519
+ `[CommunityInvestorService] Found ${cleanTicker} via DexScreener: ${bestPair.baseToken.address}`
520
+ );
521
+ return {
522
+ address: bestPair.baseToken.address,
523
+ chain: SupportedChain.SOLANA,
524
+ ticker: cleanTicker
525
+ };
526
+ }
527
+ }
528
+ } catch (error) {
529
+ logger.warn(
530
+ `[CommunityInvestorService] DexScreener search failed for ${cleanTicker}:`,
531
+ error
532
+ );
533
+ }
534
+ } else if (chain === SupportedChain.ETHEREUM) {
535
+ const knownEthereumTokens = {
536
+ ETH: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
537
+ // WETH
538
+ USDC: "0xA0b86a33E6441c69De69b9A87e20b88dd75B61FC",
539
+ USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
540
+ DAI: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
541
+ LINK: "0x514910771AF9Ca656af840dff83E8264EcF986CA",
542
+ UNI: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
543
+ WBTC: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
544
+ };
545
+ if (knownEthereumTokens[cleanTicker]) {
546
+ return {
547
+ address: knownEthereumTokens[cleanTicker],
548
+ chain: SupportedChain.ETHEREUM,
549
+ ticker: cleanTicker
550
+ };
551
+ }
552
+ try {
553
+ const searchResults = await this.dexscreenerClient.search(cleanTicker, {
554
+ expires: "5m"
555
+ });
556
+ if (searchResults?.pairs && searchResults.pairs.length > 0) {
557
+ const bestPair = searchResults.pairs.filter(
558
+ (pair) => pair.chainId.toLowerCase() === "ethereum" && pair.baseToken.symbol.toUpperCase() === cleanTicker
559
+ ).sort(
560
+ (a, b) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0)
561
+ )[0];
562
+ if (bestPair) {
563
+ logger.info(
564
+ `[CommunityInvestorService] Found ${cleanTicker} via DexScreener on Ethereum: ${bestPair.baseToken.address}`
565
+ );
566
+ return {
567
+ address: bestPair.baseToken.address,
568
+ chain: SupportedChain.ETHEREUM,
569
+ ticker: cleanTicker
570
+ };
571
+ }
572
+ }
573
+ } catch (error) {
574
+ logger.warn(
575
+ `[CommunityInvestorService] DexScreener search failed for ${cleanTicker} on Ethereum:`,
576
+ error
577
+ );
578
+ }
579
+ } else if (chain === SupportedChain.BASE) {
580
+ const knownBaseTokens = {
581
+ ETH: "0x4200000000000000000000000000000000000006",
582
+ // Base ETH
583
+ USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
584
+ WETH: "0x4200000000000000000000000000000000000006"
585
+ };
586
+ if (knownBaseTokens[cleanTicker]) {
587
+ return {
588
+ address: knownBaseTokens[cleanTicker],
589
+ chain: SupportedChain.BASE,
590
+ ticker: cleanTicker
591
+ };
592
+ }
593
+ try {
594
+ const searchResults = await this.dexscreenerClient.search(cleanTicker, {
595
+ expires: "5m"
596
+ });
597
+ if (searchResults?.pairs && searchResults.pairs.length > 0) {
598
+ const bestPair = searchResults.pairs.filter(
599
+ (pair) => pair.chainId.toLowerCase() === "base" && pair.baseToken.symbol.toUpperCase() === cleanTicker
600
+ ).sort(
601
+ (a, b) => (b.liquidity?.usd || 0) - (a.liquidity?.usd || 0)
602
+ )[0];
603
+ if (bestPair) {
604
+ logger.info(
605
+ `[CommunityInvestorService] Found ${cleanTicker} via DexScreener on Base: ${bestPair.baseToken.address}`
606
+ );
607
+ return {
608
+ address: bestPair.baseToken.address,
609
+ chain: SupportedChain.BASE,
610
+ ticker: cleanTicker
611
+ };
612
+ }
613
+ }
614
+ } catch (error) {
615
+ logger.warn(
616
+ `[CommunityInvestorService] DexScreener search failed for ${cleanTicker} on Base:`,
617
+ error
618
+ );
619
+ }
620
+ }
621
+ logger.warn(
622
+ `[CommunityInvestorService] Could not resolve ticker ${ticker} on chain ${chain}`
623
+ );
624
+ return null;
625
+ }
626
+ /**
627
+ * Get current price for a token
628
+ */
629
+ async getCurrentPrice(chain, tokenAddress) {
630
+ logger.debug("getting current price", chain, tokenAddress);
631
+ try {
632
+ const cacheKey = `token:${chain}:${tokenAddress}:price`;
633
+ const cachedPrice = await this.runtime.getCache(cacheKey);
634
+ if (cachedPrice) {
635
+ return Number.parseFloat(cachedPrice);
636
+ }
637
+ const token = await this.getTokenPerformance(tokenAddress, chain);
638
+ if (token?.price) {
639
+ await this.runtime.setCache(cacheKey, token.price.toString());
640
+ return token.price;
641
+ }
642
+ if (chain.toLowerCase() === "solana") {
643
+ const price = await this.birdeyeClient.fetchPrice(tokenAddress, {
644
+ chain: "solana"
645
+ });
646
+ await this.runtime.setCache(cacheKey, price.toString());
647
+ return price;
648
+ }
649
+ throw new Error(`Chain ${chain} not supported for price fetching`);
650
+ } catch (error) {
651
+ logger.error(`Error fetching current price for ${tokenAddress}:`, error);
652
+ return 0;
653
+ }
654
+ }
655
+ /**
656
+ * Determine if a token should be traded
657
+ */
658
+ async shouldTradeToken(chain, tokenAddress) {
659
+ logger.debug("shouldTradeToken", chain, tokenAddress);
660
+ try {
661
+ const tokenData = await this.getProcessedTokenData(chain, tokenAddress);
662
+ if (!tokenData) return false;
663
+ const { tradeData, security, dexScreenerData } = tokenData;
664
+ if (!dexScreenerData?.pairs || dexScreenerData.pairs.length === 0) {
665
+ return false;
666
+ }
667
+ const pair = dexScreenerData.pairs[0];
668
+ if (!pair.liquidity || pair.liquidity.usd < this.tradingConfig.minLiquidityUsd) {
669
+ return false;
670
+ }
671
+ if (!pair.marketCap || pair.marketCap > this.tradingConfig.maxMarketCapUsd) {
672
+ return false;
673
+ }
674
+ if (security && security.top10HolderPercent > 80) {
675
+ return false;
676
+ }
677
+ if (tradeData && tradeData.volume_24h_usd < 1e3) {
678
+ return false;
679
+ }
680
+ return true;
681
+ } catch (error) {
682
+ logger.error(
683
+ `Error checking if token ${tokenAddress} should be traded:`,
684
+ error
685
+ );
686
+ return false;
687
+ }
688
+ }
689
+ /**
690
+ * Get processed token data with security and trade information
691
+ */
692
+ async getProcessedTokenData(chain, tokenAddress) {
693
+ logger.debug("getting processed token data", chain, tokenAddress);
694
+ try {
695
+ const cacheKey = `token:${chain}:${tokenAddress}:processed`;
696
+ const cachedData = await this.runtime.getCache(cacheKey);
697
+ if (cachedData) {
698
+ return cachedData;
699
+ }
700
+ if (chain.toLowerCase() === "solana") {
701
+ const dexScreenerData = await this.dexscreenerClient.search(
702
+ tokenAddress,
703
+ {
704
+ expires: "5m"
705
+ }
706
+ );
707
+ let tokenTradeData;
708
+ let tokenSecurityData;
709
+ try {
710
+ tokenTradeData = await this.birdeyeClient.fetchTokenTradeData(
711
+ tokenAddress,
712
+ {
713
+ chain: "solana",
714
+ expires: "5m"
715
+ }
716
+ );
717
+ tokenSecurityData = await this.birdeyeClient.fetchTokenSecurity(
718
+ tokenAddress,
719
+ {
720
+ chain: "solana",
721
+ expires: "5m"
722
+ }
723
+ );
724
+ } catch (error) {
725
+ logger.error(`Error fetching token data for ${tokenAddress}:`, error);
726
+ return null;
727
+ }
728
+ const holderDistributionTrend = await this.analyzeHolderDistribution(tokenTradeData);
729
+ let highValueHolders = [];
730
+ let highSupplyHoldersCount = 0;
731
+ if (this.heliusClient) {
732
+ try {
733
+ const holders = await this.heliusClient.fetchHolderList(
734
+ tokenAddress,
735
+ {
736
+ expires: "30m"
737
+ }
738
+ );
739
+ const tokenPrice = Number.parseFloat(
740
+ tokenTradeData.price.toString()
741
+ );
742
+ highValueHolders = holders.filter((holder) => {
743
+ const balance = Number.parseFloat(holder.balance);
744
+ const balanceUsd = balance * tokenPrice;
745
+ return balanceUsd > 5;
746
+ }).map((holder) => ({
747
+ holderAddress: holder.address,
748
+ balanceUsd: (Number.parseFloat(holder.balance) * tokenPrice).toFixed(2)
749
+ }));
750
+ const totalSupply = "0";
751
+ highSupplyHoldersCount = holders.filter((holder) => {
752
+ const holderRatio = Number.parseFloat(holder.balance) / Number.parseFloat(totalSupply);
753
+ return holderRatio > 0.02;
754
+ }).length;
755
+ } catch (error) {
756
+ logger.warn(
757
+ `Error fetching holder data for ${tokenAddress}:`,
758
+ error
759
+ );
760
+ }
761
+ }
762
+ const recentTrades = tokenTradeData.volume_24h > 0;
763
+ const isDexScreenerListed = dexScreenerData.pairs.length > 0;
764
+ const isDexScreenerPaid = dexScreenerData.pairs.some(
765
+ (pair) => pair.boosts && pair.boosts.active > 0
766
+ );
767
+ const processedData = {
768
+ token: {
769
+ address: tokenAddress,
770
+ name: dexScreenerData.pairs[0]?.baseToken?.name || "",
771
+ symbol: dexScreenerData.pairs[0]?.baseToken?.symbol || "",
772
+ decimals: 9,
773
+ // Default for Solana
774
+ logoURI: ""
775
+ },
776
+ security: tokenSecurityData,
777
+ tradeData: tokenTradeData,
778
+ holderDistributionTrend,
779
+ highValueHolders,
780
+ recentTrades,
781
+ highSupplyHoldersCount,
782
+ dexScreenerData,
783
+ isDexScreenerListed,
784
+ isDexScreenerPaid
785
+ };
786
+ await this.runtime.setCache(
787
+ cacheKey,
788
+ processedData
789
+ );
790
+ return processedData;
791
+ }
792
+ throw new Error(`Chain ${chain} not supported for processed token data`);
793
+ } catch (error) {
794
+ logger.error(
795
+ `Error fetching processed token data for ${tokenAddress}:`,
796
+ error
797
+ );
798
+ return null;
799
+ }
800
+ }
801
+ /**
802
+ * Analyze holder distribution trend
803
+ */
804
+ async analyzeHolderDistribution(tradeData) {
805
+ logger.debug("analyzing holder distribution", tradeData);
806
+ const intervals = [
807
+ {
808
+ period: "30m",
809
+ change: tradeData.unique_wallet_30m_change_percent
810
+ },
811
+ { period: "1h", change: tradeData.unique_wallet_1h_change_percent },
812
+ { period: "2h", change: tradeData.unique_wallet_2h_change_percent },
813
+ { period: "4h", change: tradeData.unique_wallet_4h_change_percent },
814
+ { period: "8h", change: tradeData.unique_wallet_8h_change_percent },
815
+ {
816
+ period: "24h",
817
+ change: tradeData.unique_wallet_24h_change_percent
818
+ }
819
+ ];
820
+ const validChanges = intervals.map((interval) => interval.change).filter((change) => change !== null && change !== void 0);
821
+ if (validChanges.length === 0) {
822
+ return "stable";
823
+ }
824
+ const averageChange = validChanges.reduce((acc, curr) => acc + curr, 0) / validChanges.length;
825
+ const increaseThreshold = 10;
826
+ const decreaseThreshold = -10;
827
+ if (averageChange > increaseThreshold) {
828
+ return "increasing";
829
+ }
830
+ if (averageChange < decreaseThreshold) {
831
+ return "decreasing";
832
+ }
833
+ return "stable";
834
+ }
835
+ // ===================== SCORE MANAGER METHODS =====================
836
+ /**
837
+ * Update token performance data
838
+ */
839
+ async updateTokenPerformance(chain, tokenAddress) {
840
+ logger.debug("updating token performance", chain, tokenAddress);
841
+ try {
842
+ const tokenData = await this.getTokenOverview(chain, tokenAddress, true);
843
+ const performance = {
844
+ chain,
845
+ address: tokenAddress,
846
+ name: tokenData.name,
847
+ symbol: tokenData.symbol,
848
+ decimals: tokenData.decimals,
849
+ price: Number.parseFloat(tokenData.priceUsd),
850
+ volume: tokenData.volume24h,
851
+ liquidity: tokenData.liquidityUsd,
852
+ currentMarketCap: tokenData.marketCap,
853
+ holders: tokenData.holders,
854
+ price24hChange: tokenData.price24hChange,
855
+ volume24hChange: tokenData.volume24hChange,
856
+ metadata: tokenData.metadata,
857
+ createdAt: /* @__PURE__ */ new Date(),
858
+ updatedAt: /* @__PURE__ */ new Date()
859
+ };
860
+ await this.storeTokenPerformance(performance);
861
+ return performance;
862
+ } catch (error) {
863
+ logger.error(
864
+ `Error updating token performance for ${tokenAddress}:`,
865
+ error
866
+ );
867
+ throw error;
868
+ }
869
+ }
870
+ /**
871
+ * Calculate risk score for a token
872
+ */
873
+ calculateRiskScore(token) {
874
+ logger.debug("calculating risk score", token);
875
+ let score = 50;
876
+ const liquidity = token.liquidity || 0;
877
+ score -= getLiquidityMultiplier(liquidity);
878
+ const marketCap = token.currentMarketCap || 0;
879
+ score += getMarketCapMultiplier(marketCap);
880
+ const volume = token.volume || 0;
881
+ score -= getVolumeMultiplier(volume);
882
+ if (token.rugPull) score += 30;
883
+ if (token.isScam) score += 30;
884
+ if (token.rapidDump) score += 15;
885
+ if (token.suspiciousVolume) score += 15;
886
+ return Math.max(0, Math.min(100, score));
887
+ }
888
+ /**
889
+ * Update entity metrics based on their recommendation performance
890
+ */
891
+ async updateRecommenderMetrics(entityId, performance = 0) {
892
+ logger.debug("updating recommender metrics", entityId, performance);
893
+ const metrics = await this.getRecommenderMetrics(entityId);
894
+ if (!metrics) {
895
+ await this.initializeRecommenderMetrics(entityId, "default");
896
+ return;
897
+ }
898
+ const updatedMetrics = {
899
+ ...metrics,
900
+ totalRecommendations: metrics.totalRecommendations + 1,
901
+ successfulRecs: performance > 0 ? metrics.successfulRecs + 1 : metrics.successfulRecs,
902
+ avgTokenPerformance: (metrics.avgTokenPerformance * metrics.totalRecommendations + performance) / (metrics.totalRecommendations + 1),
903
+ trustScore: this.calculateTrustScore(metrics, performance)
904
+ };
905
+ await this.storeRecommenderMetrics(updatedMetrics);
906
+ const historyEntry = {
907
+ entityId,
908
+ metrics: updatedMetrics,
909
+ timestamp: /* @__PURE__ */ new Date()
910
+ };
911
+ await this.storeRecommenderMetricsHistory(historyEntry);
912
+ }
913
+ /**
914
+ * Calculate trust score based on metrics and new performance
915
+ */
916
+ calculateTrustScore(metrics, newPerformance) {
917
+ logger.debug("calculating trust score", metrics, newPerformance);
918
+ const HISTORY_WEIGHT = 0.7;
919
+ const NEW_PERFORMANCE_WEIGHT = 0.3;
920
+ const newSuccessRate = (metrics.successfulRecs + (newPerformance > 0 ? 1 : 0)) / (metrics.totalRecommendations + 1);
921
+ const consistencyScore = metrics.consistencyScore || 50;
922
+ const newTrustScore = metrics.trustScore * HISTORY_WEIGHT + (newPerformance > 0 ? 100 : 0) * NEW_PERFORMANCE_WEIGHT;
923
+ const successFactor = newSuccessRate * 100;
924
+ const combinedScore = newTrustScore * 0.6 + successFactor * 0.3 + consistencyScore * 0.1;
925
+ return Math.max(0, Math.min(100, combinedScore));
926
+ }
927
+ // ===================== POSITION METHODS =====================
928
+ /**
929
+ * Get or fetch token performance data
930
+ */
931
+ async getOrFetchTokenPerformance(tokenAddress, chain) {
932
+ logger.debug("getting or fetching token performance", tokenAddress, chain);
933
+ try {
934
+ let tokenPerformance = await this.getTokenPerformance(
935
+ tokenAddress,
936
+ chain
937
+ );
938
+ if (!tokenPerformance) {
939
+ const tokenOverview = await this.getTokenOverview(chain, tokenAddress);
940
+ tokenPerformance = {
941
+ chain,
942
+ address: tokenAddress,
943
+ name: tokenOverview.name,
944
+ symbol: tokenOverview.symbol,
945
+ decimals: tokenOverview.decimals,
946
+ price: Number.parseFloat(tokenOverview.priceUsd),
947
+ volume: tokenOverview.volume24h,
948
+ price24hChange: tokenOverview.price24hChange,
949
+ liquidity: tokenOverview.liquidityUsd,
950
+ holders: tokenOverview.holders,
951
+ createdAt: /* @__PURE__ */ new Date(),
952
+ updatedAt: /* @__PURE__ */ new Date()
953
+ };
954
+ if (tokenPerformance) {
955
+ await this.storeTokenPerformance(tokenPerformance);
956
+ }
957
+ }
958
+ return tokenPerformance;
959
+ } catch (error) {
960
+ logger.error(
961
+ `Error fetching token performance for ${tokenAddress}:`,
962
+ error
963
+ );
964
+ return null;
965
+ }
966
+ }
967
+ /**
968
+ * Validate if a token meets trading criteria
969
+ */
970
+ validateToken(token) {
971
+ if (token.address?.startsWith("sim_")) {
972
+ return true;
973
+ }
974
+ if (token.isScam || token.rugPull) {
975
+ return false;
976
+ }
977
+ const liquidity = token.liquidity || 0;
978
+ if (liquidity < this.tradingConfig.minLiquidityUsd) {
979
+ return false;
980
+ }
981
+ const marketCap = token.currentMarketCap || 0;
982
+ if (marketCap > this.tradingConfig.maxMarketCapUsd) {
983
+ return false;
984
+ }
985
+ return true;
986
+ }
987
+ /**
988
+ * Create a token recommendation
989
+ */
990
+ async createTokenRecommendation(entityId, token, conviction = Conviction.MEDIUM, type = RecommendationType.BUY) {
991
+ logger.debug(
992
+ "creating token recommendation",
993
+ entityId,
994
+ token,
995
+ conviction,
996
+ type
997
+ );
998
+ try {
999
+ const recommendation = {
1000
+ id: uuidv4(),
1001
+ entityId,
1002
+ chain: token.chain || this.tradingConfig.defaultChain,
1003
+ tokenAddress: token.address || "",
1004
+ type,
1005
+ conviction,
1006
+ initialMarketCap: (token.initialMarketCap || 0).toString(),
1007
+ initialLiquidity: (token.liquidity || 0).toString(),
1008
+ initialPrice: (token.price || 0).toString(),
1009
+ marketCap: (token.currentMarketCap || 0).toString(),
1010
+ liquidity: (token.liquidity || 0).toString(),
1011
+ price: (token.price || 0).toString(),
1012
+ rugPull: token.rugPull || false,
1013
+ isScam: token.isScam || false,
1014
+ riskScore: this.calculateRiskScore(token),
1015
+ performanceScore: 0,
1016
+ metadata: {},
1017
+ status: "ACTIVE",
1018
+ createdAt: /* @__PURE__ */ new Date(),
1019
+ updatedAt: /* @__PURE__ */ new Date()
1020
+ };
1021
+ await this.storeTokenRecommendation(recommendation);
1022
+ return recommendation;
1023
+ } catch (error) {
1024
+ logger.error("Error creating token recommendation:", error);
1025
+ return null;
1026
+ }
1027
+ }
1028
+ /**
1029
+ * Calculate buy amount based on entity trust score and conviction
1030
+ */
1031
+ calculateBuyAmount(entity, conviction, token) {
1032
+ logger.debug("calculating buy amount", entity, conviction, token);
1033
+ let trustScore = 50;
1034
+ if (entity.id) {
1035
+ const metricsPromise = this.getRecommenderMetrics(entity.id);
1036
+ metricsPromise.then((metrics) => {
1037
+ if (metrics) {
1038
+ trustScore = metrics.trustScore;
1039
+ }
1040
+ }).catch((error) => {
1041
+ logger.error(`Error getting entity metrics for ${entity.id}:`, error);
1042
+ });
1043
+ }
1044
+ const { baseAmount, minAmount, maxAmount, trustScoreMultiplier } = this.tradingConfig.buyAmountConfig;
1045
+ const trustMultiplier = 1 + trustScore / 100 * trustScoreMultiplier;
1046
+ const convMultiplier = getConvictionMultiplier(conviction);
1047
+ let amount = baseAmount * trustMultiplier * convMultiplier;
1048
+ if (token.liquidity) {
1049
+ amount *= getLiquidityMultiplier(token.liquidity);
1050
+ }
1051
+ amount = Math.max(minAmount, Math.min(maxAmount, amount));
1052
+ return BigInt(Math.floor(amount * 1e9));
1053
+ }
1054
+ /**
1055
+ * Create a new position
1056
+ */
1057
+ async createPosition(recommendationId, entityId, tokenAddress, walletAddress, amount, price, isSimulation) {
1058
+ logger.debug(
1059
+ "creating position",
1060
+ recommendationId,
1061
+ entityId,
1062
+ tokenAddress,
1063
+ walletAddress,
1064
+ amount,
1065
+ price,
1066
+ isSimulation
1067
+ );
1068
+ try {
1069
+ const position = {
1070
+ id: uuidv4(),
1071
+ chain: this.tradingConfig.defaultChain,
1072
+ tokenAddress,
1073
+ walletAddress,
1074
+ isSimulation,
1075
+ entityId,
1076
+ recommendationId,
1077
+ initialPrice: price,
1078
+ balance: "0",
1079
+ status: "OPEN",
1080
+ amount: amount.toString(),
1081
+ createdAt: /* @__PURE__ */ new Date()
1082
+ };
1083
+ await this.storePosition(position);
1084
+ return position;
1085
+ } catch (error) {
1086
+ logger.error("Error creating position:", error);
1087
+ return null;
1088
+ }
1089
+ }
1090
+ /**
1091
+ * Record a transaction
1092
+ */
1093
+ async recordTransaction(positionId, tokenAddress, type, amount, price, isSimulation) {
1094
+ logger.debug(
1095
+ "recording transaction",
1096
+ positionId,
1097
+ tokenAddress,
1098
+ type,
1099
+ amount,
1100
+ price,
1101
+ isSimulation
1102
+ );
1103
+ try {
1104
+ const transaction = {
1105
+ id: uuidv4(),
1106
+ positionId,
1107
+ chain: this.tradingConfig.defaultChain,
1108
+ tokenAddress,
1109
+ type,
1110
+ amount: amount.toString(),
1111
+ price: price.toString(),
1112
+ isSimulation,
1113
+ timestamp: /* @__PURE__ */ new Date()
1114
+ };
1115
+ await this.storeTransaction(transaction);
1116
+ return true;
1117
+ } catch (error) {
1118
+ logger.error("Error recording transaction:", error);
1119
+ return false;
1120
+ }
1121
+ }
1122
+ /**
1123
+ * Get all positions for an entity
1124
+ */
1125
+ async getPositionsByRecommender(entityId) {
1126
+ logger.debug("getting positions by recommender", entityId);
1127
+ try {
1128
+ const recommendations = await this.getRecommendationsByRecommender(entityId);
1129
+ const positions = [];
1130
+ for (const recommendation of recommendations) {
1131
+ const positionMatches = await this.getPositionsByToken(
1132
+ recommendation.tokenAddress
1133
+ );
1134
+ const entityPositions = positionMatches.filter(
1135
+ (position) => position.entityId === entityId
1136
+ );
1137
+ positions.push(...entityPositions);
1138
+ }
1139
+ return positions;
1140
+ } catch (error) {
1141
+ logger.error("Error getting positions by entity:", error);
1142
+ return [];
1143
+ }
1144
+ }
1145
+ /**
1146
+ * Get all positions for a token
1147
+ */
1148
+ async getPositionsByToken(tokenAddress) {
1149
+ logger.debug("getting positions by token", tokenAddress);
1150
+ try {
1151
+ const positions = await this.getOpenPositionsWithBalance();
1152
+ return positions.filter(
1153
+ (position) => position.tokenAddress === tokenAddress
1154
+ );
1155
+ } catch (error) {
1156
+ logger.error("Error getting positions by token:", error);
1157
+ return [];
1158
+ }
1159
+ }
1160
+ /**
1161
+ * Get all transactions for a position
1162
+ */
1163
+ async getTransactionsByPosition(positionId) {
1164
+ logger.debug("getting transactions by position", positionId);
1165
+ try {
1166
+ const query = `transactions for position ${positionId}`;
1167
+ const embedding = await this.runtime.useModel(
1168
+ ModelType.TEXT_EMBEDDING,
1169
+ query
1170
+ );
1171
+ const memories = await this.runtime.searchMemories({
1172
+ tableName: "transactions",
1173
+ embedding,
1174
+ match_threshold: 0.7,
1175
+ limit: 20
1176
+ });
1177
+ const transactions = [];
1178
+ for (const memory of memories) {
1179
+ const transaction = asTransaction(memory.content.transaction);
1180
+ if (transaction && transaction.positionId === positionId) {
1181
+ transactions.push(transaction);
1182
+ }
1183
+ }
1184
+ return transactions;
1185
+ } catch (error) {
1186
+ logger.error("Error getting transactions by position:", error);
1187
+ return [];
1188
+ }
1189
+ }
1190
+ /**
1191
+ * Get all transactions for a token
1192
+ */
1193
+ async getTransactionsByToken(tokenAddress) {
1194
+ logger.debug("getting transactions by token", tokenAddress);
1195
+ try {
1196
+ const query = `transactions for token ${tokenAddress}`;
1197
+ const embedding = await this.runtime.useModel(
1198
+ ModelType.TEXT_EMBEDDING,
1199
+ query
1200
+ );
1201
+ const memories = await this.runtime.searchMemories({
1202
+ tableName: "transactions",
1203
+ embedding,
1204
+ match_threshold: 0.7,
1205
+ limit: 50
1206
+ });
1207
+ const transactions = [];
1208
+ for (const memory of memories) {
1209
+ const transaction = asTransaction(memory.content.transaction);
1210
+ if (transaction && transaction.tokenAddress === tokenAddress) {
1211
+ transactions.push(transaction);
1212
+ }
1213
+ }
1214
+ return transactions;
1215
+ } catch (error) {
1216
+ logger.error("Error getting transactions by token:", error);
1217
+ return [];
1218
+ }
1219
+ }
1220
+ /**
1221
+ * Get a position by ID
1222
+ */
1223
+ async getPosition(positionId) {
1224
+ logger.debug("getting position", positionId);
1225
+ try {
1226
+ const cacheKey = `position:${positionId}`;
1227
+ const cachedPosition = await this.runtime.getCache(cacheKey);
1228
+ if (cachedPosition) {
1229
+ return cachedPosition;
1230
+ }
1231
+ const query = `position with ID ${positionId}`;
1232
+ const embedding = await this.runtime.useModel(
1233
+ ModelType.TEXT_EMBEDDING,
1234
+ query
1235
+ );
1236
+ const memories = await this.runtime.searchMemories({
1237
+ tableName: "positions",
1238
+ embedding,
1239
+ match_threshold: 0.7,
1240
+ limit: 1
1241
+ });
1242
+ const position = asContentObject(
1243
+ memories[0]?.content.position,
1244
+ ["id", "entityId", "tokenAddress"]
1245
+ );
1246
+ if (position) {
1247
+ await this.runtime.setCache(cacheKey, position);
1248
+ return position;
1249
+ }
1250
+ return null;
1251
+ } catch (error) {
1252
+ logger.error("Error getting position:", error);
1253
+ return null;
1254
+ }
1255
+ }
1256
+ /**
1257
+ * Get all recommendations by a entity
1258
+ */
1259
+ async getRecommendationsByRecommender(entityId) {
1260
+ logger.debug("getting recommendations by recommender", entityId);
1261
+ try {
1262
+ const query = `recommendations by entity ${entityId}`;
1263
+ const embedding = await this.runtime.useModel(
1264
+ ModelType.TEXT_EMBEDDING,
1265
+ query
1266
+ );
1267
+ const memories = await this.runtime.searchMemories({
1268
+ tableName: "recommendations",
1269
+ embedding,
1270
+ match_threshold: 0.7,
1271
+ limit: 50
1272
+ });
1273
+ const recommendations = [];
1274
+ for (const memory of memories) {
1275
+ const meta = memory.metadata;
1276
+ if (meta?.recommendation && meta.recommendation.entityId === entityId) {
1277
+ recommendations.push(meta.recommendation);
1278
+ }
1279
+ }
1280
+ return recommendations;
1281
+ } catch (error) {
1282
+ logger.error("Error getting recommendations by entity:", error);
1283
+ return [];
1284
+ }
1285
+ }
1286
+ /**
1287
+ * Close a position and update metrics
1288
+ */
1289
+ async closePosition(positionId) {
1290
+ logger.debug("closing position", positionId);
1291
+ try {
1292
+ const position = await this.getPosition(positionId);
1293
+ if (!position) {
1294
+ logger.error(`Position ${positionId} not found`);
1295
+ return false;
1296
+ }
1297
+ position.status = "CLOSED";
1298
+ position.closedAt = /* @__PURE__ */ new Date();
1299
+ const transactions = await this.getTransactionsByPosition(positionId);
1300
+ const performance = await this.calculatePositionPerformance(
1301
+ position,
1302
+ transactions
1303
+ );
1304
+ await this.updateRecommenderMetrics(position.entityId, performance);
1305
+ await this.storePosition(position);
1306
+ return true;
1307
+ } catch (error) {
1308
+ logger.error(`Failed to close position ${positionId}:`, error);
1309
+ return false;
1310
+ }
1311
+ }
1312
+ /**
1313
+ * Calculate position performance
1314
+ */
1315
+ async calculatePositionPerformance(position, transactions) {
1316
+ logger.debug("calculating position performance", position, transactions);
1317
+ if (!transactions.length) return 0;
1318
+ const buyTxs = transactions.filter((t) => t.type === TransactionType.BUY);
1319
+ const sellTxs = transactions.filter((t) => t.type === TransactionType.SELL);
1320
+ const totalBuyAmount = buyTxs.reduce(
1321
+ (sum, tx) => sum + BigInt(tx.amount),
1322
+ 0n
1323
+ );
1324
+ const _totalSellAmount = sellTxs.reduce(
1325
+ (sum, tx) => sum + BigInt(tx.amount),
1326
+ 0n
1327
+ );
1328
+ position.amount = totalBuyAmount.toString();
1329
+ const avgBuyPrice = buyTxs.reduce((sum, tx) => sum + Number(tx.price), 0) / buyTxs.length;
1330
+ const avgSellPrice = sellTxs.length ? sellTxs.reduce((sum, tx) => sum + Number(tx.price), 0) / sellTxs.length : await this.getCurrentPrice(position.chain, position.tokenAddress);
1331
+ position.currentPrice = avgSellPrice.toString();
1332
+ return (avgSellPrice - avgBuyPrice) / avgBuyPrice * 100;
1333
+ }
1334
+ /**
1335
+ * Store token performance data
1336
+ */
1337
+ async storeTokenPerformance(token) {
1338
+ logger.debug("storing token performance", token);
1339
+ try {
1340
+ const text = `Token performance data for ${token.symbol || token.address} on ${token.chain}`;
1341
+ const memory = {
1342
+ id: uuidv4(),
1343
+ entityId: this.runtime.agentId,
1344
+ roomId: "global",
1345
+ content: {
1346
+ text,
1347
+ token: toJsonRecord(token)
1348
+ },
1349
+ createdAt: Date.now()
1350
+ };
1351
+ const embedding = await this.runtime.useModel("TEXT_EMBEDDING", text);
1352
+ const memoryWithEmbedding = { ...memory, embedding };
1353
+ await this.runtime.createMemory(memoryWithEmbedding, "tokens", true);
1354
+ const cacheKey = `token:${token.chain}:${token.address}:performance`;
1355
+ await this.runtime.setCache(cacheKey, token);
1356
+ } catch (error) {
1357
+ logger.error(
1358
+ `Error storing token performance for ${token.address}:`,
1359
+ error
1360
+ );
1361
+ }
1362
+ }
1363
+ /**
1364
+ * Store position data
1365
+ */
1366
+ async storePosition(position) {
1367
+ logger.debug("storing position", position);
1368
+ try {
1369
+ const text = `Position data for token ${position.tokenAddress} by entity ${position.entityId}`;
1370
+ const memory = {
1371
+ id: uuidv4(),
1372
+ entityId: this.runtime.agentId,
1373
+ roomId: "global",
1374
+ content: {
1375
+ text,
1376
+ position: toJsonRecord(position)
1377
+ },
1378
+ createdAt: Date.now()
1379
+ };
1380
+ const embedding = await this.runtime.useModel("TEXT_EMBEDDING", text);
1381
+ const memoryWithEmbedding = { ...memory, embedding };
1382
+ await this.runtime.createMemory(memoryWithEmbedding, "positions", true);
1383
+ const cacheKey = `position:${position.id}`;
1384
+ await this.runtime.setCache(cacheKey, position);
1385
+ } catch (error) {
1386
+ logger.error(
1387
+ `Error storing position for ${position.tokenAddress}:`,
1388
+ error
1389
+ );
1390
+ }
1391
+ }
1392
+ /**
1393
+ * Store transaction data
1394
+ */
1395
+ async storeTransaction(transaction) {
1396
+ logger.debug("storing transaction", transaction);
1397
+ try {
1398
+ const text = `Transaction data for position ${transaction.positionId} token ${transaction.tokenAddress} ${transaction.type}`;
1399
+ const memory = {
1400
+ id: uuidv4(),
1401
+ entityId: this.runtime.agentId,
1402
+ roomId: "global",
1403
+ content: {
1404
+ text,
1405
+ transaction: toJsonRecord(transaction)
1406
+ },
1407
+ createdAt: Date.now()
1408
+ };
1409
+ const embedding = await this.runtime.useModel("TEXT_EMBEDDING", text);
1410
+ const memoryWithEmbedding = { ...memory, embedding };
1411
+ await this.runtime.createMemory(
1412
+ memoryWithEmbedding,
1413
+ "transactions",
1414
+ true
1415
+ );
1416
+ const cacheKey = `position:${transaction.positionId}:transactions`;
1417
+ const cachedTxs = await this.runtime.getCache(cacheKey);
1418
+ if (cachedTxs) {
1419
+ const txs = cachedTxs;
1420
+ txs.push(transaction);
1421
+ await this.runtime.setCache(cacheKey, txs);
1422
+ } else {
1423
+ await this.runtime.setCache(cacheKey, [transaction]);
1424
+ }
1425
+ } catch (error) {
1426
+ logger.error(
1427
+ `Error storing transaction for position ${transaction.positionId}:`,
1428
+ error
1429
+ );
1430
+ }
1431
+ }
1432
+ /**
1433
+ * Store token recommendation data
1434
+ */
1435
+ async storeTokenRecommendation(recommendation) {
1436
+ logger.debug("storing token recommendation", recommendation);
1437
+ try {
1438
+ const text = `Token recommendation for ${recommendation.tokenAddress} by entity ${recommendation.entityId}`;
1439
+ const memory = {
1440
+ id: uuidv4(),
1441
+ entityId: this.runtime.agentId,
1442
+ roomId: "global",
1443
+ content: {
1444
+ text,
1445
+ recommendation: toJsonRecord(recommendation)
1446
+ },
1447
+ createdAt: Date.now()
1448
+ };
1449
+ const embedding = await this.runtime.useModel("TEXT_EMBEDDING", text);
1450
+ const memoryWithEmbedding = { ...memory, embedding };
1451
+ await this.runtime.createMemory(
1452
+ memoryWithEmbedding,
1453
+ "recommendations",
1454
+ true
1455
+ );
1456
+ const cacheKey = `recommendation:${recommendation.id}`;
1457
+ await this.runtime.setCache(
1458
+ cacheKey,
1459
+ recommendation
1460
+ );
1461
+ } catch (error) {
1462
+ logger.error(
1463
+ `Error storing recommendation for ${recommendation.tokenAddress}:`,
1464
+ error
1465
+ );
1466
+ }
1467
+ }
1468
+ /**
1469
+ * Store entity metrics
1470
+ */
1471
+ async storeRecommenderMetrics(metrics) {
1472
+ logger.debug("storing recommender metrics", metrics);
1473
+ try {
1474
+ const text = `Recommender metrics for ${metrics.entityId}`;
1475
+ const memory = {
1476
+ id: uuidv4(),
1477
+ entityId: this.runtime.agentId,
1478
+ roomId: "global",
1479
+ content: {
1480
+ text,
1481
+ metrics: toJsonRecord(metrics)
1482
+ },
1483
+ createdAt: Date.now()
1484
+ };
1485
+ const embedding = await this.runtime.useModel("TEXT_EMBEDDING", text);
1486
+ const memoryWithEmbedding = { ...memory, embedding };
1487
+ await this.runtime.createMemory(
1488
+ memoryWithEmbedding,
1489
+ "recommender_metrics",
1490
+ true
1491
+ );
1492
+ const cacheKey = `entity:${metrics.entityId}:metrics`;
1493
+ await this.runtime.setCache(cacheKey, metrics);
1494
+ } catch (error) {
1495
+ logger.error(
1496
+ `Error storing entity metrics for ${metrics.entityId}:`,
1497
+ error
1498
+ );
1499
+ }
1500
+ }
1501
+ /**
1502
+ * Store entity metrics history
1503
+ */
1504
+ async storeRecommenderMetricsHistory(history) {
1505
+ logger.debug("storing recommender metrics history", history);
1506
+ try {
1507
+ const text = `Recommender metrics history for ${history.entityId}`;
1508
+ const memory = {
1509
+ id: uuidv4(),
1510
+ entityId: this.runtime.agentId,
1511
+ roomId: "global",
1512
+ content: {
1513
+ text,
1514
+ history: toJsonRecord(history)
1515
+ },
1516
+ createdAt: Date.now()
1517
+ };
1518
+ const embedding = await this.runtime.useModel("TEXT_EMBEDDING", text);
1519
+ const memoryWithEmbedding = { ...memory, embedding };
1520
+ await this.runtime.createMemory(
1521
+ memoryWithEmbedding,
1522
+ "recommender_metrics_history",
1523
+ true
1524
+ );
1525
+ const cacheKey = `entity:${history.entityId}:history`;
1526
+ const cachedHistory = await this.runtime.getCache(cacheKey);
1527
+ if (cachedHistory) {
1528
+ const histories = cachedHistory;
1529
+ histories.push(history);
1530
+ const recentHistories = histories.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, 10);
1531
+ await this.runtime.setCache(
1532
+ cacheKey,
1533
+ recentHistories
1534
+ );
1535
+ } else {
1536
+ await this.runtime.setCache(cacheKey, [
1537
+ history
1538
+ ]);
1539
+ }
1540
+ } catch (error) {
1541
+ logger.error(
1542
+ `Error storing entity metrics history for ${history.entityId}:`,
1543
+ error
1544
+ );
1545
+ }
1546
+ }
1547
+ /**
1548
+ * Get entity metrics
1549
+ */
1550
+ async getRecommenderMetrics(entityId) {
1551
+ logger.debug("getting recommender metrics", entityId);
1552
+ try {
1553
+ const cacheKey = `entity:${entityId}:metrics`;
1554
+ const cachedMetrics = await this.runtime.getCache(cacheKey);
1555
+ if (cachedMetrics) {
1556
+ return cachedMetrics;
1557
+ }
1558
+ const query = `entity metrics for entity ${entityId}`;
1559
+ const embedding = await this.runtime.useModel(
1560
+ ModelType.TEXT_EMBEDDING,
1561
+ query
1562
+ );
1563
+ const memories = await this.runtime.searchMemories({
1564
+ tableName: "recommender_metrics",
1565
+ embedding,
1566
+ match_threshold: 0.7,
1567
+ limit: 1
1568
+ });
1569
+ const metrics = asContentObject(
1570
+ memories[0]?.content.metrics,
1571
+ ["entityId", "platform"]
1572
+ );
1573
+ if (metrics) {
1574
+ await this.runtime.setCache(cacheKey, metrics);
1575
+ return metrics;
1576
+ }
1577
+ return null;
1578
+ } catch (error) {
1579
+ logger.error(`Error getting entity metrics for ${entityId}:`, error);
1580
+ return null;
1581
+ }
1582
+ }
1583
+ /**
1584
+ * Get entity metrics history
1585
+ */
1586
+ async getRecommenderMetricsHistory(entityId) {
1587
+ logger.debug("getting recommender metrics history", entityId);
1588
+ try {
1589
+ const cacheKey = `entity:${entityId}:history`;
1590
+ const cachedHistory = await this.runtime.getCache(cacheKey);
1591
+ if (cachedHistory) {
1592
+ return cachedHistory;
1593
+ }
1594
+ const query = `entity metrics history for entity ${entityId}`;
1595
+ const embedding = await this.runtime.useModel(
1596
+ ModelType.TEXT_EMBEDDING,
1597
+ query
1598
+ );
1599
+ const memories = await this.runtime.searchMemories({
1600
+ tableName: "recommender_metrics_history",
1601
+ embedding,
1602
+ match_threshold: 0.7,
1603
+ limit: 10
1604
+ });
1605
+ const historyEntries = [];
1606
+ for (const memory of memories) {
1607
+ const history = asContentObject(
1608
+ memory.content.history,
1609
+ ["entityId"]
1610
+ );
1611
+ if (history && history.entityId === entityId) {
1612
+ historyEntries.push(history);
1613
+ }
1614
+ }
1615
+ const sortedEntries = historyEntries.sort(
1616
+ (a, b) => b.timestamp.getTime() - a.timestamp.getTime()
1617
+ );
1618
+ await this.runtime.setCache(
1619
+ cacheKey,
1620
+ sortedEntries
1621
+ );
1622
+ return sortedEntries;
1623
+ } catch (error) {
1624
+ logger.error(
1625
+ `Error getting entity metrics history for ${entityId}:`,
1626
+ error
1627
+ );
1628
+ return [];
1629
+ }
1630
+ }
1631
+ /**
1632
+ * Initialize entity metrics
1633
+ */
1634
+ async initializeRecommenderMetrics(entityId, platform) {
1635
+ logger.debug("initializing recommender metrics", entityId, platform);
1636
+ try {
1637
+ const initialMetrics = {
1638
+ entityId,
1639
+ platform,
1640
+ totalRecommendations: 0,
1641
+ successfulRecs: 0,
1642
+ consistencyScore: 50,
1643
+ trustScore: 50,
1644
+ failedTrades: 0,
1645
+ totalProfit: 0,
1646
+ avgTokenPerformance: 0,
1647
+ lastUpdated: /* @__PURE__ */ new Date(),
1648
+ createdAt: /* @__PURE__ */ new Date()
1649
+ };
1650
+ await this.storeRecommenderMetrics(initialMetrics);
1651
+ const historyEntry = {
1652
+ entityId,
1653
+ metrics: initialMetrics,
1654
+ timestamp: /* @__PURE__ */ new Date()
1655
+ };
1656
+ await this.storeRecommenderMetricsHistory(historyEntry);
1657
+ } catch (error) {
1658
+ logger.error(`Error initializing entity metrics for ${entityId}:`, error);
1659
+ }
1660
+ }
1661
+ /**
1662
+ * Get token performance
1663
+ */
1664
+ async getTokenPerformance(tokenAddress, chain) {
1665
+ logger.debug("getting token performance", tokenAddress, chain);
1666
+ try {
1667
+ const cacheKey = `token:${chain}:${tokenAddress}:performance`;
1668
+ const cachedToken = await this.runtime.getCache(cacheKey);
1669
+ if (cachedToken) {
1670
+ return cachedToken;
1671
+ }
1672
+ const query = `token performance for ${tokenAddress}`;
1673
+ const embedding = await this.runtime.useModel(
1674
+ ModelType.TEXT_EMBEDDING,
1675
+ query
1676
+ );
1677
+ const memories = await this.runtime.searchMemories({
1678
+ tableName: "tokens",
1679
+ embedding,
1680
+ match_threshold: 0.7,
1681
+ limit: 1
1682
+ });
1683
+ if (memories.length > 0 && memories[0].content.token) {
1684
+ const token = memories[0].content.token;
1685
+ await this.runtime.setCache(cacheKey, token);
1686
+ return token;
1687
+ }
1688
+ return null;
1689
+ } catch (error) {
1690
+ logger.error(
1691
+ `Error getting token performance for ${tokenAddress}:`,
1692
+ error
1693
+ );
1694
+ return null;
1695
+ }
1696
+ }
1697
+ /**
1698
+ * Get open positions with balance
1699
+ */
1700
+ async getOpenPositionsWithBalance() {
1701
+ logger.debug("getting open positions with balance");
1702
+ try {
1703
+ const cacheKey = "positions:open:with-balance";
1704
+ const cachedPositions = await this.runtime.getCache(cacheKey);
1705
+ if (cachedPositions) {
1706
+ return cachedPositions;
1707
+ }
1708
+ const query = "open positions with balance";
1709
+ const embedding = await this.runtime.useModel(
1710
+ ModelType.TEXT_EMBEDDING,
1711
+ query
1712
+ );
1713
+ const memories = await this.runtime.searchMemories({
1714
+ tableName: "positions",
1715
+ embedding,
1716
+ match_threshold: 0.7,
1717
+ limit: 50
1718
+ });
1719
+ const positions = [];
1720
+ for (const memory of memories) {
1721
+ const position = asContentObject(memory.content.position, [
1722
+ "id",
1723
+ "entityId",
1724
+ "tokenAddress"
1725
+ ]);
1726
+ if (position) {
1727
+ if (position.status === "OPEN") {
1728
+ positions.push({
1729
+ ...position,
1730
+ balance: BigInt(position.balance || "0")
1731
+ });
1732
+ }
1733
+ }
1734
+ }
1735
+ await this.runtime.setCache(cacheKey, positions);
1736
+ return positions;
1737
+ } catch (error) {
1738
+ logger.error("Error getting open positions with balance:", error);
1739
+ return [];
1740
+ }
1741
+ }
1742
+ /**
1743
+ * Get positions transactions
1744
+ */
1745
+ async getPositionsTransactions(positionIds) {
1746
+ logger.debug("getting positions transactions", positionIds);
1747
+ try {
1748
+ const allTransactions = [];
1749
+ for (const positionId of positionIds) {
1750
+ const transactions = await this.getTransactionsByPosition(positionId);
1751
+ allTransactions.push(...transactions);
1752
+ }
1753
+ return allTransactions;
1754
+ } catch (error) {
1755
+ logger.error("Error getting transactions for positions:", error);
1756
+ return [];
1757
+ }
1758
+ }
1759
+ /**
1760
+ * Get formatted portfolio report
1761
+ */
1762
+ async getFormattedPortfolioReport(entityId) {
1763
+ logger.debug("getting formatted portfolio report", entityId);
1764
+ try {
1765
+ const positions = await this.getOpenPositionsWithBalance();
1766
+ const filteredPositions = entityId ? positions.filter((p) => p.entityId === entityId) : positions;
1767
+ if (filteredPositions.length === 0) {
1768
+ return "No open positions found.";
1769
+ }
1770
+ const tokens = [];
1771
+ const tokenSet = /* @__PURE__ */ new Set();
1772
+ for (const position of filteredPositions) {
1773
+ if (tokenSet.has(`${position.chain}:${position.tokenAddress}`))
1774
+ continue;
1775
+ const token = await this.getTokenPerformance(
1776
+ position.tokenAddress,
1777
+ position.chain
1778
+ );
1779
+ if (token) tokens.push(token);
1780
+ tokenSet.add(`${position.chain}:${position.tokenAddress}`);
1781
+ }
1782
+ const transactions = await this.getPositionsTransactions(
1783
+ filteredPositions.map((p) => p.id)
1784
+ );
1785
+ const report = formatFullReport(tokens, filteredPositions, transactions);
1786
+ return `
1787
+ Portfolio Summary:
1788
+ Total Current Value: ${report.totalCurrentValue}
1789
+ Total Realized P&L: ${report.totalRealizedPnL}
1790
+ Total Unrealized P&L: ${report.totalUnrealizedPnL}
1791
+ Total P&L: ${report.totalPnL}
1792
+
1793
+ Positions:
1794
+ ${report.positionReports.join("\n")}
1795
+
1796
+ Tokens:
1797
+ ${report.tokenReports.join("\n")}
1798
+ `.trim();
1799
+ } catch (error) {
1800
+ logger.error("Error generating portfolio report:", error);
1801
+ return "Error generating portfolio report.";
1802
+ }
1803
+ }
1804
+ async initialize(runtime) {
1805
+ logger.info("[CommunityInvestorService] Initializing...");
1806
+ this.apiKeys.birdeye = runtime.getSetting("BIRDEYE_API_KEY");
1807
+ this.apiKeys.moralis = runtime.getSetting("MORALIS_API_KEY");
1808
+ await this.loadUserRegistry();
1809
+ logger.info("[CommunityInvestorService] Initialized.");
1810
+ }
1811
+ /**
1812
+ * Fetches token data from an external API.
1813
+ * Uses Birdeye and DexScreener for real market data.
1814
+ */
1815
+ async getTokenAPIData(address, chain) {
1816
+ logger.debug(
1817
+ `[CommunityInvestorService] Fetching token API data for ${address} on ${chain}`
1818
+ );
1819
+ try {
1820
+ let tokenData = {};
1821
+ if (chain === SupportedChain.SOLANA) {
1822
+ try {
1823
+ const [tokenOverview, price, security, tradeData, dexScreenerData] = await Promise.all([
1824
+ this.birdeyeClient.fetchTokenOverview(address, {
1825
+ chain: "solana",
1826
+ expires: "5m"
1827
+ }),
1828
+ this.birdeyeClient.fetchPrice(address, {
1829
+ chain: "solana",
1830
+ expires: "1m"
1831
+ }),
1832
+ this.birdeyeClient.fetchTokenSecurity(address, {
1833
+ chain: "solana",
1834
+ expires: "10m"
1835
+ }),
1836
+ this.birdeyeClient.fetchTokenTradeData(address, {
1837
+ chain: "solana",
1838
+ expires: "5m"
1839
+ }),
1840
+ this.dexscreenerClient.search(address, { expires: "5m" })
1841
+ ]);
1842
+ const dexPair = dexScreenerData.pairs?.[0];
1843
+ tokenData = {
1844
+ name: tokenOverview.name || dexPair?.baseToken?.name,
1845
+ symbol: tokenOverview.symbol || dexPair?.baseToken?.symbol,
1846
+ currentPrice: price || parseFloat(dexPair?.priceUsd || "0"),
1847
+ liquidity: dexPair?.liquidity?.usd || 0,
1848
+ marketCap: dexPair?.marketCap || tradeData.market || 0,
1849
+ isKnownScam: false
1850
+ // Would need additional scam detection logic
1851
+ };
1852
+ if (tradeData) {
1853
+ const recent24hPrices = [
1854
+ tradeData.price,
1855
+ tradeData.history_24h_price,
1856
+ tradeData.history_12h_price,
1857
+ tradeData.history_8h_price,
1858
+ tradeData.history_6h_price,
1859
+ tradeData.history_4h_price,
1860
+ tradeData.history_2h_price,
1861
+ tradeData.history_1h_price,
1862
+ tradeData.history_30m_price
1863
+ ].filter((p) => p != null && p > 0);
1864
+ if (recent24hPrices.length > 0) {
1865
+ tokenData.ath = Math.max(...recent24hPrices);
1866
+ tokenData.atl = Math.min(...recent24hPrices);
1867
+ }
1868
+ const now = Date.now();
1869
+ tokenData.priceHistory = [
1870
+ {
1871
+ timestamp: now - 24 * 60 * 60 * 1e3,
1872
+ price: tradeData.history_24h_price || tradeData.price
1873
+ },
1874
+ {
1875
+ timestamp: now - 12 * 60 * 60 * 1e3,
1876
+ price: tradeData.history_12h_price || tradeData.price
1877
+ },
1878
+ {
1879
+ timestamp: now - 8 * 60 * 60 * 1e3,
1880
+ price: tradeData.history_8h_price || tradeData.price
1881
+ },
1882
+ {
1883
+ timestamp: now - 6 * 60 * 60 * 1e3,
1884
+ price: tradeData.history_6h_price || tradeData.price
1885
+ },
1886
+ {
1887
+ timestamp: now - 4 * 60 * 60 * 1e3,
1888
+ price: tradeData.history_4h_price || tradeData.price
1889
+ },
1890
+ {
1891
+ timestamp: now - 2 * 60 * 60 * 1e3,
1892
+ price: tradeData.history_2h_price || tradeData.price
1893
+ },
1894
+ {
1895
+ timestamp: now - 1 * 60 * 60 * 1e3,
1896
+ price: tradeData.history_1h_price || tradeData.price
1897
+ },
1898
+ {
1899
+ timestamp: now - 30 * 60 * 1e3,
1900
+ price: tradeData.history_30m_price || tradeData.price
1901
+ },
1902
+ { timestamp: now, price: tradeData.price }
1903
+ ].filter((p) => p.price != null && p.price > 0);
1904
+ const hasRugPullPattern = tradeData.price_change_24h_percent < -90 || // 90% drop in 24h
1905
+ tradeData.volume_24h_usd < 1e3 && tokenData.marketCap && tokenData.marketCap > 1e5 || // Low volume but high market cap
1906
+ security && security.top10HolderPercent > 95;
1907
+ tokenData.isKnownScam = hasRugPullPattern;
1908
+ }
1909
+ } catch (error) {
1910
+ logger.warn(
1911
+ `[CommunityInvestorService] Error fetching Solana token data for ${address}:`,
1912
+ error
1913
+ );
1914
+ try {
1915
+ const dexScreenerData = await this.dexscreenerClient.search(
1916
+ address,
1917
+ { expires: "5m" }
1918
+ );
1919
+ const dexPair = dexScreenerData.pairs?.[0];
1920
+ if (dexPair) {
1921
+ tokenData = {
1922
+ name: dexPair.baseToken.name,
1923
+ symbol: dexPair.baseToken.symbol,
1924
+ currentPrice: parseFloat(dexPair.priceUsd || "0"),
1925
+ liquidity: dexPair.liquidity?.usd || 0,
1926
+ marketCap: dexPair.marketCap || 0,
1927
+ isKnownScam: false
1928
+ // Dexscreener doesn't directly provide this
1929
+ };
1930
+ } else {
1931
+ logger.debug(
1932
+ `[CommunityInvestorService] DexScreener found no pair for ${address} after Birdeye failure.`
1933
+ );
1934
+ return null;
1935
+ }
1936
+ } catch (fallbackError) {
1937
+ logger.error(
1938
+ `[CommunityInvestorService] Fallback DexScreener search also failed for ${address}:`,
1939
+ fallbackError
1940
+ );
1941
+ return null;
1942
+ }
1943
+ }
1944
+ } else if (chain === SupportedChain.ETHEREUM || chain === SupportedChain.BASE) {
1945
+ try {
1946
+ const dexScreenerData = await this.dexscreenerClient.search(address, {
1947
+ expires: "5m"
1948
+ });
1949
+ const chainFilter = chain === SupportedChain.ETHEREUM ? "ethereum" : "base";
1950
+ const dexPair = dexScreenerData.pairs?.find(
1951
+ (pair) => pair.chainId.toLowerCase() === chainFilter
1952
+ );
1953
+ if (dexPair) {
1954
+ tokenData = {
1955
+ name: dexPair.baseToken.name,
1956
+ symbol: dexPair.baseToken.symbol,
1957
+ currentPrice: parseFloat(dexPair.priceUsd || "0"),
1958
+ liquidity: dexPair.liquidity?.usd || 0,
1959
+ marketCap: dexPair.marketCap || 0,
1960
+ isKnownScam: false
1961
+ };
1962
+ const now = Date.now();
1963
+ const currentPrice = parseFloat(dexPair.priceUsd || "0");
1964
+ tokenData.priceHistory = [
1965
+ {
1966
+ timestamp: now - 24 * 60 * 60 * 1e3,
1967
+ price: currentPrice / (1 + (dexPair.priceChange?.h24 || 0) / 100)
1968
+ },
1969
+ {
1970
+ timestamp: now - 6 * 60 * 60 * 1e3,
1971
+ price: currentPrice / (1 + (dexPair.priceChange?.h6 || 0) / 100)
1972
+ },
1973
+ {
1974
+ timestamp: now - 1 * 60 * 60 * 1e3,
1975
+ price: currentPrice / (1 + (dexPair.priceChange?.h1 || 0) / 100)
1976
+ },
1977
+ {
1978
+ timestamp: now - 5 * 60 * 1e3,
1979
+ price: currentPrice / (1 + (dexPair.priceChange?.m5 || 0) / 100)
1980
+ },
1981
+ { timestamp: now, price: currentPrice }
1982
+ ].filter((p) => p.price > 0);
1983
+ if (tokenData.priceHistory.length > 0) {
1984
+ tokenData.ath = Math.max(
1985
+ ...tokenData.priceHistory.map((p) => p.price)
1986
+ );
1987
+ tokenData.atl = Math.min(
1988
+ ...tokenData.priceHistory.map((p) => p.price)
1989
+ );
1990
+ }
1991
+ const hasRugPullPattern = (dexPair.priceChange?.h24 || 0) < -90 || // 90% drop in 24h
1992
+ (dexPair.volume?.h24 || 0) < 1e3 && (dexPair.marketCap || 0) > 1e5;
1993
+ tokenData.isKnownScam = hasRugPullPattern;
1994
+ }
1995
+ } catch (error) {
1996
+ logger.error(
1997
+ `[CommunityInvestorService] Error fetching ${chain} token data for ${address}:`,
1998
+ error
1999
+ );
2000
+ return null;
2001
+ }
2002
+ } else {
2003
+ logger.warn(`[CommunityInvestorService] Unsupported chain: ${chain}`);
2004
+ return null;
2005
+ }
2006
+ if (Object.keys(tokenData).length > 0) {
2007
+ if (!tokenData.ath && tokenData.currentPrice) {
2008
+ tokenData.ath = tokenData.currentPrice * 1.1;
2009
+ }
2010
+ if (!tokenData.atl && tokenData.currentPrice) {
2011
+ tokenData.atl = tokenData.currentPrice * 0.9;
2012
+ }
2013
+ logger.debug(
2014
+ `[CommunityInvestorService] Successfully fetched token data for ${tokenData.symbol || address} (${address})`
2015
+ );
2016
+ return tokenData;
2017
+ } else if (chain !== SupportedChain.SOLANA) {
2018
+ logger.warn(
2019
+ `[CommunityInvestorService] Failed to fetch token data for ${address} on ${chain} after all attempts.`
2020
+ );
2021
+ return null;
2022
+ }
2023
+ return null;
2024
+ } catch (error) {
2025
+ logger.error(
2026
+ `[CommunityInvestorService] Unexpected error fetching token API data for ${address}:`,
2027
+ error
2028
+ );
2029
+ return null;
2030
+ }
2031
+ }
2032
+ async isLikelyScamOrRug(tokenData, recommendationTimestamp) {
2033
+ if (tokenData.isKnownScam) {
2034
+ logger.warn(
2035
+ `[CommunityInvestorService] Token ${tokenData.symbol} already flagged as known scam`
2036
+ );
2037
+ return true;
2038
+ }
2039
+ const warnings = [];
2040
+ let riskScore = 0;
2041
+ const pricesPostRecommendation = tokenData.priceHistory?.filter(
2042
+ (p) => p.timestamp > recommendationTimestamp
2043
+ ) || [];
2044
+ if (pricesPostRecommendation.length > 1) {
2045
+ const peakPricePostRec = Math.max(
2046
+ ...pricesPostRecommendation.map((p) => p.price)
2047
+ );
2048
+ const lastKnownPricePostRec = pricesPostRecommendation[pricesPostRecommendation.length - 1].price;
2049
+ const currentPrice = tokenData.currentPrice || 0;
2050
+ if (peakPricePostRec > 0 && lastKnownPricePostRec < peakPricePostRec * 0.1 && currentPrice < peakPricePostRec * 0.1) {
2051
+ warnings.push(
2052
+ `Severe price drop: >90% from peak ($${peakPricePostRec.toFixed(6)} to $${currentPrice.toFixed(6)})`
2053
+ );
2054
+ riskScore += 40;
2055
+ } else if (peakPricePostRec > 0 && lastKnownPricePostRec < peakPricePostRec * 0.3 && currentPrice < peakPricePostRec * 0.3) {
2056
+ warnings.push(
2057
+ `Major price drop: >70% from peak ($${peakPricePostRec.toFixed(6)} to $${currentPrice.toFixed(6)})`
2058
+ );
2059
+ riskScore += 25;
2060
+ }
2061
+ }
2062
+ const marketCap = tokenData.marketCap || 0;
2063
+ const liquidity = tokenData.liquidity || 0;
2064
+ if (marketCap > 1e4 && liquidity > 0) {
2065
+ const liquidityRatio = liquidity / marketCap;
2066
+ if (liquidityRatio < 5e-3) {
2067
+ warnings.push(
2068
+ `Extremely low liquidity ratio: ${(liquidityRatio * 100).toFixed(2)}% (Liquidity: $${liquidity.toFixed(0)}, MC: $${marketCap.toFixed(0)})`
2069
+ );
2070
+ riskScore += 30;
2071
+ } else if (liquidityRatio < 0.01) {
2072
+ warnings.push(
2073
+ `Very low liquidity ratio: ${(liquidityRatio * 100).toFixed(2)}%`
2074
+ );
2075
+ riskScore += 20;
2076
+ } else if (liquidityRatio < 0.02) {
2077
+ warnings.push(
2078
+ `Low liquidity ratio: ${(liquidityRatio * 100).toFixed(2)}%`
2079
+ );
2080
+ riskScore += 10;
2081
+ }
2082
+ }
2083
+ if (liquidity > 0) {
2084
+ if (liquidity < 500) {
2085
+ warnings.push(`Critical liquidity: $${liquidity.toFixed(0)}`);
2086
+ riskScore += 35;
2087
+ } else if (liquidity < 2e3) {
2088
+ warnings.push(`Very low liquidity: $${liquidity.toFixed(0)}`);
2089
+ riskScore += 20;
2090
+ } else if (liquidity < 5e3) {
2091
+ warnings.push(`Low liquidity: $${liquidity.toFixed(0)}`);
2092
+ riskScore += 10;
2093
+ }
2094
+ } else {
2095
+ warnings.push("No liquidity data available");
2096
+ riskScore += 15;
2097
+ }
2098
+ if (marketCap > 0 && tokenData.currentPrice && tokenData.currentPrice > 0) {
2099
+ if (marketCap > 1e6 && liquidity < 1e4) {
2100
+ warnings.push(
2101
+ `Suspicious MC/Liquidity: MC $${marketCap.toFixed(0)} vs Liquidity $${liquidity.toFixed(0)}`
2102
+ );
2103
+ riskScore += 25;
2104
+ }
2105
+ }
2106
+ if (tokenData.priceHistory && tokenData.priceHistory.length >= 3) {
2107
+ const prices = tokenData.priceHistory.map((p) => p.price);
2108
+ const priceChanges = [];
2109
+ for (let i = 1; i < prices.length; i++) {
2110
+ if (prices[i - 1] > 0) {
2111
+ const change = (prices[i] - prices[i - 1]) / prices[i - 1] * 100;
2112
+ priceChanges.push(Math.abs(change));
2113
+ }
2114
+ }
2115
+ if (priceChanges.length > 0) {
2116
+ const avgVolatility = priceChanges.reduce((sum, change) => sum + change, 0) / priceChanges.length;
2117
+ const maxChange = Math.max(...priceChanges);
2118
+ if (maxChange > 200) {
2119
+ warnings.push(
2120
+ `Extreme volatility: ${maxChange.toFixed(1)}% max change`
2121
+ );
2122
+ riskScore += 20;
2123
+ } else if (avgVolatility > 50) {
2124
+ warnings.push(
2125
+ `High volatility: ${avgVolatility.toFixed(1)}% average change`
2126
+ );
2127
+ riskScore += 10;
2128
+ }
2129
+ }
2130
+ }
2131
+ const tokenAge = Date.now() - recommendationTimestamp;
2132
+ const ageInHours = tokenAge / (1e3 * 60 * 60);
2133
+ if (ageInHours < 24) {
2134
+ warnings.push(`Very new token: ${ageInHours.toFixed(1)} hours old`);
2135
+ riskScore += 15;
2136
+ } else if (ageInHours < 72) {
2137
+ warnings.push(`New token: ${ageInHours.toFixed(1)} hours old`);
2138
+ riskScore += 8;
2139
+ }
2140
+ const isLikelyRug = riskScore >= 50;
2141
+ if (warnings.length > 0) {
2142
+ const logLevel = isLikelyRug ? "warn" : "debug";
2143
+ logger[logLevel](
2144
+ `[CommunityInvestorService] Token ${tokenData.symbol} risk analysis (Score: ${riskScore}/100): ${warnings.join("; ")}`
2145
+ );
2146
+ }
2147
+ if (isLikelyRug) {
2148
+ logger.warn(
2149
+ `[CommunityInvestorService] Token ${tokenData.symbol} classified as likely scam/rug (Risk Score: ${riskScore}/100)`
2150
+ );
2151
+ }
2152
+ return isLikelyRug;
2153
+ }
2154
+ async evaluateRecommendationPerformance(recommendation, tokenData) {
2155
+ logger.debug(
2156
+ `[CommunityInvestorService] Evaluating performance for rec ID: ${recommendation.id}`
2157
+ );
2158
+ const metric = {
2159
+ evaluationTimestamp: Date.now(),
2160
+ isScamOrRug: await this.isLikelyScamOrRug(
2161
+ tokenData,
2162
+ recommendation.timestamp
2163
+ ),
2164
+ notes: ""
2165
+ };
2166
+ const priceAtRec = recommendation.priceAtRecommendation || tokenData.priceHistory?.find(
2167
+ (p) => p.timestamp >= recommendation.timestamp
2168
+ )?.price || tokenData.currentPrice || 0;
2169
+ const pricesAfterRec = tokenData.priceHistory?.filter(
2170
+ (p) => p.timestamp > recommendation.timestamp
2171
+ ) || [];
2172
+ if (metric.isScamOrRug) {
2173
+ if (recommendation.recommendationType === "BUY") {
2174
+ metric.potentialProfitPercent = -99;
2175
+ metric.notes = "Token identified as likely scam/rug pull after BUY recommendation.";
2176
+ }
2177
+ if (recommendation.recommendationType === "SELL") {
2178
+ metric.avoidedLossPercent = 99;
2179
+ metric.notes = "Criticism/SELL recommendation was correct; token identified as likely scam/rug pull.";
2180
+ }
2181
+ logger.debug(
2182
+ `[CommunityInvestorService] Rec ${recommendation.id} (Scam/Rug): Performance ${metric.potentialProfitPercent || metric.avoidedLossPercent}%`
2183
+ );
2184
+ return metric;
2185
+ }
2186
+ if (pricesAfterRec.length === 0) {
2187
+ metric.notes = "No significant price data available after recommendation time to evaluate performance yet.";
2188
+ if (tokenData.currentPrice && tokenData.currentPrice !== priceAtRec && priceAtRec > 0) {
2189
+ const currentPerformance = (tokenData.currentPrice - priceAtRec) / priceAtRec * 100;
2190
+ if (recommendation.recommendationType === "BUY")
2191
+ metric.potentialProfitPercent = currentPerformance;
2192
+ if (recommendation.recommendationType === "SELL")
2193
+ metric.avoidedLossPercent = -currentPerformance;
2194
+ metric.notes = "Evaluated based on current price vs price at recommendation.";
2195
+ } else if (priceAtRec === 0 && tokenData.currentPrice && tokenData.currentPrice > 0 && recommendation.recommendationType === "BUY") {
2196
+ metric.potentialProfitPercent = Infinity;
2197
+ metric.notes = "Token acquired at effectively zero cost and now has value.";
2198
+ } else if (priceAtRec > 0 && tokenData.currentPrice === 0 && recommendation.recommendationType === "SELL") {
2199
+ metric.avoidedLossPercent = 100;
2200
+ metric.notes = "Token value went to zero after sell recommendation.";
2201
+ }
2202
+ logger.debug(
2203
+ `[CommunityInvestorService] Rec ${recommendation.id} (No prices after): Performance ${metric.potentialProfitPercent || metric.avoidedLossPercent}%`
2204
+ );
2205
+ return metric;
2206
+ }
2207
+ const peakPriceAfterRec = Math.max(
2208
+ ...pricesAfterRec.map((p) => p.price),
2209
+ priceAtRec
2210
+ );
2211
+ const troughPriceAfterRec = Math.min(
2212
+ ...pricesAfterRec.map((p) => p.price),
2213
+ priceAtRec
2214
+ );
2215
+ if (recommendation.recommendationType === "BUY") {
2216
+ if (priceAtRec > 0) {
2217
+ if (peakPriceAfterRec > priceAtRec) {
2218
+ metric.potentialProfitPercent = (peakPriceAfterRec - priceAtRec) / priceAtRec * 100;
2219
+ metric.notes = `Potential profit to peak of $${peakPriceAfterRec.toFixed(4)} from $${priceAtRec.toFixed(4)}.`;
2220
+ } else {
2221
+ const lossPrice = Math.min(
2222
+ tokenData.currentPrice || 0,
2223
+ troughPriceAfterRec
2224
+ );
2225
+ metric.potentialProfitPercent = (lossPrice - priceAtRec) / priceAtRec * 100;
2226
+ metric.notes = `No profitable exit; current/trough price $${lossPrice.toFixed(4)} vs buy $${priceAtRec.toFixed(4)}.`;
2227
+ }
2228
+ } else {
2229
+ metric.potentialProfitPercent = peakPriceAfterRec > 0 ? Infinity : 0;
2230
+ metric.notes = `Bought at effectively zero, peak price $${peakPriceAfterRec.toFixed(4)}.`;
2231
+ }
2232
+ } else if (recommendation.recommendationType === "SELL") {
2233
+ if (priceAtRec > 0) {
2234
+ if (troughPriceAfterRec < priceAtRec) {
2235
+ metric.avoidedLossPercent = (priceAtRec - troughPriceAfterRec) / priceAtRec * 100;
2236
+ metric.notes = `Avoided loss as price dropped to $${troughPriceAfterRec.toFixed(4)} from $${priceAtRec.toFixed(4)}.`;
2237
+ } else {
2238
+ const missedProfitPrice = Math.max(
2239
+ tokenData.currentPrice || 0,
2240
+ peakPriceAfterRec
2241
+ );
2242
+ metric.avoidedLossPercent = (priceAtRec - missedProfitPrice) / priceAtRec * 100;
2243
+ metric.notes = `Missed potential gains; price rose/stayed above $${priceAtRec.toFixed(4)}, reaching $${missedProfitPrice.toFixed(4)}.`;
2244
+ }
2245
+ } else {
2246
+ metric.avoidedLossPercent = 0;
2247
+ metric.notes = `Token was at zero or near-zero at time of sell/criticism.`;
2248
+ }
2249
+ }
2250
+ logger.debug(
2251
+ `[CommunityInvestorService] Rec ${recommendation.id}: Performance ${metric.potentialProfitPercent || metric.avoidedLossPercent}%`
2252
+ );
2253
+ return metric;
2254
+ }
2255
+ getRecencyWeight(recommendationTimestamp) {
2256
+ const now = Date.now();
2257
+ const ageInMilliseconds = now - recommendationTimestamp;
2258
+ const ageInMonths = ageInMilliseconds / (1e3 * 60 * 60 * 24 * 30.44);
2259
+ if (ageInMonths > this.RECENCY_WEIGHT_MONTHS) return 0.1;
2260
+ return Math.max(0.1, 1 - ageInMonths / this.RECENCY_WEIGHT_MONTHS * 0.9);
2261
+ }
2262
+ getConvictionWeight(conviction) {
2263
+ switch (conviction) {
2264
+ case "HIGH":
2265
+ return 1.5;
2266
+ case "MEDIUM":
2267
+ return 1;
2268
+ case "LOW":
2269
+ return 0.5;
2270
+ default:
2271
+ return 0.25;
2272
+ }
2273
+ }
2274
+ async calculateUserTrustScore(userId, runtime, _worldId) {
2275
+ logger.info(
2276
+ `[CommunityInvestorService] Starting calculateUserTrustScore for user ${userId} (components in world/room: ${this.componentWorldId})`
2277
+ );
2278
+ const componentResult = await runtime.getComponent(
2279
+ userId,
2280
+ TRUST_MARKETPLACE_COMPONENT_TYPE,
2281
+ this.componentWorldId,
2282
+ runtime.agentId
2283
+ );
2284
+ if (!componentResult) {
2285
+ const newProfile = {
2286
+ version: "1.0.0",
2287
+ userId,
2288
+ trustScore: 0,
2289
+ lastTrustScoreCalculationTimestamp: Date.now(),
2290
+ recommendations: []
2291
+ };
2292
+ await runtime.createComponent({
2293
+ id: userId,
2294
+ // Use userId as component ID
2295
+ entityId: userId,
2296
+ agentId: runtime.agentId,
2297
+ worldId: this.componentWorldId,
2298
+ roomId: this.componentRoomId,
2299
+ sourceEntityId: runtime.agentId,
2300
+ type: TRUST_MARKETPLACE_COMPONENT_TYPE,
2301
+ createdAt: Date.now(),
2302
+ data: newProfile
2303
+ });
2304
+ this.registerUser(userId);
2305
+ logger.info(
2306
+ `[CommunityInvestorService] User ${userId} trust score is now: 0.00. Profile marked for update.`
2307
+ );
2308
+ return 0;
2309
+ }
2310
+ const userProfile = componentResult.data;
2311
+ if (!Array.isArray(userProfile.recommendations)) {
2312
+ logger.warn(
2313
+ `[calculateUserTrustScore] User ${userId} profile recommendations was not an array. Initializing.`
2314
+ );
2315
+ userProfile.recommendations = [];
2316
+ }
2317
+ let _metricsUpdated = false;
2318
+ for (const rec of userProfile.recommendations) {
2319
+ if (!rec.tokenAddress || !rec.chain) {
2320
+ logger.warn(
2321
+ `[calculateUserTrustScore] Rec ${rec.id} for user ${userId} missing address/chain. Skipping metric evaluation.`
2322
+ );
2323
+ continue;
2324
+ }
2325
+ const needsReEval = !rec.metrics?.evaluationTimestamp || Date.now() - rec.metrics.evaluationTimestamp > this.METRIC_REFRESH_INTERVAL;
2326
+ if (needsReEval) {
2327
+ try {
2328
+ const tokenData = await this.getTokenAPIData(
2329
+ rec.tokenAddress,
2330
+ rec.chain
2331
+ );
2332
+ if (!tokenData) {
2333
+ logger.warn(
2334
+ `[calculateUserTrustScore] No token data for ${rec.tokenAddress} (rec ${rec.id}, user ${userId}) to update metrics.`
2335
+ );
2336
+ continue;
2337
+ }
2338
+ const newMetric = await this.evaluateRecommendationPerformance(
2339
+ rec,
2340
+ tokenData
2341
+ );
2342
+ rec.metrics = newMetric;
2343
+ _metricsUpdated = true;
2344
+ logger.debug(
2345
+ `[calculateUserTrustScore] Updated metrics for rec ${rec.id}, user ${userId}: ${JSON.stringify(newMetric)}`
2346
+ );
2347
+ } catch (error) {
2348
+ logger.error(
2349
+ `[calculateUserTrustScore] Error updating metrics for rec ${rec.id}, user ${userId}:`,
2350
+ error
2351
+ );
2352
+ }
2353
+ } else {
2354
+ logger.debug(
2355
+ `[calculateUserTrustScore] Rec ${rec.id} for user ${userId} has fresh metrics, skipping re-evaluation.`
2356
+ );
2357
+ }
2358
+ if (!rec.metrics) {
2359
+ logger.warn(
2360
+ `[calculateUserTrustScore] Rec ${rec.id} for user ${userId} still has no metrics. It will not contribute to score.`
2361
+ );
2362
+ }
2363
+ }
2364
+ const { trustScore: updatedScore } = this.calculateNewScoreFromProfile(userProfile);
2365
+ userProfile.trustScore = updatedScore;
2366
+ userProfile.lastTrustScoreCalculationTimestamp = Date.now();
2367
+ await runtime.updateComponent({
2368
+ ...componentResult,
2369
+ data: userProfile
2370
+ });
2371
+ this.registerUser(userId);
2372
+ logger.info(
2373
+ `[CommunityInvestorService] User ${userId} trust score is now: ${updatedScore.toFixed(2)}. Profile updated.`
2374
+ );
2375
+ return updatedScore;
2376
+ }
2377
+ /**
2378
+ * Calculate trust score from user profile recommendations
2379
+ */
2380
+ calculateNewScoreFromProfile(userProfile) {
2381
+ const recommendations = userProfile.recommendations || [];
2382
+ if (recommendations.length === 0) {
2383
+ return { trustScore: 0 };
2384
+ }
2385
+ const aggregatedMetrics = {
2386
+ totalCalls: recommendations.length,
2387
+ profitableCalls: 0,
2388
+ totalProfit: 0,
2389
+ totalWeightedProfit: 0,
2390
+ totalWeight: 0,
2391
+ profits: [],
2392
+ rugPromotions: 0,
2393
+ goodCalls: 0
2394
+ };
2395
+ for (const rec of recommendations) {
2396
+ if (!rec.metrics) continue;
2397
+ let performance = 0;
2398
+ const potentialProfit = rec.metrics.potentialProfitPercent || 0;
2399
+ const isLikelyRug = rec.metrics.isScamOrRug || potentialProfit <= -80;
2400
+ if (isLikelyRug) {
2401
+ if (rec.recommendationType === "BUY") {
2402
+ aggregatedMetrics.rugPromotions++;
2403
+ performance = -100;
2404
+ } else if (rec.recommendationType === "SELL") {
2405
+ aggregatedMetrics.goodCalls++;
2406
+ performance = rec.metrics.avoidedLossPercent || 50;
2407
+ }
2408
+ } else if (rec.recommendationType === "BUY") {
2409
+ performance = potentialProfit;
2410
+ if (performance > 20) {
2411
+ aggregatedMetrics.goodCalls++;
2412
+ }
2413
+ } else if (rec.recommendationType === "SELL") {
2414
+ performance = rec.metrics.avoidedLossPercent || 0;
2415
+ if (potentialProfit < -30) {
2416
+ aggregatedMetrics.goodCalls++;
2417
+ }
2418
+ }
2419
+ const recencyWeight = this.getRecencyWeight(rec.timestamp);
2420
+ const convictionWeight = this.getConvictionWeight(rec.conviction);
2421
+ const totalRecWeight = recencyWeight * convictionWeight;
2422
+ aggregatedMetrics.totalWeightedProfit += performance * totalRecWeight;
2423
+ aggregatedMetrics.totalWeight += totalRecWeight;
2424
+ aggregatedMetrics.totalProfit += performance;
2425
+ aggregatedMetrics.profits.push(performance);
2426
+ if (performance > 0) {
2427
+ aggregatedMetrics.profitableCalls++;
2428
+ }
2429
+ }
2430
+ const winRate = aggregatedMetrics.profitableCalls / aggregatedMetrics.totalCalls;
2431
+ const averageProfit = aggregatedMetrics.totalProfit / aggregatedMetrics.totalCalls;
2432
+ const profitMean = averageProfit;
2433
+ const variance = aggregatedMetrics.profits.reduce(
2434
+ (sum, p) => sum + (p - profitMean) ** 2,
2435
+ 0
2436
+ ) / aggregatedMetrics.profits.length;
2437
+ const stdDev = Math.sqrt(variance);
2438
+ const consistency = stdDev > 0 ? Math.max(0, 1 - stdDev / 100) : 1;
2439
+ const sharpeRatio = stdDev > 0 ? averageProfit / stdDev : 0;
2440
+ const marketAverage = 0;
2441
+ const alpha = averageProfit - marketAverage;
2442
+ const metrics = {
2443
+ totalCalls: aggregatedMetrics.totalCalls,
2444
+ profitableCalls: aggregatedMetrics.profitableCalls,
2445
+ averageProfit,
2446
+ winRate,
2447
+ sharpeRatio,
2448
+ alpha,
2449
+ volumePenalty: 0,
2450
+ // Not used in balanced calculator
2451
+ consistency
2452
+ };
2453
+ let archetype = "newbie";
2454
+ if (winRate > 0.7 && averageProfit > 30) {
2455
+ archetype = "elite_analyst";
2456
+ } else if (winRate > 0.6 && averageProfit > 15) {
2457
+ archetype = "skilled_trader";
2458
+ } else if (winRate > 0.5) {
2459
+ archetype = "technical_analyst";
2460
+ } else if (aggregatedMetrics.rugPromotions > aggregatedMetrics.totalCalls * 0.5) {
2461
+ archetype = "rug_promoter";
2462
+ } else if (aggregatedMetrics.totalCalls > 50 && winRate < 0.3) {
2463
+ archetype = "bot_spammer";
2464
+ }
2465
+ const trustScore = this.balancedTrustCalculator.calculateBalancedTrustScore(
2466
+ metrics,
2467
+ archetype,
2468
+ aggregatedMetrics.rugPromotions,
2469
+ aggregatedMetrics.goodCalls,
2470
+ aggregatedMetrics.totalCalls
2471
+ );
2472
+ logger.debug(
2473
+ `[calculateNewScoreFromProfile] User ${userProfile.userId}: archetype=${archetype}, winRate=${(winRate * 100).toFixed(1)}%, avgProfit=${averageProfit.toFixed(1)}%, trustScore=${trustScore.toFixed(1)}`
2474
+ );
2475
+ return { trustScore };
2476
+ }
2477
+ // --- Task Worker Execution --- (Could be in a separate tasks.ts file)
2478
+ async executeProcessTradeDecision(options, task) {
2479
+ logger.info(
2480
+ `[CommunityInvestorService] Task Worker: Processing rec: ${options.recommendationId}, user: ${options.userId}`
2481
+ );
2482
+ const { recommendationId, userId } = options;
2483
+ const runtime = this.runtime;
2484
+ const userProfileWorldId = runtime.agentId;
2485
+ const componentResult = await runtime.getComponent(
2486
+ userId,
2487
+ TRUST_MARKETPLACE_COMPONENT_TYPE,
2488
+ userProfileWorldId,
2489
+ runtime.agentId
2490
+ );
2491
+ if (!componentResult?.data) {
2492
+ logger.error(
2493
+ `Task Worker: UserProfile component not found for user ${userId}. Deleting task.`
2494
+ );
2495
+ await runtime.deleteTask(task.id);
2496
+ return;
2497
+ }
2498
+ const userProfile = componentResult.data;
2499
+ let recommendation = userProfile.recommendations.find(
2500
+ (r) => r.id === recommendationId
2501
+ );
2502
+ if (!recommendation) {
2503
+ logger.error(
2504
+ `Task Worker: Rec ${recommendationId} not found in profile for user ${userId}. Deleting task.`
2505
+ );
2506
+ await runtime.deleteTask(task.id);
2507
+ return;
2508
+ }
2509
+ if (recommendation.processedForTradeDecision && !(userProfile.lastTradeDecisionMadeTimestamp && Date.now() - userProfile.lastTradeDecisionMadeTimestamp < this.USER_TRADE_COOLDOWN_HOURS * 36e5)) {
2510
+ logger.info(
2511
+ `Task Worker: Rec ${recommendationId} already fully processed & not in cooldown. Deleting task.`
2512
+ );
2513
+ await runtime.deleteTask(task.id);
2514
+ return;
2515
+ }
2516
+ await this.calculateUserTrustScore(userId, runtime);
2517
+ const updatedComponent = await runtime.getComponent(
2518
+ userId,
2519
+ TRUST_MARKETPLACE_COMPONENT_TYPE,
2520
+ userProfileWorldId,
2521
+ runtime.agentId
2522
+ );
2523
+ if (!updatedComponent?.data) {
2524
+ logger.error(
2525
+ `Task Worker: Profile for ${userId} disappeared after score recalc. Deleting task.`
2526
+ );
2527
+ await runtime.deleteTask(task.id);
2528
+ return;
2529
+ }
2530
+ const updatedUserProfile = updatedComponent.data;
2531
+ const finalTrustScore = updatedUserProfile.trustScore;
2532
+ recommendation = updatedUserProfile.recommendations.find(
2533
+ (r) => r.id === recommendationId
2534
+ ) || recommendation;
2535
+ const now = Date.now();
2536
+ if (updatedUserProfile.lastTradeDecisionMadeTimestamp && now - updatedUserProfile.lastTradeDecisionMadeTimestamp < this.USER_TRADE_COOLDOWN_HOURS * 36e5) {
2537
+ logger.info(
2538
+ `Task Worker: User ${userId} on trade cooldown (post-score update). Holding on rec ${recommendationId}.`
2539
+ );
2540
+ if (recommendation) {
2541
+ recommendation.processedForTradeDecision = false;
2542
+ } else {
2543
+ logger.error(
2544
+ "Task Worker: Rec null after profile refresh in cooldown check."
2545
+ );
2546
+ }
2547
+ await runtime.updateComponent({
2548
+ ...updatedComponent,
2549
+ data: updatedUserProfile
2550
+ });
2551
+ await runtime.deleteTask(task.id);
2552
+ return;
2553
+ }
2554
+ let decisionMade = false;
2555
+ if (recommendation.recommendationType === "BUY") {
2556
+ if (finalTrustScore > this.POSITIVE_TRADE_THRESHOLD) {
2557
+ logger.info(
2558
+ `Task Worker: SIMULATING BUY for rec ${recommendationId}. User ${userId}, Score: ${finalTrustScore.toFixed(2)}`
2559
+ );
2560
+ updatedUserProfile.lastTradeDecisionMadeTimestamp = now;
2561
+ decisionMade = true;
2562
+ } else {
2563
+ logger.info(
2564
+ `Task Worker: HOLDING on BUY rec ${recommendationId}. User ${userId}, Score: ${finalTrustScore.toFixed(2)}, Threshold: >${this.POSITIVE_TRADE_THRESHOLD})`
2565
+ );
2566
+ }
2567
+ } else {
2568
+ if (finalTrustScore > this.POSITIVE_TRADE_THRESHOLD) {
2569
+ logger.info(
2570
+ `Task Worker: ACKNOWLEDGING VALID SELL/CRITICISM for rec ${recommendationId}. User ${userId}, Score: ${finalTrustScore.toFixed(2)}`
2571
+ );
2572
+ updatedUserProfile.lastTradeDecisionMadeTimestamp = now;
2573
+ decisionMade = true;
2574
+ } else if (finalTrustScore < -this.NEUTRAL_MARGIN) {
2575
+ logger.info(
2576
+ `Task Worker: IGNORING POTENTIAL FUD SELL/CRITICISM for rec ${recommendationId}. User ${userId}, Score: ${finalTrustScore.toFixed(2)} (Threshold for FUD: <${-this.NEUTRAL_MARGIN})`
2577
+ );
2578
+ } else {
2579
+ logger.info(
2580
+ `Task Worker: NOTING SELL/CRITICISM for rec ${recommendationId}. User ${userId}, Score: ${finalTrustScore.toFixed(2)}`
2581
+ );
2582
+ }
2583
+ }
2584
+ const recToUpdate = updatedUserProfile.recommendations.find(
2585
+ (r) => r.id === recommendationId
2586
+ );
2587
+ if (recToUpdate) {
2588
+ recToUpdate.processedForTradeDecision = true;
2589
+ } else {
2590
+ logger.error(
2591
+ `[CommunityInvestorService] Task Worker: Could not find rec ${recommendationId} in updated profile to mark as processed.`
2592
+ );
2593
+ }
2594
+ await runtime.updateComponent({
2595
+ ...updatedComponent,
2596
+ data: updatedUserProfile
2597
+ });
2598
+ await runtime.deleteTask(task.id);
2599
+ logger.info(
2600
+ `Task Worker: Finished trade decision for rec ${recommendationId}. User: ${userId}. Made Sim Trade: ${decisionMade}`
2601
+ );
2602
+ }
2603
+ registerTaskWorkers(runtime) {
2604
+ runtime.registerTaskWorker({
2605
+ name: "PROCESS_TRADE_DECISION",
2606
+ execute: async (_runtime, options, task) => {
2607
+ await this.executeProcessTradeDecision(
2608
+ options,
2609
+ task
2610
+ );
2611
+ return void 0;
2612
+ }
2613
+ });
2614
+ logger.info(
2615
+ "[CommunityInvestorService] Registered PROCESS_TRADE_DECISION task worker."
2616
+ );
2617
+ }
2618
+ async getLeaderboardData(runtime) {
2619
+ logger.info("[CommunityInvestorService] getLeaderboardData called");
2620
+ const leaderboardEntries = [];
2621
+ const worldIdForComponents = this.componentWorldId;
2622
+ logger.info(
2623
+ `[CommunityInvestorService] Preparing leaderboard from world ${worldIdForComponents}. Checking ${this.userRegistry.size} registered users from userRegistry: [${Array.from(this.userRegistry).join(", ")}]`
2624
+ );
2625
+ for (const userId of this.userRegistry) {
2626
+ logger.debug(
2627
+ `[CommunityInvestorService] Leaderboard: Processing registered user ${userId} from world ${worldIdForComponents}`
2628
+ );
2629
+ try {
2630
+ const component = await runtime.getComponent(
2631
+ userId,
2632
+ TRUST_MARKETPLACE_COMPONENT_TYPE,
2633
+ worldIdForComponents,
2634
+ // Use consistent worldId
2635
+ runtime.agentId
2636
+ );
2637
+ if (component?.data) {
2638
+ const profileData = component.data;
2639
+ const entityDetails = await runtime.getEntityById(component.entityId);
2640
+ const recommendations = Array.isArray(profileData.recommendations) ? profileData.recommendations : [];
2641
+ leaderboardEntries.push({
2642
+ userId: component.entityId,
2643
+ username: entityDetails?.names?.[0] || component.entityId.toString(),
2644
+ trustScore: profileData.trustScore || 0,
2645
+ recommendations
2646
+ });
2647
+ logger.debug(
2648
+ `[CommunityInvestorService] Added user ${userId} to leaderboard with score ${profileData.trustScore}`
2649
+ );
2650
+ } else {
2651
+ logger.debug(
2652
+ `[CommunityInvestorService] Leaderboard: No profile component found for registered user ${userId}`
2653
+ );
2654
+ }
2655
+ } catch (error) {
2656
+ logger.error(
2657
+ `[CommunityInvestorService] Leaderboard: Error fetching profile component for user ${userId}:`,
2658
+ error
2659
+ );
2660
+ }
2661
+ }
2662
+ logger.info(
2663
+ `[CommunityInvestorService] Leaderboard: Found ${leaderboardEntries.length} users with profiles to include.`
2664
+ );
2665
+ leaderboardEntries.sort((a, b) => b.trustScore - a.trustScore);
2666
+ const rankedLeaderboard = leaderboardEntries.map((entry, index) => ({
2667
+ ...entry,
2668
+ rank: index + 1
2669
+ }));
2670
+ logger.info(
2671
+ `[CommunityInvestorService] Leaderboard generated with ${rankedLeaderboard.length} entries.`
2672
+ );
2673
+ return rankedLeaderboard;
2674
+ }
2675
+ // Add this method to register a user when they make a recommendation
2676
+ registerUser(userId) {
2677
+ const originalSize = this.userRegistry.size;
2678
+ this.userRegistry.add(userId);
2679
+ if (this.userRegistry.size > originalSize) {
2680
+ logger.info(
2681
+ `[CommunityInvestorService] User ${userId} ADDED to registry. New size: ${this.userRegistry.size}. Registry now: [${Array.from(this.userRegistry).join(", ")}]`
2682
+ );
2683
+ } else {
2684
+ logger.debug(
2685
+ `[CommunityInvestorService] User ${userId} already in registry. Size: ${this.userRegistry.size}`
2686
+ );
2687
+ }
2688
+ const registryCacheKey = `community-investor:user-registry:${this.componentWorldId}`;
2689
+ this.runtime.setCache(registryCacheKey, Array.from(this.userRegistry)).then(
2690
+ () => logger.debug(
2691
+ `[CommunityInvestorService] User registry cache updated for user ${userId} at key ${registryCacheKey}.`
2692
+ )
2693
+ ).catch(
2694
+ (err) => logger.error(
2695
+ `[CommunityInvestorService] FAILED to update user registry cache for ${userId} at key ${registryCacheKey}:`,
2696
+ err
2697
+ )
2698
+ );
2699
+ }
2700
+ // Load user registry on initialization
2701
+ async loadUserRegistry() {
2702
+ const registryCacheKey = `community-investor:user-registry:${this.componentWorldId}`;
2703
+ try {
2704
+ const cached = await this.runtime.getCache(registryCacheKey);
2705
+ if (cached && Array.isArray(cached)) {
2706
+ this.userRegistry = new Set(cached);
2707
+ logger.info(
2708
+ `[CommunityInvestorService] Loaded ${this.userRegistry.size} users from registry cache at key ${registryCacheKey}. Users: [${Array.from(this.userRegistry).join(", ")}]`
2709
+ );
2710
+ } else {
2711
+ logger.info(
2712
+ `[CommunityInvestorService] No user registry found in cache at key ${registryCacheKey}, starting fresh.`
2713
+ );
2714
+ }
2715
+ } catch (error) {
2716
+ logger.warn(
2717
+ `[CommunityInvestorService] Failed to load user registry from cache at key ${registryCacheKey}:`,
2718
+ error
2719
+ );
2720
+ }
2721
+ }
2722
+ async ensurePluginComponentContext() {
2723
+ try {
2724
+ await this.runtime.ensureWorldExists({
2725
+ id: this.componentWorldId,
2726
+ name: `Social Alpha Global World (Agent: ${this.runtime.agentId})`,
2727
+ agentId: this.runtime.agentId,
2728
+ metadata: {
2729
+ plugin_managed: true,
2730
+ description: "World context for CommunityInvestor plugin components"
2731
+ }
2732
+ });
2733
+ logger.info(
2734
+ `[CommunityInvestorService] Ensured plugin component world ${this.componentWorldId} exists.`
2735
+ );
2736
+ await this.runtime.ensureRoomExists({
2737
+ id: this.componentRoomId,
2738
+ name: `Social Alpha Global Room (Agent: ${this.runtime.agentId})`,
2739
+ worldId: this.componentWorldId,
2740
+ agentId: this.runtime.agentId,
2741
+ channelId: TRUST_LEADERBOARD_WORLD_SEED,
2742
+ source: "plugin_internal",
2743
+ type: ChannelType.API,
2744
+ // Use API as fallback channel type
2745
+ metadata: {
2746
+ plugin_managed: true,
2747
+ description: "Room context for CommunityInvestor plugin components"
2748
+ }
2749
+ });
2750
+ logger.info(
2751
+ `[CommunityInvestorService] Ensured plugin component room ${this.componentRoomId} in world ${this.componentWorldId} exists.`
2752
+ );
2753
+ } catch (error) {
2754
+ logger.error(
2755
+ `[CommunityInvestorService] FAILED to ensure plugin component world/room context (ID: ${this.componentWorldId}):`,
2756
+ error
2757
+ );
2758
+ }
2759
+ }
2760
+ // ===================== NEW CORE PROCESSING LOGIC =====================
2761
+ /**
2762
+ * Processes a batch of historical messages, intended to be called from a script.
2763
+ */
2764
+ async processHistoricalData(batch) {
2765
+ logger.info(
2766
+ `[Service] Processing historical batch ${batch.fileId}_${batch.batchIndex}`
2767
+ );
2768
+ const { messages, userMap } = batch;
2769
+ const contextText = "";
2770
+ const messagesText = messages.map(
2771
+ (msg, idx) => `[${idx}] ${userMap[msg.uid] || msg.uid}: ${msg.content}`
2772
+ ).join("\n");
2773
+ const { systemPrompt, userPrompt } = this.buildExtractionPrompts(
2774
+ contextText,
2775
+ messagesText,
2776
+ messages.length,
2777
+ "Multiple Users"
2778
+ );
2779
+ try {
2780
+ const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
2781
+ prompt: `${systemPrompt}
2782
+ ${userPrompt}`
2783
+ });
2784
+ const parsed = parseRecommendationExtraction(response);
2785
+ if (!parsed?.recommendations || parsed.recommendations.length === 0) {
2786
+ logger.debug(
2787
+ `[Service] No recommendations extracted from historical batch ${batch.fileId}_${batch.batchIndex}.`
2788
+ );
2789
+ return [];
2790
+ }
2791
+ const callsByUserId = /* @__PURE__ */ new Map();
2792
+ for (const rec of parsed.recommendations) {
2793
+ const message = messages[rec.messageIndex];
2794
+ if (!message) continue;
2795
+ const userId = asUUID(createUniqueUuid(this.runtime, message.uid));
2796
+ if (!callsByUserId.has(userId)) {
2797
+ callsByUserId.set(userId, { messages: [], recommendations: [] });
2798
+ }
2799
+ callsByUserId.get(userId)?.messages.push(message);
2800
+ callsByUserId.get(userId)?.recommendations.push(rec);
2801
+ }
2802
+ for (const [userId, data] of callsByUserId.entries()) {
2803
+ await this.updateProfileWithRecommendations(
2804
+ userId,
2805
+ data.messages,
2806
+ data.recommendations
2807
+ );
2808
+ }
2809
+ return parsed.recommendations;
2810
+ } catch (error) {
2811
+ logger.error(
2812
+ `[Service] Error processing historical batch ${batch.fileId}_${batch.batchIndex}:`,
2813
+ error
2814
+ );
2815
+ return [];
2816
+ }
2817
+ }
2818
+ /**
2819
+ * Main entry point for processing a single, real-time message from the event handler.
2820
+ */
2821
+ async processIncomingMessage(message) {
2822
+ const { userId, roomId, text, timestamp, id, username } = message;
2823
+ const messageId = id || asUUID(createUniqueUuid(this.runtime, `${userId}-${timestamp}`));
2824
+ const recentMessages = await this.runtime.getMemories({
2825
+ tableName: "messages",
2826
+ roomId,
2827
+ count: 10,
2828
+ // Fetch recent messages for context
2829
+ unique: false
2830
+ });
2831
+ const contextText = recentMessages.map(
2832
+ (msg) => `${msg.content?.name || msg.entityId.toString()}: ${msg.content?.text || ""}`
2833
+ ).join("\n");
2834
+ const messagesText = `[0] ${username || userId}: ${text}`;
2835
+ const { systemPrompt, userPrompt } = this.buildExtractionPrompts(
2836
+ contextText,
2837
+ messagesText,
2838
+ 1,
2839
+ username || userId.toString()
2840
+ );
2841
+ try {
2842
+ const response = await this.runtime.useModel(ModelType.TEXT_LARGE, {
2843
+ prompt: `${systemPrompt}
2844
+ ${userPrompt}`
2845
+ });
2846
+ const parsed = parseRecommendationExtraction(response);
2847
+ if (!parsed?.recommendations || parsed.recommendations.length === 0) {
2848
+ logger.debug(
2849
+ `[Service] No recommendations extracted from message ${messageId}.`
2850
+ );
2851
+ return;
2852
+ }
2853
+ const messageForUpdate = {
2854
+ id: messageId,
2855
+ content: text,
2856
+ uid: userId,
2857
+ ts: new Date(timestamp).toISOString()
2858
+ };
2859
+ await this.updateProfileWithRecommendations(
2860
+ userId,
2861
+ [messageForUpdate],
2862
+ parsed.recommendations
2863
+ );
2864
+ } catch (e) {
2865
+ logger.error(
2866
+ `[Service] Error processing incoming message for user ${userId}:`,
2867
+ e
2868
+ );
2869
+ }
2870
+ }
2871
+ /**
2872
+ * Builds the system and user prompts for the recommendation extraction LLM call.
2873
+ */
2874
+ buildExtractionPrompts(contextText, messagesText, batchSize, _senderName) {
2875
+ const systemPrompt = `Extract crypto trading signals, calls, recommendations, and sentiment from Discord messages.
2876
+
2877
+ \u{1F3AF} WHAT COUNTS AS A TRADING SIGNAL:
2878
+ \u2022 Direct trading advice: "buy X", "sell Y", "hold Z"
2879
+ \u2022 Token mentions with $ symbol: $SOL, $PEPE, $DOGE, etc.
2880
+ \u2022 Contract addresses posted for token discovery
2881
+ \u2022 Price predictions: "X going to moon", "Y will dump"
2882
+ \u2022 Market sentiment: "bullish on X", "bearish on Y"
2883
+ \u2022 Technical analysis mentions
2884
+ \u2022 FUD or criticism about specific tokens/projects
2885
+ \u2022 Trading intent: "I'm buying X", "waiting for dip"
2886
+ \u2022 Token performance discussion
2887
+
2888
+ \u{1F6AB} WHAT TO EXCLUDE:
2889
+ \u2022 Rick bot automated messages (User ID: 1081815963990761542)
2890
+ \u2022 Generic DAO/protocol discussion without specific tokens
2891
+ \u2022 Users with "*bot" in username
2892
+ \u2022 Messages about "mintable" (it's a property, not a token)
2893
+ \u2022 General crypto news without specific token focus
2894
+
2895
+ \u{1F524} CRYPTO SLANG DICTIONARY:
2896
+ \u2022 fsh = full stack hitler (derogatory)
2897
+ \u2022 dca = dollar cost averaging
2898
+ \u2022 ath = all time high
2899
+ \u2022 atl = all time low
2900
+ \u2022 mcap = market cap
2901
+ \u2022 ser = sir
2902
+ \u2022 ngmi = not gonna make it
2903
+ \u2022 wagmi = we're all gonna make it
2904
+ \u2022 wen = when
2905
+ \u2022 gm = good morning
2906
+ \u2022 ser = sir
2907
+
2908
+ \u26A0\uFE0F CRITICAL OUTPUT REQUIREMENTS:
2909
+ - Respond with JSON only.
2910
+ - EXACTLY ${batchSize} recommendations entries (messageIndex 0 to ${batchSize - 1})
2911
+ - Every entry MUST include ALL required fields
2912
+
2913
+ REQUIRED JSON SHAPE:
2914
+ {"recommendations":[{"messageIndex":0,"isCall":true,"tokenMentioned":"SOL","nameMentioned":"","caMentioned":"","chain":"solana","sentiment":"positive","conviction":"medium","llmReasoning":"User mentioned buying $SOL with medium confidence"}]}
2915
+
2916
+ FIELD REQUIREMENTS:
2917
+ \u2022 messageIndex: 0 to ${batchSize - 1}
2918
+ \u2022 isCall: true/false
2919
+ \u2022 tokenMentioned: ticker without $ (or "" if none)
2920
+ \u2022 nameMentioned: full token name (or "" if none)
2921
+ \u2022 caMentioned: contract address (or "" if none)
2922
+ \u2022 chain: "solana", "ethereum", "bitcoin", "base", "unknown"
2923
+ \u2022 sentiment: "positive", "negative", "neutral"
2924
+ \u2022 conviction: "high", "medium", "low", "neutral"
2925
+ \u2022 llmReasoning: 1-2 sentence explanation
2926
+
2927
+ VALIDATION RULES:
2928
+ \u2022 If isCall=true: At least ONE of tokenMentioned, nameMentioned, or caMentioned must be non-empty
2929
+ \u2022 If isCall=false: ALL three can be empty
2930
+ \u2022 sentiment "bullish"\u2192"positive", "bearish"\u2192"negative"
2931
+ \u2022 Be VERY generous with extraction - include borderline cases`;
2932
+ const userPrompt = `
2933
+ RECENT CONTEXT:
2934
+ ${contextText}
2935
+
2936
+ \u{1F50D} ANALYZE THESE ${batchSize} MESSAGES FOR TRADING SIGNALS:
2937
+ ${messagesText}
2938
+
2939
+ \u{1F4CB} EXTRACTION RULES:
2940
+ 1. Look for contract addresses: long alphanumeric strings (32-44 chars for Solana, 0x+40 chars for ETH/Base)
2941
+ 2. Extract ANY token mentions: $BTC, $ETH, $SOL, $PEPE, etc.
2942
+ 3. Capture trading sentiment and conviction level
2943
+ 4. Include FUD, criticism, or warnings about tokens
2944
+ 5. Be generous - include subtle references
2945
+ 6. MUST return EXACTLY ${batchSize} results
2946
+
2947
+ Examples that SHOULD be extracted:
2948
+ - "the dev is a CA spammer so we dont know where it could go from here" \u2192 negative sentiment about a project
2949
+ - "Dqyrmg6y7QFhsbCgpkNwnp8wFMs81z3ToPACYAipump is this legit?" \u2192 contract address inquiry
2950
+ - "I will wait for retrace to enter" \u2192 trading intent
2951
+ - "$SOL looking good" \u2192 positive sentiment
2952
+ - "most ai stuff are getting a dump" \u2192 negative sentiment on AI tokens
2953
+ - Contract addresses without context \u2192 neutral discovery
2954
+
2955
+ RESPOND WITH JSON CONTAINING EXACTLY ${batchSize} RECOMMENDATION ENTRIES:`;
2956
+ return { systemPrompt, userPrompt };
2957
+ }
2958
+ /**
2959
+ * Updates a user's profile with new recommendations extracted from a message batch.
2960
+ */
2961
+ async updateProfileWithRecommendations(userId, messagesInBatch, recommendationsFromLlm) {
2962
+ const component = await this.runtime.getComponent(
2963
+ userId,
2964
+ TRUST_MARKETPLACE_COMPONENT_TYPE,
2965
+ this.componentWorldId,
2966
+ this.runtime.agentId
2967
+ );
2968
+ let userProfile;
2969
+ if (!component?.data) {
2970
+ userProfile = {
2971
+ version: "1.0.0",
2972
+ userId,
2973
+ trustScore: 0,
2974
+ lastTrustScoreCalculationTimestamp: Date.now(),
2975
+ recommendations: []
2976
+ };
2977
+ } else {
2978
+ userProfile = component.data;
2979
+ if (!Array.isArray(userProfile.recommendations))
2980
+ userProfile.recommendations = [];
2981
+ }
2982
+ let profileUpdated = false;
2983
+ for (const rec of recommendationsFromLlm) {
2984
+ if (!rec.isCall || !rec.sentiment || rec.sentiment === "neutral") {
2985
+ continue;
2986
+ }
2987
+ const sentiment = rec.sentiment;
2988
+ const tokenMentioned = rec.tokenMentioned?.trim() && rec.tokenMentioned !== "N/A" ? rec.tokenMentioned : void 0;
2989
+ const nameMentioned = rec.nameMentioned?.trim() ? rec.nameMentioned : void 0;
2990
+ const caMentioned = rec.caMentioned?.trim() ? rec.caMentioned : void 0;
2991
+ if (!tokenMentioned && !nameMentioned && !caMentioned) {
2992
+ continue;
2993
+ }
2994
+ const originalMessage = messagesInBatch[rec.messageIndex];
2995
+ if (!originalMessage) continue;
2996
+ let resolvedToken = null;
2997
+ if (caMentioned) {
2998
+ resolvedToken = {
2999
+ address: caMentioned,
3000
+ chain: rec.chain || SupportedChain.SOLANA,
3001
+ ticker: tokenMentioned || nameMentioned || caMentioned.slice(0, 8)
3002
+ };
3003
+ } else if (tokenMentioned) {
3004
+ resolvedToken = await this.resolveTicker(
3005
+ tokenMentioned,
3006
+ rec.chain || SupportedChain.SOLANA
3007
+ );
3008
+ } else if (nameMentioned) {
3009
+ resolvedToken = await this.resolveTicker(
3010
+ nameMentioned,
3011
+ rec.chain || SupportedChain.SOLANA
3012
+ );
3013
+ }
3014
+ if (!resolvedToken) {
3015
+ logger.warn(
3016
+ `[Service] Could not resolve token for: "${tokenMentioned || nameMentioned || caMentioned}". Skipping.`
3017
+ );
3018
+ continue;
3019
+ }
3020
+ const newRecommendation = {
3021
+ id: asUUID(uuidv4()),
3022
+ userId,
3023
+ messageId: originalMessage.id,
3024
+ timestamp: new Date(originalMessage.ts).getTime(),
3025
+ tokenTicker: resolvedToken.ticker,
3026
+ tokenAddress: resolvedToken.address,
3027
+ chain: resolvedToken.chain,
3028
+ recommendationType: sentiment === "positive" ? "BUY" : "SELL",
3029
+ conviction: rec.conviction,
3030
+ rawMessageQuote: originalMessage.content,
3031
+ priceAtRecommendation: 0,
3032
+ processedForTradeDecision: false
3033
+ };
3034
+ userProfile.recommendations.unshift(newRecommendation);
3035
+ profileUpdated = true;
3036
+ logger.info(
3037
+ `[Service] Added ${sentiment.toUpperCase()} recommendation for ${resolvedToken.ticker} from user ${userId}`
3038
+ );
3039
+ await this.runtime.createTask({
3040
+ name: "PROCESS_TRADE_DECISION",
3041
+ description: `Process trade decision for rec ${newRecommendation.id}`,
3042
+ metadata: { recommendationId: newRecommendation.id, userId },
3043
+ tags: ["socialAlpha", "tradeDecision"],
3044
+ roomId: this.componentRoomId,
3045
+ worldId: this.componentWorldId,
3046
+ entityId: userId
3047
+ });
3048
+ }
3049
+ if (profileUpdated) {
3050
+ if (component) {
3051
+ await this.runtime.updateComponent({
3052
+ ...component,
3053
+ data: userProfile
3054
+ });
3055
+ } else {
3056
+ const newComponentId = asUUID(
3057
+ createUniqueUuid(this.runtime, userId.toString())
3058
+ );
3059
+ await this.runtime.createComponent({
3060
+ id: newComponentId,
3061
+ entityId: userId,
3062
+ agentId: this.runtime.agentId,
3063
+ worldId: this.componentWorldId,
3064
+ roomId: this.componentRoomId,
3065
+ sourceEntityId: this.runtime.agentId,
3066
+ type: TRUST_MARKETPLACE_COMPONENT_TYPE,
3067
+ createdAt: Date.now(),
3068
+ data: userProfile
3069
+ });
3070
+ }
3071
+ await this.calculateUserTrustScore(userId, this.runtime);
3072
+ }
3073
+ }
3074
+ }
3075
+ export {
3076
+ CommunityInvestorService
3077
+ };
3078
+ //# sourceMappingURL=service.js.map