Skip to main content

madeonsol/
lib.rs

1//! # MadeOnSol — official Rust SDK
2//!
3//! Solana KOL wallet tracking, Pump.fun deployer intelligence, alpha-wallet scoring,
4//! and an all-DEX trade firehose.
5//!
6//! ## Get an API key
7//!
8//! Free tier: **200 requests/day, no credit card** at <https://madeonsol.com/pricing>.
9//! Paid tiers (PRO $49/mo, ULTRA $149/mo) unlock higher rate limits, sub-hour windows,
10//! WebSocket streaming, webhooks, and the all-DEX firehose.
11//!
12//! All keys start with `msk_`.
13//!
14//! ## Quick start
15//!
16//! ```no_run
17//! use madeonsol::{MadeOnSol, types::KolFeedParams};
18//!
19//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
20//! let api_key = std::env::var("MADEONSOL_API_KEY")?;
21//! let client = MadeOnSol::new(api_key)?;
22//!
23//! let feed = client
24//!     .kol
25//!     .feed(&KolFeedParams { limit: Some(10), ..Default::default() })
26//!     .await?;
27//!
28//! for trade in feed.trades {
29//!     println!("{:?} bought {:?}", trade.kol_name, trade.token_symbol);
30//! }
31//! # Ok(())
32//! # }
33//! ```
34//!
35//! ## Namespaces
36//!
37//! - [`MadeOnSol::kol`] — KOL feed, leaderboard, coordination, PnL, trending tokens, alerts, scout leaderboard
38//! - [`MadeOnSol::deployer`] — Pump.fun deployer leaderboard, alerts, trajectory
39//! - [`MadeOnSol::alpha`] — alpha-wallet leaderboard, profiles, cap tables, buyer quality
40//! - [`MadeOnSol::wallet_tracker`] — track arbitrary Solana wallets (watchlist)
41//! - [`MadeOnSol::wallet`] — universal wallet stats, FIFO PnL, open positions, paginated trades (PRO+)
42//! - [`MadeOnSol::coordination_alerts`] — push alerts on coordinated buying (PRO/ULTRA)
43//! - [`MadeOnSol::price_alerts`] — price-drop / recovery alert rules CRUD (PRO/ULTRA)
44//! - [`MadeOnSol::signals`] — Signal Scorecard: out-of-sample, machine-readable signal reliability
45//! - [`MadeOnSol::tools`] — Solana tool directory search
46//! - [`MadeOnSol::stream`] — WebSocket streaming token issuance + live session list/kill
47//! - [`MadeOnSol::webhooks`] — webhook CRUD (PRO/ULTRA)
48//!
49//! Full API reference: <https://madeonsol.com/api-docs>
50
51#![warn(missing_debug_implementations)]
52#![warn(rust_2018_idioms)]
53
54mod client;
55pub mod api;
56pub mod error;
57pub mod types;
58
59use std::sync::Arc;
60
61use crate::api::{
62    alpha::Alpha, coordination_alerts::CoordinationAlerts, deployer::Deployer,
63    first_touch_subscriptions::FirstTouchSubscriptions, kol::Kol, me::Me,
64    price_alerts::PriceAlerts, signals::Signals, sniper::Sniper, stream::Stream, token::Token,
65    tools::Tools, wallet::Wallet, wallet_tracker::WalletTracker, webhooks::Webhooks,
66};
67use crate::client::HttpCore;
68use crate::error::{MadeOnSolError, Result};
69
70pub use crate::error::MadeOnSolError as Error;
71
72/// MadeOnSol API client.
73///
74/// Construct with [`MadeOnSol::new`] and a `msk_…` API key, then access the
75/// namespaced sub-clients ([`kol`](Self::kol), [`deployer`](Self::deployer), etc.).
76///
77/// Cheap to clone — internal HTTP state is reference-counted.
78///
79/// # Example
80///
81/// ```no_run
82/// use madeonsol::MadeOnSol;
83///
84/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
85/// let client = MadeOnSol::new(std::env::var("MADEONSOL_API_KEY")?)?;
86/// let stats = client.deployer.stats().await?;
87/// println!("{} deployers tracked", stats.tracked_count);
88/// # Ok(())
89/// # }
90/// ```
91#[derive(Debug, Clone)]
92pub struct MadeOnSol {
93    /// KOL wallet tracking endpoints.
94    pub kol: Kol,
95    /// Pump.fun deployer intelligence endpoints.
96    pub deployer: Deployer,
97    /// Alpha wallet intelligence: leaderboard, profiles, cap tables, buyer quality.
98    pub alpha: Alpha,
99    /// Token intelligence — comprehensive per-mint snapshot + batch lookups.
100    pub token: Token,
101    /// Account self-inspection — tier, quota, feature usage (v0.8).
102    pub me: Me,
103    /// Wallet tracker: watchlist CRUD, trades, summary.
104    pub wallet_tracker: WalletTracker,
105    /// Universal wallet endpoints — stats, FIFO PnL, open positions, paginated trades for any Solana wallet. PRO+.
106    pub wallet: Wallet,
107    /// Coordination alert rules CRUD (v1.1) — PRO/ULTRA.
108    pub coordination_alerts: CoordinationAlerts,
109    /// First-touch webhook subscriptions CRUD — ULTRA only. Use `kol.first_touches()` for read-only queries.
110    pub first_touch_subscriptions: FirstTouchSubscriptions,
111    /// Price-drop / recovery alert rules CRUD (v1.9) — PRO/ULTRA.
112    pub price_alerts: PriceAlerts,
113    /// Signal Scorecard (v0.16) — out-of-sample, machine-readable signal reliability + catalog.
114    pub signals: Signals,
115    /// Deshred pre-confirm pump.fun sniper feed + custom watchlist — PRO/ULTRA.
116    pub sniper: Sniper,
117    /// Solana tool directory search.
118    pub tools: Tools,
119    /// WebSocket streaming token issuance.
120    pub stream: Stream,
121    /// Webhook management (PRO/ULTRA).
122    pub webhooks: Webhooks,
123}
124
125impl MadeOnSol {
126    /// Construct a new client.
127    ///
128    /// `api_key` must start with `msk_`. Get a free key (200 req/day, no card)
129    /// at <https://madeonsol.com/pricing>.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`MadeOnSolError::MissingApiKey`] if the key is empty or missing the
134    /// `msk_` prefix. The error message includes the signup URL so end users know
135    /// where to go.
136    pub fn new(api_key: impl Into<String>) -> Result<Self> {
137        let api_key = api_key.into();
138        if !api_key.starts_with("msk_") {
139            // Print to stderr too — a bare Err can be swallowed and the user
140            // never sees the link to /pricing.
141            eprintln!(
142                "\n[madeonsol] Missing or invalid API key.\n\
143                 → Get a free key (200 req/day, no card) at https://madeonsol.com/pricing\n\
144                 → Then: madeonsol::MadeOnSol::new(std::env::var(\"MADEONSOL_API_KEY\")?)?\n"
145            );
146            return Err(MadeOnSolError::MissingApiKey);
147        }
148
149        let core = Arc::new(HttpCore::new(api_key));
150        Ok(Self {
151            kol: Kol { core: Arc::clone(&core) },
152            deployer: Deployer { core: Arc::clone(&core) },
153            alpha: Alpha { core: Arc::clone(&core) },
154            token: Token { core: Arc::clone(&core) },
155            me: Me { core: Arc::clone(&core) },
156            wallet_tracker: WalletTracker { core: Arc::clone(&core) },
157            wallet: Wallet { core: Arc::clone(&core) },
158            coordination_alerts: CoordinationAlerts { core: Arc::clone(&core) },
159            first_touch_subscriptions: FirstTouchSubscriptions { core: Arc::clone(&core) },
160            price_alerts: PriceAlerts { core: Arc::clone(&core) },
161            signals: Signals { core: Arc::clone(&core) },
162            sniper: Sniper { core: Arc::clone(&core) },
163            tools: Tools { core: Arc::clone(&core) },
164            stream: Stream { core: Arc::clone(&core) },
165            webhooks: Webhooks { core },
166        })
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn rejects_missing_api_key() {
176        let err = MadeOnSol::new("").unwrap_err();
177        assert!(matches!(err, MadeOnSolError::MissingApiKey));
178    }
179
180    #[test]
181    fn rejects_wrong_prefix() {
182        let err = MadeOnSol::new("sk_live_abc").unwrap_err();
183        assert!(matches!(err, MadeOnSolError::MissingApiKey));
184    }
185
186    #[test]
187    fn accepts_valid_prefix() {
188        let client = MadeOnSol::new("msk_test_abcdef").unwrap();
189        // Smoke test — namespaces exist and the client clones cheaply.
190        let _cloned = client.clone();
191    }
192
193    /// Regression: since 2026-08-27 `POST /stream/token` returns
194    /// `expires_at: null` / `next_refresh_at: null` (stream tokens never
195    /// expire) plus `rotated` / `lifetime`. 0.26.0's
196    /// `expires_at: String` refused that body, so `get_token()` errored for
197    /// every caller.
198    #[test]
199    fn stream_token_deserializes_null_expiry() {
200        let t: crate::types::StreamToken = serde_json::from_str(
201            r#"{"token":"abc","expires_at":null,"next_refresh_at":null,"rotated":false,
202                "lifetime":"This token does not expire.",
203                "ws_url":"wss://madeonsol.com/ws/v1/stream","usage":"connect"}"#,
204        )
205        .unwrap();
206        assert_eq!(t.token, "abc");
207        assert!(t.expires_at.is_none());
208        assert!(t.next_refresh_at.is_none());
209        assert_eq!(t.rotated, Some(false));
210        assert!(t.lifetime.is_some());
211        assert!(t.dex_ws_url.is_none());
212
213        // Pre-2026-08-27 servers sent a timestamp and omitted the new fields.
214        let old: crate::types::StreamToken = serde_json::from_str(
215            r#"{"token":"abc","expires_at":"2026-08-28T00:00:00Z",
216                "ws_url":"wss://madeonsol.com/ws/v1/stream","usage":"connect"}"#,
217        )
218        .unwrap();
219        assert_eq!(old.expires_at.as_deref(), Some("2026-08-28T00:00:00Z"));
220        assert!(old.rotated.is_none());
221        assert!(old.lifetime.is_none());
222    }
223}