Skip to content

Commit b20c25c

Browse files
0xrinegadeclaude
andcommitted
refactor(research): Global aggregation with top-3 counterparties only
Minimize OVSM output for maximum token efficiency by aggregating ALL transfers across tokens and returning only top 3 global senders/receivers. Changes: - OVSM script now aggregates by address (not per-token) - Returns top 3 senders/receivers with ALL their tokens - Output format: {address: {symbol1: amt1, symbol2: amt2, ...}} - Removed per-token detailed breakdowns (was 7 tokens × 10 fields each) - JSON payload reduced from ~70 to ~15-20 data points Benefits: - Saves AI tokens by preprocessing ALL aggregation in OVSM - Shows cross-token patterns (identify key counterparties) - More actionable: "who are the main players?" vs token-by-token detail - Faster AI formatting (less data to process) Example output: - Top sender: CradPJy...E77 sent SLONANA: 451M, SOL: 36.7, SVMAI: 76M - Shows wallet's most important relationships at a glance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 57024f8 commit b20c25c

File tree

1 file changed

+147
-161
lines changed

1 file changed

+147
-161
lines changed

src/commands/research.rs

Lines changed: 147 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -206,80 +206,80 @@ fn generate_wallet_analysis_script(wallet: &str) -> String {
206206
(entries dedup_map)
207207
(lambda (entry) (get entry 1))))
208208
209-
;; Now aggregate by token mint
210-
(define by_mint (group-by unique_transfers (lambda (tx) (get tx "mint"))))
209+
;; Split all transfers by direction
210+
(define all_inflows (filter unique_transfers (lambda (t) (= (get t "transferType") "IN"))))
211+
(define all_outflows (filter unique_transfers (lambda (t) (= (get t "transferType") "OUT"))))
211212
212-
;; For each token, aggregate senders/receivers
213-
(define token_summaries
214-
(map
215-
(entries by_mint)
216-
(lambda (mint_pair)
213+
;; Count unique tokens
214+
(define unique_tokens (group-by unique_transfers (lambda (tx) (get tx "mint"))))
215+
(define num_tokens (length (entries unique_tokens)))
216+
217+
;; Aggregate ALL inflows by sender (with token details)
218+
(define global_senders
219+
(reduce
220+
all_inflows
221+
{{}}
222+
(lambda (acc tx)
217223
(do
218-
(define mint (get mint_pair 0))
219-
(define txs (get mint_pair 1))
220-
221-
;; Get token symbol from first tx
222-
(define symbol (if (> (length txs) 0)
223-
(get (get txs 0) "tokenSymbol")
224-
mint))
225-
226-
;; Split by direction
227-
(define inflows (filter txs (lambda (t) (= (get t "transferType") "IN"))))
228-
(define outflows (filter txs (lambda (t) (= (get t "transferType") "OUT"))))
229-
230-
;; Aggregate inflows by sender
231-
(define inflow_agg
232-
(reduce
233-
inflows
234-
{{}}
235-
(lambda (acc tx)
236-
(do
237-
(define from (get tx "from"))
238-
(define amt (float (get tx "tokenAmount")))
239-
(define existing (get acc from))
240-
(define current (if existing existing 0))
241-
(put acc from (+ current amt))))))
242-
243-
;; Aggregate outflows by receiver
244-
(define outflow_agg
245-
(reduce
246-
outflows
247-
{{}}
248-
(lambda (acc tx)
249-
(do
250-
(define to (get tx "to"))
251-
(define amt (float (get tx "tokenAmount")))
252-
(define existing (get acc to))
253-
(define current (if existing existing 0))
254-
(put acc to (+ current amt))))))
255-
256-
;; Sort and take top 5
257-
(define top_senders
258-
(take 5
259-
(sort
260-
(entries inflow_agg)
261-
(lambda (a b) (> (get a 1) (get b 1))))))
262-
263-
(define top_receivers
264-
(take 5
265-
(sort
266-
(entries outflow_agg)
267-
(lambda (a b) (> (get a 1) (get b 1))))))
268-
269-
{{:mint mint
270-
:symbol symbol
271-
:transfer_count (length txs)
272-
:inflow_count (length inflows)
273-
:outflow_count (length outflows)
274-
:top_senders top_senders
275-
:top_receivers top_receivers}})))
276-
277-
;; Return AGGREGATED summaries (NOT raw transfers!)
224+
(define from (get tx "from"))
225+
(define symbol (get tx "tokenSymbol"))
226+
(define amt (float (get tx "tokenAmount")))
227+
228+
;; Get or create sender record
229+
(define existing (get acc from))
230+
(define sender_record (if existing existing {{}}))
231+
232+
;; Add token amount to sender's token list
233+
(define token_existing (get sender_record symbol))
234+
(define token_current (if token_existing token_existing 0))
235+
(define updated_record (put sender_record symbol (+ token_current amt)))
236+
237+
(put acc from updated_record)))))
238+
239+
;; Aggregate ALL outflows by receiver (with token details)
240+
(define global_receivers
241+
(reduce
242+
all_outflows
243+
{{}}
244+
(lambda (acc tx)
245+
(do
246+
(define to (get tx "to"))
247+
(define symbol (get tx "tokenSymbol"))
248+
(define amt (float (get tx "tokenAmount")))
249+
250+
;; Get or create receiver record
251+
(define existing (get acc to))
252+
(define receiver_record (if existing existing {{}}))
253+
254+
;; Add token amount to receiver's token list
255+
(define token_existing (get receiver_record symbol))
256+
(define token_current (if token_existing token_existing 0))
257+
(define updated_record (put receiver_record symbol (+ token_current amt)))
258+
259+
(put acc to updated_record)))))
260+
261+
;; Convert to sorted arrays and take top 3
262+
(define top_senders
263+
(take 3
264+
(sort
265+
(entries global_senders)
266+
(lambda (a b) (> (length (entries (get a 1))) (length (entries (get b 1))))))))
267+
268+
(define top_receivers
269+
(take 3
270+
(sort
271+
(entries global_receivers)
272+
(lambda (a b) (> (length (entries (get a 1))) (length (entries (get b 1))))))))
273+
274+
;; Return MINIMAL summary (no per-token details!)
278275
{{:wallet target
279276
:total_transfers_raw (length all_transfers)
280277
:total_transfers_unique (length unique_transfers)
281-
:num_tokens (length token_summaries)
282-
:tokens token_summaries}})
278+
:num_tokens num_tokens
279+
:inflow_count (length all_inflows)
280+
:outflow_count (length all_outflows)
281+
:top_senders top_senders
282+
:top_receivers top_receivers}})
283283
"#, wallet)
284284
}
285285

