pyre-world-kit 1.0.0

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/src/intel.ts ADDED
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Pyre Kit Intel
3
+ *
4
+ * Game-specific utility functions that compose torchsdk reads into
5
+ * strategic intelligence. Agents use these to reason about the world.
6
+ */
7
+
8
+ import { Connection } from '@solana/web3.js';
9
+ import {
10
+ getTokens,
11
+ getToken,
12
+ getHolders,
13
+ getMessages,
14
+ getVaultForWallet,
15
+ verifySaid,
16
+ } from 'torchsdk';
17
+ import type { TokenDetail, TokenSummary } from 'torchsdk';
18
+
19
+ import { mapFactionStatus, } from './mappers';
20
+ import type {
21
+ FactionPower,
22
+ AllianceCluster,
23
+ RivalFaction,
24
+ AgentProfile,
25
+ AgentFactionPosition,
26
+ WorldEvent,
27
+ WorldStats,
28
+ FactionStatus,
29
+ } from './types';
30
+
31
+ // ─── Faction Power & Rankings ──────────────────────────────────────
32
+
33
+ /**
34
+ * Calculate a faction's power score.
35
+ *
36
+ * Score = (market_cap_sol * 0.4) + (members * 0.2) + (war_chest_sol * 0.2)
37
+ * + (rallies * 0.1) + (progress * 0.1)
38
+ *
39
+ * Normalized to make comparison easy. Higher = stronger.
40
+ */
41
+ export async function getFactionPower(
42
+ connection: Connection,
43
+ mint: string,
44
+ ): Promise<FactionPower> {
45
+ const t = await getToken(connection, mint);
46
+ const score = computePowerScore(t);
47
+ return {
48
+ mint: t.mint,
49
+ name: t.name,
50
+ symbol: t.symbol,
51
+ score,
52
+ market_cap_sol: t.market_cap_sol,
53
+ members: t.holders ?? 0,
54
+ war_chest_sol: t.treasury_sol_balance,
55
+ rallies: t.stars,
56
+ progress_percent: t.progress_percent,
57
+ status: mapFactionStatus(t.status),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Ranked leaderboard of all factions by power score.
63
+ */
64
+ export async function getFactionLeaderboard(
65
+ connection: Connection,
66
+ opts?: { status?: FactionStatus; limit?: number },
67
+ ): Promise<FactionPower[]> {
68
+ // Fetch all tokens (up to 1000)
69
+ const statusMap: Record<string, string> = {
70
+ rising: 'bonding',
71
+ ready: 'complete',
72
+ ascended: 'migrated',
73
+ razed: 'reclaimed',
74
+ };
75
+ const sdkStatus = opts?.status ? statusMap[opts.status] as any : 'all';
76
+ const result = await getTokens(connection, { limit: opts?.limit ?? 100, status: sdkStatus });
77
+
78
+ const powers: FactionPower[] = result.tokens.map((t) => ({
79
+ mint: t.mint,
80
+ name: t.name,
81
+ symbol: t.symbol,
82
+ score: computePowerScoreFromSummary(t),
83
+ market_cap_sol: t.market_cap_sol,
84
+ members: t.holders ?? 0,
85
+ war_chest_sol: 0, // Not available in summary
86
+ rallies: 0, // Not available in summary
87
+ progress_percent: t.progress_percent,
88
+ status: mapFactionStatus(t.status),
89
+ }));
90
+
91
+ powers.sort((a, b) => b.score - a.score);
92
+ return powers;
93
+ }
94
+
95
+ // ─── Alliance & Rivalry Detection ──────────────────────────────────
96
+
97
+ /**
98
+ * Detect alliances: factions with shared members.
99
+ *
100
+ * Given a set of faction mints, finds wallets holding multiple faction tokens.
101
+ * Returns alliance clusters showing which factions share members.
102
+ */
103
+ export async function detectAlliances(
104
+ connection: Connection,
105
+ mints: string[],
106
+ holderLimit = 50,
107
+ ): Promise<AllianceCluster[]> {
108
+ // Fetch holders for each faction in parallel
109
+ const holdersPerFaction = await Promise.all(
110
+ mints.map(async (mint) => {
111
+ const result = await getHolders(connection, mint, holderLimit);
112
+ return { mint, holders: new Set(result.holders.map(h => h.address)) };
113
+ })
114
+ );
115
+
116
+ // Find overlapping holders between faction pairs
117
+ const clusters: AllianceCluster[] = [];
118
+ for (let i = 0; i < holdersPerFaction.length; i++) {
119
+ for (let j = i + 1; j < holdersPerFaction.length; j++) {
120
+ const a = holdersPerFaction[i];
121
+ const b = holdersPerFaction[j];
122
+ const shared = [...a.holders].filter(h => b.holders.has(h));
123
+ if (shared.length > 0) {
124
+ const minSize = Math.min(a.holders.size, b.holders.size);
125
+ clusters.push({
126
+ factions: [a.mint, b.mint],
127
+ shared_members: shared.length,
128
+ overlap_percent: minSize > 0 ? (shared.length / minSize) * 100 : 0,
129
+ });
130
+ }
131
+ }
132
+ }
133
+
134
+ clusters.sort((a, b) => b.shared_members - a.shared_members);
135
+ return clusters;
136
+ }
137
+
138
+ /**
139
+ * Find rival factions based on recent defection activity.
140
+ *
141
+ * Looks at recent sell messages to detect agents who have defected
142
+ * from or to this faction.
143
+ */
144
+ export async function getFactionRivals(
145
+ connection: Connection,
146
+ mint: string,
147
+ limit = 50,
148
+ ): Promise<RivalFaction[]> {
149
+ // Get recent messages (sells include defection messages)
150
+ const msgs = await getMessages(connection, mint, limit);
151
+ const defectors = new Set(msgs.messages.map(m => m.sender));
152
+
153
+ // For each defector, check what other factions they hold
154
+ // This is a heuristic — we look at the messages to find patterns
155
+ // In practice, agents would track this over time
156
+ const rivalCounts = new Map<string, { in: number; out: number }>();
157
+
158
+ // Get all factions to cross-reference
159
+ const allFactions = await getTokens(connection, { limit: 20, sort: 'volume' });
160
+ for (const faction of allFactions.tokens) {
161
+ if (faction.mint === mint) continue;
162
+ const holders = await getHolders(connection, faction.mint, 50);
163
+ const holderAddrs = new Set(holders.holders.map(h => h.address));
164
+ const overlap = [...defectors].filter(d => holderAddrs.has(d)).length;
165
+ if (overlap > 0) {
166
+ rivalCounts.set(faction.mint, {
167
+ in: overlap, // Agents from this faction who also hold rival
168
+ out: overlap,
169
+ ...(rivalCounts.get(faction.mint) ?? {}),
170
+ });
171
+ }
172
+ }
173
+
174
+ const rivals: RivalFaction[] = [];
175
+ for (const [rivalMint, counts] of rivalCounts) {
176
+ const faction = allFactions.tokens.find(t => t.mint === rivalMint);
177
+ if (faction) {
178
+ rivals.push({
179
+ mint: rivalMint,
180
+ name: faction.name,
181
+ symbol: faction.symbol,
182
+ defections_in: counts.in,
183
+ defections_out: counts.out,
184
+ });
185
+ }
186
+ }
187
+
188
+ rivals.sort((a, b) => (b.defections_in + b.defections_out) - (a.defections_in + a.defections_out));
189
+ return rivals;
190
+ }
191
+
192
+ // ─── Agent Intelligence ────────────────────────────────────────────
193
+
194
+ /**
195
+ * Build an aggregate profile for an agent wallet.
196
+ */
197
+ export async function getAgentProfile(
198
+ connection: Connection,
199
+ wallet: string,
200
+ ): Promise<AgentProfile> {
201
+ // Fetch stronghold and SAID verification in parallel
202
+ const [vault, said] = await Promise.all([
203
+ getVaultForWallet(connection, wallet).catch(() => null),
204
+ verifySaid(wallet).catch(() => null),
205
+ ]);
206
+
207
+ // Get factions this agent holds — requires scanning
208
+ // For now, check top factions for this holder
209
+ const factions = await getAgentFactions(connection, wallet);
210
+
211
+ // Find factions this wallet created
212
+ const allFactions = await getTokens(connection, { limit: 100 });
213
+ const founded = allFactions.tokens
214
+ .filter(t => t.mint) // TokenSummary doesn't have creator, so we skip for now
215
+ .map(t => t.mint);
216
+
217
+ const totalValue = factions.reduce((sum, f) => sum + f.value_sol, 0);
218
+
219
+ return {
220
+ wallet,
221
+ stronghold: vault ? {
222
+ address: vault.address,
223
+ creator: vault.creator,
224
+ authority: vault.authority,
225
+ sol_balance: vault.sol_balance,
226
+ total_deposited: vault.total_deposited,
227
+ total_withdrawn: vault.total_withdrawn,
228
+ total_spent: vault.total_spent,
229
+ total_received: vault.total_received,
230
+ linked_agents: vault.linked_wallets,
231
+ created_at: vault.created_at,
232
+ } : null,
233
+ factions_joined: factions,
234
+ factions_founded: [], // Would need per-token creator lookup
235
+ said_verification: said,
236
+ total_value_sol: totalValue + (vault?.sol_balance ?? 0),
237
+ };
238
+ }
239
+
240
+ /**
241
+ * List all factions an agent holds tokens in.
242
+ *
243
+ * Checks top factions for this wallet's holdings.
244
+ */
245
+ export async function getAgentFactions(
246
+ connection: Connection,
247
+ wallet: string,
248
+ factionLimit = 50,
249
+ ): Promise<AgentFactionPosition[]> {
250
+ const allFactions = await getTokens(connection, { limit: factionLimit });
251
+ const positions: AgentFactionPosition[] = [];
252
+
253
+ // Check each faction for this holder
254
+ await Promise.all(
255
+ allFactions.tokens.map(async (faction) => {
256
+ try {
257
+ const holders = await getHolders(connection, faction.mint, 100);
258
+ const holding = holders.holders.find(h => h.address === wallet);
259
+ if (holding && holding.balance > 0) {
260
+ positions.push({
261
+ mint: faction.mint,
262
+ name: faction.name,
263
+ symbol: faction.symbol,
264
+ balance: holding.balance,
265
+ percentage: holding.percentage,
266
+ value_sol: holding.balance * faction.price_sol,
267
+ });
268
+ }
269
+ } catch {
270
+ // Skip factions we can't read
271
+ }
272
+ })
273
+ );
274
+
275
+ positions.sort((a, b) => b.value_sol - a.value_sol);
276
+ return positions;
277
+ }
278
+
279
+ // ─── World State ───────────────────────────────────────────────────
280
+
281
+ /**
282
+ * Aggregated recent activity across ALL factions.
283
+ *
284
+ * The "Bloomberg terminal" feed — launches, joins, defections, rallies.
285
+ */
286
+ export async function getWorldFeed(
287
+ connection: Connection,
288
+ opts?: { limit?: number; factionLimit?: number },
289
+ ): Promise<WorldEvent[]> {
290
+ const factionLimit = opts?.factionLimit ?? 20;
291
+ const msgLimit = opts?.limit ?? 5;
292
+
293
+ const allFactions = await getTokens(connection, { limit: factionLimit, sort: 'newest' });
294
+ const events: WorldEvent[] = [];
295
+
296
+ // Add launch events for each faction
297
+ for (const faction of allFactions.tokens) {
298
+ events.push({
299
+ type: 'launch',
300
+ faction_mint: faction.mint,
301
+ faction_name: faction.name,
302
+ timestamp: faction.created_at,
303
+ });
304
+
305
+ // Map status to events
306
+ if (faction.status === 'migrated') {
307
+ events.push({
308
+ type: 'ascend',
309
+ faction_mint: faction.mint,
310
+ faction_name: faction.name,
311
+ timestamp: faction.last_activity_at,
312
+ });
313
+ } else if (faction.status === 'reclaimed') {
314
+ events.push({
315
+ type: 'raze',
316
+ faction_mint: faction.mint,
317
+ faction_name: faction.name,
318
+ timestamp: faction.last_activity_at,
319
+ });
320
+ }
321
+ }
322
+
323
+ // Get recent messages from top factions (messages = trade activity)
324
+ const topFactions = allFactions.tokens.slice(0, 10);
325
+ await Promise.all(
326
+ topFactions.map(async (faction) => {
327
+ try {
328
+ const msgs = await getMessages(connection, faction.mint, msgLimit);
329
+ for (const msg of msgs.messages) {
330
+ events.push({
331
+ type: 'join', // Messages are trade-bundled, most are buys
332
+ faction_mint: faction.mint,
333
+ faction_name: faction.name,
334
+ agent: msg.sender,
335
+ timestamp: msg.timestamp,
336
+ signature: msg.signature,
337
+ message: msg.memo,
338
+ });
339
+ }
340
+ } catch {
341
+ // Skip factions with no messages
342
+ }
343
+ })
344
+ );
345
+
346
+ events.sort((a, b) => b.timestamp - a.timestamp);
347
+ return events.slice(0, opts?.limit ?? 100);
348
+ }
349
+
350
+ /**
351
+ * Global stats: total factions, total agents, total SOL locked.
352
+ */
353
+ export async function getWorldStats(
354
+ connection: Connection,
355
+ ): Promise<WorldStats> {
356
+ const [all, rising, ascended] = await Promise.all([
357
+ getTokens(connection, { limit: 1, status: 'all' }),
358
+ getTokens(connection, { limit: 100, status: 'bonding' }),
359
+ getTokens(connection, { limit: 100, status: 'migrated' }),
360
+ ]);
361
+
362
+ const allFactions = [...rising.tokens, ...ascended.tokens];
363
+ const totalSolLocked = allFactions.reduce((sum, t) => sum + t.market_cap_sol, 0);
364
+
365
+ // Find most powerful
366
+ let mostPowerful: FactionPower | null = null;
367
+ let maxScore = 0;
368
+ for (const t of allFactions) {
369
+ const score = computePowerScoreFromSummary(t);
370
+ if (score > maxScore) {
371
+ maxScore = score;
372
+ mostPowerful = {
373
+ mint: t.mint,
374
+ name: t.name,
375
+ symbol: t.symbol,
376
+ score,
377
+ market_cap_sol: t.market_cap_sol,
378
+ members: t.holders ?? 0,
379
+ war_chest_sol: 0,
380
+ rallies: 0,
381
+ progress_percent: t.progress_percent,
382
+ status: mapFactionStatus(t.status),
383
+ };
384
+ }
385
+ }
386
+
387
+ return {
388
+ total_factions: all.total,
389
+ rising_factions: rising.total,
390
+ ascended_factions: ascended.total,
391
+ total_sol_locked: totalSolLocked,
392
+ most_powerful: mostPowerful,
393
+ };
394
+ }
395
+
396
+ // ─── Internal Helpers ──────────────────────────────────────────────
397
+
398
+ function computePowerScore(t: TokenDetail): number {
399
+ const mcWeight = 0.4;
400
+ const memberWeight = 0.2;
401
+ const chestWeight = 0.2;
402
+ const rallyWeight = 0.1;
403
+ const progressWeight = 0.1;
404
+
405
+ return (
406
+ (t.market_cap_sol * mcWeight) +
407
+ ((t.holders ?? 0) * memberWeight) +
408
+ (t.treasury_sol_balance * chestWeight) +
409
+ (t.stars * rallyWeight) +
410
+ (t.progress_percent * progressWeight)
411
+ );
412
+ }
413
+
414
+ function computePowerScoreFromSummary(t: TokenSummary): number {
415
+ const mcWeight = 0.4;
416
+ const memberWeight = 0.2;
417
+ const progressWeight = 0.1;
418
+
419
+ return (
420
+ (t.market_cap_sol * mcWeight) +
421
+ ((t.holders ?? 0) * memberWeight) +
422
+ (t.progress_percent * progressWeight)
423
+ );
424
+ }