solwatch 0.1.18

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
//! Jupiter lite-api client (tokenless). One call returns the dev wallet,
//! holder count, mint/freeze audit bits, dev-balance %, dev's total token
//! count (serial-deployer signal), organic-volume score, verification tags,
//! and the launchpad — plus a `recent` feed that powers `solwatch scan`.

use super::SolClient;
use anyhow::Result;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct JupAudit {
    #[serde(rename = "mintAuthorityDisabled")]
    pub mint_authority_disabled: Option<bool>,
    #[serde(rename = "freezeAuthorityDisabled")]
    pub freeze_authority_disabled: Option<bool>,
    #[serde(rename = "topHoldersPercentage")]
    pub top_holders_percentage: Option<f64>,
    #[serde(rename = "devBalancePercentage")]
    pub dev_balance_percentage: Option<f64>,
    #[serde(rename = "devMints")]
    pub dev_mints: Option<u64>,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct JupPool {
    pub id: Option<String>,
    #[serde(rename = "createdAt")]
    pub created_at: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct JupToken {
    pub id: String,
    pub name: Option<String>,
    pub symbol: Option<String>,
    pub icon: Option<String>,
    pub dev: Option<String>,
    pub decimals: Option<u32>,
    #[serde(rename = "totalSupply")]
    pub total_supply: Option<f64>,
    #[serde(rename = "tokenProgram")]
    pub token_program: Option<String>,
    pub launchpad: Option<String>,
    #[serde(rename = "holderCount")]
    pub holder_count: Option<u64>,
    pub fdv: Option<f64>,
    pub mcap: Option<f64>,
    #[serde(rename = "usdPrice")]
    pub usd_price: Option<f64>,
    pub liquidity: Option<f64>,
    #[serde(rename = "firstPool")]
    pub first_pool: Option<JupPool>,
    #[serde(default)]
    pub audit: JupAudit,
    #[serde(rename = "organicScore")]
    pub organic_score: Option<f64>,
    #[serde(rename = "organicScoreLabel")]
    pub organic_score_label: Option<String>,
    #[serde(rename = "isVerified")]
    pub is_verified: Option<bool>,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(rename = "createdAt")]
    pub created_at: Option<String>,
}

impl SolClient {
    async fn jup(&self, path: &str) -> Result<Vec<JupToken>> {
        let url = format!("{}{}", self.jupiter, path);
        let mut last = anyhow::anyhow!("jupiter {path} failed");
        for attempt in 0..3u32 {
            if attempt > 0 {
                tokio::time::sleep(std::time::Duration::from_millis(400 * attempt as u64)).await;
            }
            match self.http.get(&url).send().await {
                Ok(resp) => {
                    let status = resp.status();
                    if status.as_u16() == 429 || status.is_server_error() {
                        last = anyhow::anyhow!("jupiter HTTP {status}");
                        continue;
                    }
                    if !status.is_success() {
                        return Err(anyhow::anyhow!("jupiter HTTP {status}"));
                    }
                    match resp.json::<Vec<JupToken>>().await {
                        Ok(r) => return Ok(r),
                        Err(e) => {
                            last = anyhow::anyhow!("jupiter decode: {e}");
                            continue;
                        }
                    }
                }
                Err(e) => last = anyhow::anyhow!("jupiter: {e}"),
            }
        }
        Err(last)
    }

    /// Jupiter's view of one mint (or None if unindexed there).
    pub async fn jupiter_token(&self, mint: &str) -> Result<Option<JupToken>> {
        let hits = self.jup(&format!("/tokens/v2/search?query={mint}")).await?;
        Ok(hits.into_iter().find(|t| t.id == mint))
    }

    /// Recently created tokens across Solana launchpads (newest first).
    pub async fn jupiter_recent(&self) -> Result<Vec<JupToken>> {
        self.jup("/tokens/v2/recent").await
    }
}