@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.
- package/dist/clients.d.ts +354 -0
- package/dist/clients.d.ts.map +1 -0
- package/dist/clients.js +670 -0
- package/dist/clients.js.map +1 -0
- package/dist/config.d.ts +144 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +122 -0
- package/dist/config.js.map +1 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +426 -0
- package/dist/events.js.map +1 -0
- package/dist/frontend/LeaderboardView.helpers.d.ts +6 -0
- package/dist/frontend/LeaderboardView.helpers.d.ts.map +1 -0
- package/dist/frontend/LeaderboardView.helpers.js +59 -0
- package/dist/frontend/LeaderboardView.helpers.js.map +1 -0
- package/dist/frontend/SocialAlphaSpatialView.d.ts +52 -0
- package/dist/frontend/SocialAlphaSpatialView.d.ts.map +1 -0
- package/dist/frontend/SocialAlphaSpatialView.js +72 -0
- package/dist/frontend/SocialAlphaSpatialView.js.map +1 -0
- package/dist/frontend/SocialAlphaView.d.ts +35 -0
- package/dist/frontend/SocialAlphaView.d.ts.map +1 -0
- package/dist/frontend/SocialAlphaView.js +125 -0
- package/dist/frontend/SocialAlphaView.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/mockPriceService.d.ts +22 -0
- package/dist/mockPriceService.d.ts.map +1 -0
- package/dist/mockPriceService.js +21 -0
- package/dist/mockPriceService.js.map +1 -0
- package/dist/providers/socialAlphaProvider.d.ts +15 -0
- package/dist/providers/socialAlphaProvider.d.ts.map +1 -0
- package/dist/providers/socialAlphaProvider.js +261 -0
- package/dist/providers/socialAlphaProvider.js.map +1 -0
- package/dist/register-terminal-view.d.ts +15 -0
- package/dist/register-terminal-view.d.ts.map +1 -0
- package/dist/register-terminal-view.js +21 -0
- package/dist/register-terminal-view.js.map +1 -0
- package/dist/register.d.ts +10 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +5 -0
- package/dist/register.js.map +1 -0
- package/dist/reports.d.ts +57 -0
- package/dist/reports.d.ts.map +1 -0
- package/dist/reports.js +455 -0
- package/dist/reports.js.map +1 -0
- package/dist/routes.d.ts +3 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +59 -0
- package/dist/routes.js.map +1 -0
- package/dist/schemas.d.ts +151 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +258 -0
- package/dist/schemas.js.map +1 -0
- package/dist/service.d.ts +306 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +3078 -0
- package/dist/service.js.map +1 -0
- package/dist/services/balancedTrustScoreCalculator.d.ts +61 -0
- package/dist/services/balancedTrustScoreCalculator.d.ts.map +1 -0
- package/dist/services/balancedTrustScoreCalculator.js +207 -0
- package/dist/services/balancedTrustScoreCalculator.js.map +1 -0
- package/dist/services/historicalPriceService.d.ts +59 -0
- package/dist/services/historicalPriceService.d.ts.map +1 -0
- package/dist/services/historicalPriceService.js +291 -0
- package/dist/services/historicalPriceService.js.map +1 -0
- package/dist/services/index.d.ts +12 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +17 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/priceEnrichmentService.d.ts +109 -0
- package/dist/services/priceEnrichmentService.d.ts.map +1 -0
- package/dist/services/priceEnrichmentService.js +780 -0
- package/dist/services/priceEnrichmentService.js.map +1 -0
- package/dist/services/simulationActorsV2.d.ts +54 -0
- package/dist/services/simulationActorsV2.d.ts.map +1 -0
- package/dist/services/simulationActorsV2.js +362 -0
- package/dist/services/simulationActorsV2.js.map +1 -0
- package/dist/services/simulationRunner.d.ts +113 -0
- package/dist/services/simulationRunner.d.ts.map +1 -0
- package/dist/services/simulationRunner.js +771 -0
- package/dist/services/simulationRunner.js.map +1 -0
- package/dist/services/tokenSimulationService.d.ts +34 -0
- package/dist/services/tokenSimulationService.d.ts.map +1 -0
- package/dist/services/tokenSimulationService.js +297 -0
- package/dist/services/tokenSimulationService.js.map +1 -0
- package/dist/services/trustScoreOptimizer.d.ts +110 -0
- package/dist/services/trustScoreOptimizer.d.ts.map +1 -0
- package/dist/services/trustScoreOptimizer.js +635 -0
- package/dist/services/trustScoreOptimizer.js.map +1 -0
- package/dist/simulationActors.d.ts +35 -0
- package/dist/simulationActors.d.ts.map +1 -0
- package/dist/simulationActors.js +160 -0
- package/dist/simulationActors.js.map +1 -0
- package/dist/social-alpha-view-bundle.d.ts +2 -0
- package/dist/social-alpha-view-bundle.d.ts.map +1 -0
- package/dist/social-alpha-view-bundle.js +5 -0
- package/dist/social-alpha-view-bundle.js.map +1 -0
- package/dist/types.d.ts +937 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +46 -0
- package/dist/types.js.map +1 -0
- package/dist/views/brand/background/clouds_background.jpg +0 -0
- package/dist/views/brand/banners/eliza_banner.svg +20 -0
- package/dist/views/brand/banners/elizacloud_banner.svg +20 -0
- package/dist/views/brand/banners/elizaos_banner.svg +20 -0
- package/dist/views/brand/concepts/billboard_concept_1200.jpg +0 -0
- package/dist/views/brand/concepts/chibi_usb_concept_900.jpg +0 -0
- package/dist/views/brand/concepts/concept_minipc_900.jpg +0 -0
- package/dist/views/brand/concepts/concept_phone_800.jpg +0 -0
- package/dist/views/brand/concepts/concept_usbdrive_900.jpg +0 -0
- package/dist/views/brand/favicons/android-chrome-192x192.png +0 -0
- package/dist/views/brand/favicons/android-chrome-512x512.png +0 -0
- package/dist/views/brand/favicons/apple-touch-icon.png +0 -0
- package/dist/views/brand/favicons/favicon-16x16.png +0 -0
- package/dist/views/brand/favicons/favicon-32x32.png +0 -0
- package/dist/views/brand/favicons/favicon.ico +0 -0
- package/dist/views/brand/favicons/favicon.svg +17 -0
- package/dist/views/brand/logos/elizaOS_text_black.svg +3 -0
- package/dist/views/brand/logos/elizaOS_text_white.svg +3 -0
- package/dist/views/brand/logos/eliza_logotext.svg +26 -0
- package/dist/views/brand/logos/eliza_logotext_black.svg +26 -0
- package/dist/views/brand/logos/eliza_text_black.svg +3 -0
- package/dist/views/brand/logos/eliza_text_white.svg +3 -0
- package/dist/views/brand/logos/elizacloud_logotext.svg +26 -0
- package/dist/views/brand/logos/elizacloud_logotext_black.svg +26 -0
- package/dist/views/brand/logos/elizacloud_text_black.svg +3 -0
- package/dist/views/brand/logos/elizacloud_text_white.svg +3 -0
- package/dist/views/brand/logos/elizaos_logotext.svg +26 -0
- package/dist/views/brand/logos/elizaos_logotext_black.svg +26 -0
- package/dist/views/brand/logos/logo_blue_blackbg.svg +18 -0
- package/dist/views/brand/logos/logo_blue_nobg.svg +17 -0
- package/dist/views/brand/logos/logo_orange_blackbg.svg +18 -0
- package/dist/views/brand/logos/logo_orange_nobg.svg +17 -0
- package/dist/views/brand/logos/logo_white_blackbg.svg +25 -0
- package/dist/views/brand/logos/logo_white_bluebg.svg +25 -0
- package/dist/views/brand/logos/logo_white_graybg.svg +18 -0
- package/dist/views/brand/logos/logo_white_nobg.svg +24 -0
- package/dist/views/brand/logos/logo_white_orangebg.svg +25 -0
- package/dist/views/brand/ogembeds/eliza_ogembed.png +0 -0
- package/dist/views/brand/ogembeds/eliza_ogembed.svg +20 -0
- package/dist/views/brand/ogembeds/elizacloud_ogembed.png +0 -0
- package/dist/views/brand/ogembeds/elizacloud_ogembed.svg +20 -0
- package/dist/views/brand/ogembeds/elizaos_ogembed.png +0 -0
- package/dist/views/brand/ogembeds/elizaos_ogembed.svg +20 -0
- package/dist/views/bundle.js +268 -0
- package/dist/views/bundle.js.map +1 -0
- package/dist/views/site.webmanifest +19 -0
- package/package.json +5 -5
package/dist/service.js
ADDED
|
@@ -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
|