solwatch 0.1.5

Real-data Solana memecoin auditor — rug/freeze/bundle scanner with a 0-100 risk score, plain-English flags, and a live dashboard. Sister tool to Hoodwatch.
Documentation
//! Deployer analysis: the dev wallet (from Jupiter's index), its age, how much
//! of the supply it still holds, and how many OTHER tokens it has launched —
//! the serial-rugger signal.

use crate::chain::jupiter::JupToken;
use crate::chain::SolClient;
use crate::types::{Flag, Severity};
use serde_json::{json, Value};

pub async fn analyze(client: &SolClient, jup: Option<&JupToken>) -> (Value, Vec<Flag>) {
    let mut flags = vec![];
    let dev = jup.and_then(|j| j.dev.clone());
    let dev_mints = jup.and_then(|j| j.audit.dev_mints);
    let dev_balance_pct = jup.and_then(|j| j.audit.dev_balance_percentage);

    let mut wallet_age_days: Option<i64> = None;
    let mut wallet_tx_floor: Option<usize> = None;
    if let Some(d) = &dev {
        if let Ok(hist) = client.signatures(d, 1000, None).await {
            wallet_tx_floor = Some(hist.len());
            if hist.len() < 1000 {
                if let Some(oldest) = hist.last().and_then(|s| s.block_time) {
                    let now = time::OffsetDateTime::now_utc().unix_timestamp();
                    wallet_age_days = Some((now - oldest) / 86_400);
                }
            }
            if hist.len() <= 5 {
                flags.push(Flag::new(
                    "fresh_deployer",
                    Severity::Warning,
                    "Fresh deployer wallet",
                    format!(
                        "The creator wallet {} has only {} transactions — no track record at all.",
                        short(d),
                        hist.len()
                    ),
                    5,
                ));
            }
        }
    }

    if let Some(n) = dev_mints {
        if n >= 50 {
            flags.push(Flag::new(
                "serial_deployer",
                Severity::Danger,
                "Serial token deployer",
                format!("This creator has launched {n} tokens — classic churn-and-rug factory behavior."),
                12,
            ));
        } else if n >= 10 {
            flags.push(Flag::new(
                "serial_deployer",
                Severity::Warning,
                "Creator has launched many tokens",
                format!("This creator has launched {n} tokens before this one."),
                6,
            ));
        }
    }
    if let Some(p) = dev_balance_pct {
        let p2 = (p * 100.0).round() / 100.0;
        if p >= 20.0 {
            flags.push(Flag::new(
                "dev_holdings",
                Severity::Danger,
                "Dev holds a huge bag",
                format!("The creator still holds {p2}% of the supply — one click from a rug."),
                15,
            ));
        } else if p >= 10.0 {
            flags.push(Flag::new(
                "dev_holdings",
                Severity::Warning,
                "Dev holds a large bag",
                format!("The creator still holds {p2}% of the supply."),
                8,
            ));
        }
    }

    let section = json!({
        "dev": dev,
        "walletAgeDays": wallet_age_days,
        "walletTxCount": wallet_tx_floor.map(|n| if n >= 1000 { json!("1000+") } else { json!(n) }),
        "tokensLaunched": dev_mints,
        "devBalancePct": dev_balance_pct,
    });
    (section, flags)
}

fn short(a: &str) -> String {
    if a.len() < 12 {
        a.to_string()
    } else {
        format!("{}{}", &a[..4], &a[a.len() - 4..])
    }
}