@@ -305,90 +305,81 @@ fn extract_summary_json(result: &ovsm::Value) -> Result<String> {
305305
.and_then(|v| v.as_int().ok())
306306
.unwrap_or(0);
307307

308-
let tokens_array = obj.get("tokens")
309-
.ok_or_else(|| anyhow::anyhow!("Expected tokens key"))?
308+
let inflow_count = obj.get("inflow_count")
309+
.and_then(|v| v.as_int().ok())
310+
.unwrap_or(0);
311+
312+
let outflow_count = obj.get("outflow_count")
313+
.and_then(|v| v.as_int().ok())
314+
.unwrap_or(0);
315+
316+
// Extract top senders: [[address, {symbol: amount, ...}], ...]
317+
let top_senders_array = obj.get("top_senders")
318+
.ok_or_else(|| anyhow::anyhow!("Expected top_senders key"))?
319+
.as_array()?;
320+
321+
let mut top_senders = Vec::new();
322+
for sender_pair in top_senders_array {
323+
let pair = sender_pair.as_array()?;
324+
if pair.len() == 2 {
325+
let address = pair[0].as_string().ok().unwrap_or("unknown");
326+
let tokens_obj = pair[1].as_object()?;
327+
328+
let mut tokens = Vec::new();
329+
for (symbol, amount) in tokens_obj.iter() {
330+
if let Ok(amt) = amount.as_float() {
331+
tokens.push(json!({
332+
"symbol": symbol,
333+
"amount": amt
334+
}));
335+
}
336+
}
337+
338+
top_senders.push(json!({
339+
"address": address,
340+
"tokens": tokens
341+
}));
342+
}
343+
}
344+
345+
// Extract top receivers: [[address, {symbol: amount, ...}], ...]
346+
let top_receivers_array = obj.get("top_receivers")
347+
.ok_or_else(|| anyhow::anyhow!("Expected top_receivers key"))?
310348
.as_array()?;
311349

312-
let mut tokens_summary = Vec::new();
313-
314-
for token in tokens_array {
315-
let token_obj = token.as_object()?;
316-
317-
let symbol = token_obj.get("symbol")
318-
.and_then(|v| v.as_string().ok())
319-
.unwrap_or("unknown");
320-
321-
let mint = token_obj.get("mint")
322-
.and_then(|v| v.as_string().ok())
323-
.unwrap_or("unknown");
324-
325-
let transfer_count = token_obj.get("transfer_count")
326-
.and_then(|v| v.as_int().ok())
327-
.unwrap_or(0);
328-
329-
let inflow_count = token_obj.get("inflow_count")
330-
.and_then(|v| v.as_int().ok())
331-
.unwrap_or(0);
332-
333-
let outflow_count = token_obj.get("outflow_count")
334-
.and_then(|v| v.as_int().ok())
335-
.unwrap_or(0);
336-
337-
// Extract top senders (address, amount pairs)
338-
let top_senders = token_obj.get("top_senders")
339-
.and_then(|v| v.as_array().ok())
340-
.map(|arr| {
341-
arr.iter().filter_map(|pair| {
342-
pair.as_array().ok().and_then(|p| {
343-
if p.len() == 2 {
344-
Some(json!({
345-
"address": p[0].as_string().ok().unwrap_or("unknown"),
346-
"amount": p[1].as_float().ok().unwrap_or(0.0)
347-
}))
348-
} else {
349-
None
350-
}
351-
})
352-
}).collect::<Vec<_>>()
353-
})
354-
.unwrap_or_default();
355-
356-
// Extract top receivers
357-
let top_receivers = token_obj.get("top_receivers")
358-
.and_then(|v| v.as_array().ok())
359-
.map(|arr| {
360-
arr.iter().filter_map(|pair| {
361-
pair.as_array().ok().and_then(|p| {
362-
if p.len() == 2 {
363-
Some(json!({
364-
"address": p[0].as_string().ok().unwrap_or("unknown"),
365-
"amount": p[1].as_float().ok().unwrap_or(0.0)
366-
}))
367-
} else {
368-
None
369-
}
370-
})
371-
}).collect::<Vec<_>>()
372-
})
373-
.unwrap_or_default();
374-
375-
tokens_summary.push(json!({
376-
"symbol": symbol,
377-
"mint": mint,
378-
"transfer_count": transfer_count,
379-
"inflow_count": inflow_count,
380-
"outflow_count": outflow_count,
381-
"top_senders": top_senders,
382-
"top_receivers": top_receivers
383-
}));
350+
let mut top_receivers = Vec::new();
351+
for receiver_pair in top_receivers_array {
352+
let pair = receiver_pair.as_array()?;
353+
if pair.len() == 2 {
354+
let address = pair[0].as_string().ok().unwrap_or("unknown");
355+
let tokens_obj = pair[1].as_object()?;
356+
357+
let mut tokens = Vec::new();
358+
for (symbol, amount) in tokens_obj.iter() {
359+
if let Ok(amt) = amount.as_float() {
360+
tokens.push(json!({
361+
"symbol": symbol,
362+
"amount": amt
363+
}));
364+
}
365+
}
366+
367+
top_receivers.push(json!({
368+
"address": address,
369+
"tokens": tokens
370+
}));
371+
}
384372
}
385373

386374
let summary = json!({
387375
"wallet": wallet,
388376
"total_transfers_raw": total_raw,
389377
"total_transfers_unique": total_unique,
390378
"num_tokens": num_tokens,
391-
"tokens": tokens_summary
379+
"inflow_count": inflow_count,
380+
"outflow_count": outflow_count,
381+
"top_senders": top_senders,
382+
"top_receivers": top_receivers
392383
});
393384

394385
Ok(serde_json::to_string_pretty(&summary)?)
@@ -399,28 +390,23 @@ async fn format_wallet_analysis(ai_service: &mut AiService, wallet: &str, summar
399390
// System prompt for custom formatting (bypass planning mode)
400391
let system_prompt = r#"You are a markdown formatter. The blockchain analysis is ALREADY COMPLETE.
401392
402-
Your ONLY job: Convert the provided JSON into clean markdown tables.
393+
Your ONLY job: Convert the provided JSON into clean markdown.
403394
404395
DO NOT analyze, interpret, or add commentary. ONLY format what's given:
405396
406-
For each token in the JSON, create:
407-
408-
**{symbol} ({mint})**
409-
- Transfers: {transfer_count} ({inflow_count} in, {outflow_count} out)
410-
411-
Top Inflows:
412-
| Address | Amount |
413-
|---------|--------|
414-
[list top_senders array]
397+
**Wallet Summary**
398+
- Total: {total_transfers_unique} unique transfers ({inflow_count} in, {outflow_count} out)
399+
- Tokens: {num_tokens} different tokens
415400
416-
Top Outflows:
417-
| Address | Amount |
418-
|---------|--------|
419-
[list top_receivers array]
401+
**Top 3 Senders** (addresses sending TO this wallet)
402+
| Address | Tokens Sent |
403+
|---------|-------------|
404+
[For each sender in top_senders array, list address and all tokens with amounts]
420405
421-
Summary at top:
422-
- Total: {total_transfers_unique} unique transfers
423-
- Tokens: {num_tokens}
406+
**Top 3 Receivers** (addresses receiving FROM this wallet)
407+
| Address | Tokens Received |
408+
|---------|-----------------|
409+
[For each receiver in top_receivers array, list address and all tokens with amounts]
424410
425411
NO analysis. NO interpretation. ONLY formatting the JSON into tables."#.to_string();
426412

0 commit comments

Comments
 (0)