Metadata-Version: 2.4
Name: madeonsol-x402
Version: 2.1.0
Summary: x402 client SDK for MadeOnSol Solana KOL intelligence API. Works with LangChain, CrewAI, and standalone.
License: MIT
Project-URL: Homepage, https://madeonsol.com/solana-api
Project-URL: Repository, https://github.com/madeonsol/madeonsol-python
Keywords: solana,x402,kol,kol-tracker,trading,api,langchain,crewai,ai-agent,memecoin,memecoin-tracker,alpha,alpha-bot,pumpfun,pumpfun-sniper,deployer-hunter,smart-money,copy-trading,solana-trading-bot,madeonsol
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx<1,>=0.27
Requires-Dist: x402[httpx,svm]<3,>=2.0
Provides-Extra: stream
Requires-Dist: websockets>=12; extra == "stream"
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == "langchain"
Provides-Extra: crewai
Requires-Dist: crewai>=0.80; extra == "crewai"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"

# madeonsol-x402

> **Six-family early-stream expansion (#457, not activated):** the managed `earlyStream()`
> client, private-list controls and token/wallet helpers are implemented in the
> `madeonsol-x402` TypeScript SDK. This package does **not** gain that managed client
> or new MCP/plugin actions from this PR. Existing sniper compatibility is ULTRA+
> and deploy-only. See the [release-candidate wire contract](https://madeonsol.com/docs/shredprism-stream.md)
> for raw WebSocket integration; publication/version parity remains a release gate.


[![PyPI](https://img.shields.io/pypi/v/madeonsol-x402?style=flat-square)](https://pypi.org/project/madeonsol-x402/)
[![Python](https://img.shields.io/pypi/pyversions/madeonsol-x402?style=flat-square)](https://pypi.org/project/madeonsol-x402/)
[![Downloads](https://img.shields.io/pypi/dm/madeonsol-x402?style=flat-square)](https://pypi.org/project/madeonsol-x402/)
[![GitHub stars](https://img.shields.io/github/stars/madeonsol/madeonsol-python?style=flat-square&logo=github)](https://github.com/madeonsol/madeonsol-python)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE)

> ⭐ **[Star on GitHub](https://github.com/madeonsol/madeonsol-python)** · 📂 **[Examples](./examples/)** · 📚 **[API docs](https://madeonsol.com/api-docs)**

Python SDK for the [MadeOnSol](https://madeonsol.com) Solana KOL intelligence API.

<!-- Stats below are deliberate conservative floors kept in sync with the site's canonical labels (src/lib/constants.ts KOL_COUNT_LABEL / DEPLOYERS_PROFILED_LABEL / ALPHA_WALLETS_LABEL), rounded down from a live count measured on a known date and bumped only when the real count crosses the next threshold -- never the exact live number, which changes every minute. Do not replace with a live/volatile count. -->

> Real-time Solana trading intelligence: track 2,000+ KOL wallets with <3s latency on paid keys and x402 pay-per-call (free-tier live feeds are 5-min delayed), score 85K+ Pump.fun deployers, surface deshred deploy signals ~500ms before on-chain confirmation, score 1.5M+ early-buyer wallets (incl. dump-cluster detection), push every pump.fun graduation, expose bundle-cohort supply retention (held % of supply), verify any wallet's current on-chain holdings, and stream every DEX trade. Free tier: 200 requests/day across 40+ endpoints (live feeds 5-min delayed) — no signup payment. Get a key at [madeonsol.com/pricing](https://madeonsol.com/pricing).

> **Server update 2026-10-05/08 (no package change needed): realtime developer activity and USDC/USDT trade sizing.** The signed webhook event `dev:activity` (PRO+) and the ULTRA+ DEX-socket `dev_subscribe` path deliver `dev_sell`, `dev_buy`, `dev_token_transfer_out` and `dev_token_transfer_in` (a transfer is never a sell). Transfer detection is now the union of the legacy bounded developer-wallet watch and the broad SPL Token/Token-2022 candidate source for attributed creators while live. `transfer_watch_coverage` describes only the legacy watch; `transfer_sources.broad` in the subscribe ack reports broad-source state/gaps. Recovered broad-source rows are REST history only, never retroactive realtime alerts. Missing creator attribution, source gaps and unsupported mixed swap + standalone-transfer cases remain limitations. On `dex:trades`, a swap paid in USDC or USDT now carries `sol_amount` = the SOL equivalent of its stable leg (used by `dust`, `min_sol`, `max_sol`) plus the additive fields `sol_amount_basis` (`native_sol` | `stable_quote_equivalent` | `stable_quote_unconverted`) and `stable_quote`; `stable_quote_unconverted` means `sol_amount` 0 (size unknown). Details: [changelog](https://madeonsol.com/changelog).

> **Server update 2026-10-04 (no package change needed): copy-trade rules follow any valid source wallet.** `source_wallets` no longer have to be tracked KOL wallets: any valid Solana wallet fires, KOL membership is optional enrichment, and copy-trade sources do not use Wallet Tracker quota. Each rule reports `source_admission` (`any_wallet`) and `operational_state` (`eligible`, or an infrastructure state `monitoring_pending` / `monitoring_unavailable` / `source_capacity_unavailable`). `source_wallets_tracked` / `source_wallets_untracked` and the `untracked_source_wallets` warning are legacy fields, still filled. This supersedes the "signals fire only for tracked KOL wallets" wording in older notes below. Limits are unchanged: PRO 3 rules × 5 wallets, ULTRA 20 × 50, BUSINESS 100 × 250.

> **New in 2.0.0: sniper is ULTRA+ only and no longer keyless (breaking).** In keyless (x402) mode `sniper_recent` raises `KeylessNotAvailableError` before any request or payment: the server retired the keyless sniper endpoint on 2026-10-06 (HTTP 410 `x402_endpoint_retired`, nothing charged). With an ULTRA, BUSINESS or ENTERPRISE API key it works as before (PRO keys now get 403 `tier_required`). Sniper rows can carry the early-observation fields (`event_id`, `source`, `execution_status`, `transaction_version`, ...); observations are intent, not execution proof.

> **New in 1.37.0: token lock provenance.** Docstring: Token lock rows carry `provider` (`identity` verified | compatible | unverified; `lock_url` always null on Solana, never constructed), `explorer` (Solana Explorer links), `price_usd`, `seconds_until_end`, `seconds_until_next_unlock` and, for Bonfida, the tranche `schedule` (server 2026-10-02). Additive only.

> **New in 1.36.0: keyless calls that x402 cannot serve now fail fast.** In x402 (keyless) mode, `scout_leaderboard`, `coordination_history`, `kol_consensus` and `peak_history` raise `KeylessNotAvailableError` (a `RuntimeError`) before any request or payment: those routes are not priced for x402 and returned 404. With an API key they work as before. `X402_UNAVAILABLE_PATHS` is exported. Token risk is described as risk evidence for your own policy, not a verdict (no "safety score" wording). The deployer-profile description names the fields the API returns: `is_deployer` and the nested `deployer` object (`total_tokens_deployed`, ...); an untracked wallet returns `is_deployer: false` and `deployer: null`.

> **New in 1.35.0 — copy-trade market-cap band on rules, Wallet Tracker slot ordering.** `rest.copy_trade_create(..., min_mc_usd=, max_mc_usd=)` sets a market-cap band (USD) on the rule; before, passing either raised `TypeError`. `copy_trade_update` documents the same two fields (`None` clears a bound). `rest.wallet_tracker_trades()` gains `order=` (`"slot"` | `"block_time"`) and the `before_slot=` cursor. Docstrings corrected: `only_action` defaults to `"buy"` (not `"both"`); the Wallet Tracker `action` filter is `"buy"` | `"sell"` only, since `transfer_in` / `transfer_out` were never accepted by the API; copy-trade limits include Business (250 wallets per rule); signals fire only for tracked KOL wallets. `rest.test_webhook(id, event=)` can pick which subscribed event to sample. Docstrings describe the optional fields newer servers return: `source_wallets_tracked` / `source_wallets_untracked` / `warnings` on copy-trade rules, a one-time `webhook_secret` on a `copy_trade_update` that first sets a `webhook_url`, `event` on test results, and `/me` watchlist `limit`. No existing call changes behaviour.

> **New in 1.32.0 — named subscriptions: several independent subscriptions per socket.** `subscribe(channels, filters, sub_id="...")`, `update_subscription(sub_id, filters)`, `unsubscribe("sub-id")`, `get_subscriptions()` / `await list_subscriptions()`. Each named subscription has its own channels and filters (the server caps the total per connection, default included: PRO 5, ULTRA 10, BUSINESS 20); frames carry `evt["sub_id"]`; an event matching several subscriptions is delivered once per subscription (dedupe per `(sub_id, id)`). Resume is per subscription with one commit for the connection. The plain `subscribe(channels, filters)` API is unchanged. See "Named subscriptions" in the stream section.

> **New in 1.31.0 — stream recovery: resume cursor, de-duplication, honest gaps.** The managed stream now tracks the cursor `{ instance, seq, ts }` of the last frame your handlers finished and resumes after it on every reconnect (the v1 `resume` request, with an automatic fallback to `replay_since_seq` / `replay_since_ts` on older servers). Delivery is at-least-once, de-duplicated by event `id`; new lifecycle events `cursor`, `replay`, `gap` (what could not be recovered — a `seq` gap is never loss) and `fatal`. Close codes are handled: 4001 re-fetches the token (bounded), 4002 waits ≥ 60 s instead of looping every second, 4003 stops, 4008 resumes; the backoff resets only after a `subscribed` ack. Every server `warning` frame is emitted (incl. `channels_rejected` / `channels_revoked`). `CHANNELS` gains `token:prices` (and `EVENT_NAMES` `token:price`); `client.stream(**options)` passes `resume=`, `dedupe_size=`, `max_auth_retries=`, `connection_limit_backoff=` through; the handshake token is URL-encoded. See the stream section's "Recovery" notes.

> **New in 1.29.0 — deployer reputation as-of a date, and creator-fee rewards.** `rest.deployer_as_of(wallet, date=)` binds `GET /deployer-hunter/{wallet}/as-of`: the deployer's reputation exactly as it stood on `date` (default today, UTC) — the latest write-on-change snapshot at or before it, so a backtest sees only what was knowable then. `snapshot.snapshot_date` can predate the requested date (write-on-change); `snapshot.carried` is `True` when the state was recorded earlier and had not changed by then. No snapshot at or before `date` → `as_of: False, snapshot: None` — nothing is ever synthesized. `date` must be ≥ 2026-04-07 and not in the future. `rest.deployer_rewards(wallet)` binds `GET /deployer-hunter/{wallet}/rewards`: pump.fun creator-fee rewards, answered two ways that are never merged — `collected` (what actually reached the wallet: direct vault claims kept 90 days, social-handle claims, shareholder payouts on **any** token) and `attributed` (every payout on the tokens it **deployed**, split `to_self`/`to_others` + `redirected_pct`). Every money field is `{sol, usdc, usd}`; `usd` is `None` (never a silent 0) when a SOL amount exists and no SOL price was available. `top_tokens`/`top_recipients` (≤10, USD-sorted) show where attributed fees went, recipients flagged `is_self`/`is_social_pda`. Works for non-deployers too (`is_deployer: False`, `attributed` empty). **Keyed (`msk_`) API only — not on the x402 rail; BASIC gets HTTP 403.**

> **New in 1.28.0 — token surges & revivals: momentum fires with the honest half attached.** `rest.tokens_surges(kind=, tier=, mint=, since=, before=, min_mc_usd=, max_mc_usd=, min_buys=, launchpad=, deployer_tier=, exclude_flags=, only_clean=, stats=, days=, limit=)` binds `GET /tokens/surges` (PRO+): every token momentum fire, newest first. Two kinds, one row shape. **`surge`** — a token < 30 min old whose market cap runs hard against its *launch* MC, in three tiers that each fire at most once per mint: `early` (≤10 min, ≥$12k, ≥3× launch), `strong` (≤30 min, ≥$30k, ≥6× launch **and** ≥2× the lowest sample of the last 3 min — it is climbing *now*), `breakout` (≤2 min, ≥$45k, ≥8×). A tier must be **sustained** (current tick *and* a sample ≥10 s older; nothing fires before 20 s of age) — a same-slot bundle marked to $475k at age 1 s is a spike, not a surge. **`revival`** — a token with no 1-minute trade candle for ≥24 h that starts trading again, confirmed **only by the tape** (≥5 buys, ≥$500 buy volume, MC ≥1.5× the pre-dormancy close), never by the price mark; `tier` is `None`. Hard gates on both: liquidity ≥$1.5k and ≥2 % of MC, and the MC gained must be **paid for** by buy volume (a price mark in a spoof pool moves MC on ~$0). Every row carries the burst `tape` (`source` candles / wallet_trades; `unique_buyers` only where the mint is in trade coverage — `wallet_data_available` `False` otherwise, never an inferred zero), `kol` buyers, the first-20 `early_buyers` cohort (bundled / sold / sniper wallets), `deployer` reputation and `risk_flags` (`bundled_launch`, `few_buyers`, `wash_pattern`, `thin_liquidity`, `cold_deployer`, `sniper_heavy`, `early_buyers_exiting`, `sell_pressure`, `no_tape_trades`, `no_prior_price`, `mint_authority_active`, `transfer_fee`). Rows ≥65 min old carry the +1 h `outcome`; `stats=True` prints per-(kind, tier) hit-rates (`up_1h_pct`, `median_peak_multiple`, `doubled_1h_pct`) — out-of-sample by construction. Stream: `token:surges` (events `token:surge` / `token:revival` — the same row minus `outcome`; subscribe filters `kinds`, `tiers`, `launchpads`, `exclude_flags`, `min_mc_usd` / `max_mc_usd`, `deployer_tier`) — added to `CHANNELS` / `EVENT_NAMES`; the webhook registry accepts `token:surge` / `token:revival` with the same filters. LangChain / CrewAI tool: `MadeOnSolTokenSurges`. **Keyed (`msk_`) API only — not on the x402 rail; BASIC gets HTTP 403.**

> **New in 1.27.1 — stream tokens never expire.** `POST /stream/token` (`rest.get_stream_token()`) now returns the **same token on every call, forever**. It stops working only if your subscription lapses or you call `rest.get_stream_token(rotate=True)` to replace it (the previous value keeps working for 60 s). `expires_at` and the new `next_refresh_at` are always `None` (kept for wire compatibility — do not schedule refreshes on them); the response gains `rotated` (bool) and `lifetime` (str). The server never rotates on its own and never sends `token_refresh` unless you rotated; a `4001` close means "mint again" (lapsed or rotated), never a timer. Prefer `Authorization: Bearer <token>` on the WebSocket handshake — `?token=` still works and is masked in access logs. `client.stream()` already does the right thing (it calls `get_stream_token()` on every (re)connect); no code change needed on your side.

> **New in 1.27.0 — token locks & vesting, upcoming unlocks, and pump.fun creator-fee sharing.** Five keyed (PRO+) REST bindings + two WebSocket channels. `rest.token_locks(mint, status=, program=, limit=)` — every on-chain Streamflow / Jupiter Lock / Bonfida vesting contract on a mint with the schedule (start / cliff / period / end), the terms (`cancelable_by_sender` — a cancelable lock is a weaker promise — `cancelable_by_recipient`, `transferable`, `can_topup`) and a live-derived view (`locked_raw` now, `unlocked`, `withdrawn`, `claimable`, `status`, `next_unlock`), plus a `summary` (exact `lock_count`, `distinct_lockers`, locked / deposited totals as raw + ui + usd + % of supply, `unlocking_7d_*` / `unlocking_30d_*`, nearest `next_unlock`, `active_cancelable_by_sender`). `rest.token_locks_feed(since=, before=, mint=, sender=, recipient=, program=, kind=, status=, min_usd=, min_pct_of_supply=, include_estimated=, limit=)` — cross-token feed of NEW contracts, newest first, cursor `pagination.next_since` / `next_before`. `rest.token_unlocks(within=, mint=, program=, kind=, min_usd=, min_pct_of_supply=, sort=, limit=)` — upcoming unlock EVENTS (`cliff` / `period` / `final` / `tranche`) inside `1h`…`90d` with `amount_*` and `window_amount_*`. **LP locks are NOT included** in any of the three. `rest.token_fee_shares(mint)` — the pump.fun `SharingConfig`: who receives what share (bps) of a coin's creator fees, `is_admin` / `is_social_pda` (fees earmarked for an X account etc. — `social.platform` 2 = X, `user_id` = the platform-native numeric id), `redirected_bps`, `social_bps`, `is_default: true` = 100% to the creator, plus the distributions rollup and config history. `rest.token_fee_claims(type=, mint=, recipient=, actor=, social_platform=, social_user_id=, min_sol=, since=, before=, limit=)` — the fee-event feed (`distribution` with `payouts[]`, `social_claim`, `shares_created` / `shares_updated` / `shares_reset`, `creator_transferred`, `creator_claim` only when asked via `type=`). **Fee history starts 2026-08-17.** All base-unit amounts (`*_raw`) are **strings**; ui / usd / pct are `None` when decimals or price are unknown. Streams: `token:locks` (event `token:lock`, one frame per new contract) and `token:fee_claims` (event `token:fee_claim`) — added to `CHANNELS` / `EVENT_NAMES`. LangChain / CrewAI tools: `MadeOnSolTokenLocks`, `MadeOnSolTokenLocksFeed`, `MadeOnSolTokenUnlocks`, `MadeOnSolTokenFeeShares`, `MadeOnSolTokenFeeClaims`. **Keyed (`msk_`) API only — none of these are on the x402 rail; BASIC gets HTTP 403.**

> **New in 1.26.0 — live holder census: exact holder count, labelled holders, and pools that are named, not just excluded.** `rest.token_holders(mint)` (plus `MadeOnSolTokenHolders` LangChain / CrewAI tools) binds `GET /tokens/{mint}/holders` (PRO+): every token account of the mint read from the ledger at `confirmed` and merged per owner, so `concentration.holder_count` is EXACT (distinct non-zero owners minus pools / bonding curves / burns) — never a trade-derived estimate; it is `null` only when the provider refuses the census for a mega-cap, in which case you get the top-20 view and `source.census_fallback_reason` says so. Each disclosed owner carries our labels (`deployer` / `kol` / `early_buyer` / `bundle` / `bot` / `dump_cluster` — empty means unknown to us, not clean), and `excluded[]` NAMES what was taken out of the circulating denominator: `reason` = `pool` (with `dex` + `pool_address`), `bonding_curve` (pump.fun / LaunchLab), `burn`, or `program_account` only when we genuinely cannot attribute the PDA; `pool_pct` / `burned_pct` / `program_pct` split the exclusion. Amounts are raw u64 **strings**. Disclosure: PRO ranks 1–10, ULTRA 1–50, BUSINESS 1–100 — the maths is tier-independent. Big tokens take 5–30 s upstream: you get `503 holder_scan_in_progress` with `retry_after_seconds: 20` while the scan finishes into the cache, and the retry is instant. **Keyed (v1) only — the census is not on the x402 rail.**

> **New in 1.25.0 — two prices on the trade tape, and the right one is now the default.** The trade tape now tells you what a trade actually cost. `price_sol`/`price_usd` on each trade are THIS trade's executed price — `sol_amount / token_amount`, reconciling exactly with the amounts on the same row and with the PnL endpoints. Because `sol_amount` is the wallet's net SOL movement, that is the trader's all-in effective rate: swap fee and any account rent included, not the pool mid. The market-cap tracker's canonical pool price moved to the new **`market_price_sol`/`market_price_usd`** fields — it is sampled once per token per pool update, so every trade in the same slot shares it. Until now `price_sol` carried that canonical value and disagreed with the row's own amounts by a **7.9% median** (p90 ~74%): a stale market price reads low in a pump and high in a dump, so anything you averaged out of the tape inherited the bias instead of cancelling it. Use `price_sol` for cost basis, fills and PnL; `market_price_sol` for a per-token series independent of trade size and direction. Both `rest.token_trades(mint)` and `rest.wallet_trades(address)` carry all four fields — `wallet_trades` returned amounts and no price at all before.

> **New in 1.24.0 — the Deployer Hunter surface completed.** Seven new operations that existed on the API but had no SDK binding: `deployer_leaderboard()`, `deployer_stats()`, `deployer_profile()`, `deployer_tokens()`, `deployer_alert_stats()`, `deployer_best_tokens()` and `deployer_recent_bonds()` (poll it incrementally with `next_since`). Read `bonding_rate` (lifetime) against `recent_bond_rate` (rolling) — the gap between them is the signal, not either number alone. `runner_rate` only means something once `labeled_tokens >= 3`, and an **untracked wallet returns a profile with zeroed counters, not a 404**, so check `total_deployed` before reading a 0% bond rate as a track record. Dependency ranges are now bounded to the versions actually tested (`@x402/*` `^2.x`, `@solana/kit` `^5.5.1`) instead of open-ended `>=0.0.1`, and the lazily-imported x402 peers are marked optional — a keyed install no longer pulls the whole Solana stack.

> **New in 1.23.0** — **Pool depth / price-impact + dev self-activity on the risk score.** `rest.token_depth(mint, sizes=None)` (`GET /tokens/{mint}/depth`, PRO+) answers "how much SOL does it take to move the price N%" per pool: each depth-computable pool returns `spot_price_sol`, `fee_pct`, `source` (`'stream'` for constant-product AMMs served zero-RPC from stream reserves, `'live_rpc'` for pump.fun/bonk curves priced from a live read of the curve's VIRTUAL reserves), `reserves_age_ms`, per-size `quotes` (`size_sol`, `tokens_out`, `avg_price_sol`, `price_impact_pct`), and `to_move_price` (SOL to move price `'1pct'`/`'5pct'`/`'10pct'`). `sizes` is a CSV string or list of floats (max 8, each >0 and ≤10000; default `0.5,1,5,10`); the response carries `sol_usd`, `sizes_sol`, `primary_pool`, and honesty-first `unsupported_pools` — concentrated pools (CLMM/Orca/DLMM), Meteora-DBC curves, and unclassified pools come back with a `reason` instead of a wrong number. Exposed as the `madeonsol_token_depth` (LangChain) / "MadeOnSol Token Depth" (CrewAI) tool. Plus `rest.token_risk(mint)` responses gain a top-level `dev` object — deployer self-activity for the mint: `wallet`, `launchpad`, `deployed_at`, create-tx `buy_sol`/`buy_tokens`/`buy_supply_pct`, post-create `bought_tokens_after`/`sold_tokens`/`sold_sol` with `first_sell_at`/`last_sell_at`, live on-chain `holdings_tokens`/`holdings_supply_pct`, `wallet_empty` (`bool | None`), and `transferred_out` (`bool | None` — chain balance well below the trade-derived expectation, i.e. tokens moved without a swap) — plus `as_of`. `dev` is `None` when the mint has no tracked deploy row.
>
> **New in 1.22.0** — **Batch wallet classification, token trade tape + 7 endpoints go keyless.** `rest.wallet_batch_classify(["addr1", ...])` returns reputation flags for 1–100 wallets in one call (counts as 1 request): per wallet `is_sniper` / `is_bundler` / `is_dumper` / `is_kol` (+ `kol_name`), `bot_confidence` (string enum `'none'`/`'low'`/`'medium'`/`'high'`, `None` when not alpha-tracked), and a `dump_cluster` block (`dump_cohorts`, `runner_cohorts`, `total_cohorts`, `as_of`). Flags are pump.fun-pipeline scoped — `False` = not observed, NOT verified clean; `is_bundler` is lifetime, `is_dumper` is a rolling 42d window. `rest.token_trades(mint, limit=100, cursor=None, action=None, wallet=None, since=None, until=None)` is the mint-scoped trade tape — cursor-paginated raw trades (default FULL history; capture starts 2026-04-12) with a `coverage` honesty block. Both PRO+. `wallet_stats()` flags gain the same reputation flags + `dump_cluster`, and `bot_confidence` is now correctly a string enum (a server bug made it always `None` before — real values now). `token_risk()` inputs and `sniper_recent()` deploys gain the slot-window `sniper_footprint`/`footprint` rollup. The **keyless x402 catalog grows 18 → 25**: new `MadeOnSolClient` (keyless-capable) methods `token_candles` ($0.01), `almost_bonded` ($0.01), `token_top_traders` ($0.02), `token_cap_table` ($0.02), `sniper_recent` ($0.01), `deployer_trajectory` ($0.01) — joining `token_flow` ($0.01).
>
> **New in 1.21.0** — **Verified wallet holdings.** `rest.wallet_holdings(address, limit=200, min_value_usd=0)` reads the wallet's actual SPL + Token-2022 token accounts and SOL balance directly from chain, enriches each with our price/MC/name/symbol data, and computes `transfer_delta` (on-chain amount minus trade-derived net position) to expose non-swap flows — airdrops, insider funding, wallet-hopping. Distinct from `wallet_positions` (trade-derived FIFO): holdings is "what they actually hold right now". Returns `address`, `sol_balance`, `holdings` (each with `mint`, `symbol`, `name`, `amount`, `amount_raw`, `decimals`, `token_program` (`'spl'` | `'token2022'`), `price_usd`, `value_usd`, `market_cap_usd`, `is_bonded`, `trade_derived_amount`, `transfer_delta`), a `summary` (`token_accounts`, `non_zero`, `returned`, `priced`, `total_value_usd`, `truncated`), `verified_at`, `trade_window_days`, `cache_hit`, and `ttl_seconds`. `limit` 1–500 (default 200), `min_value_usd` ≥ 0 (default 0). ULTRA only.

> **New in 1.20.0** — **Bundle-cohort holdings.** `rest.token_bundle(mint)` reveals how much of a token's supply its launch bundle still holds. The `bundle` summary carries `wallet_count`, `bundle_kind` (`'atomic_tx'` | `'same_slot'` | `'none'`), `held_pct_of_supply` (net-held / circulating supply — the HEADLINE signal, `null` when supply is unknown), `held_ratio` (net-held / buy-volume — churn-sensitive, secondary), `fully_exited`, `buy_volume` (cumulative buy volume — NOT distinct tokens, can exceed supply), and `tokens_held` (swap-derived net position). All tiers reach the endpoint, field-gated by tier: BASIC get the `bundle` summary only (`wallets: []`), PRO adds the top-10 `wallets` with flags only (`rank`, `wallet`, `held_ratio`, `has_sold`, `atomic`, `is_kol`), and ULTRA adds per-wallet identity (`kol_name`, `win_rate`, `bot_confidence`, `tokens_held`).
>
> **New in 1.19.0** — **Batch risk scoring + stream-session control.** `rest.tokens_batch_risk(["mint1", "mint2", ...])` scores 1–50 base58 mints for rug-risk/safety in a single call (counts as 1 request against quota). Returns `{ "tokens": [...], "count": N }` where each entry mirrors the single-mint `rest.token_risk()` shape (`risk_score`, `band`, `factors`, `inputs`) plus an `as_of` ISO-8601 timestamp; untracked mints come back as `{ "mint", "error": "not_tracked" }` and don't fail the batch. `tokens` preserves de-duplicated input order. PRO/ULTRA only. Plus WebSocket session control: `rest.stream_sessions()` lists your live sessions across both stream services (each with `id`, `service`, `tier`, `channels`, `connected_at`, `remote_ip`, `messages_sent`) and `rest.kill_stream_session(id)` force-terminates one and frees its connection slot — the self-serve fix for a 4002 connection-limit lockout after a deploy overlap leaves a ghost socket. Both PRO/ULTRA only, exposed as the `madeonsol_stream_sessions` / `madeonsol_kill_stream_session` (LangChain) and "MadeOnSol Stream Sessions" / "MadeOnSol Kill Stream Session" (CrewAI) tools.
>
> **New in 1.18.0** — **Almost-bonded discovery + trending sorts.** `rest.almost_bonded(min_progress=90, min_velocity_pct_per_min=0.5, deployer_tier="elite", sort="eta_asc", limit=25)` returns pre-bond pump.fun tokens near graduation, ranked by velocity (Δprogress/min) — "95% and accelerating" beats "92% stalled". Each token carries `progress_pct`, `velocity_pct_per_min`, `eta_minutes`, `stalled`, `real_sol_reserves`, `market_cap_usd`, `liquidity_usd`, `authorities_revoked`, `deployer_tier`, and `age_minutes`. `sort` is `'velocity_desc'` (default) / `'progress_desc'` / `'eta_asc'`. PRO/ULTRA only (keyed). Exposed as the `madeonsol_almost_bonded` (LangChain) / "MadeOnSol Almost Bonded" (CrewAI) tool. Plus `rest.tokens_list(sort=...)` gains four momentum sorts — `'mc_change_5m_desc'`, `'mc_change_1h_desc'`, `'volume_1h_desc'`, and `'trending'` (composite recent-volume × positive-momentum rank).
>
> **New in 1.17.0** — **Token money-flow.** `client.token_flow(mint, window="1h")` (and the async `await client.token_flow_async(mint, window="1h")`) aggregates buy/sell pressure for a token over a rolling `'1h'` or `'24h'` window. Returns `mint`, `window`, `from`, `unique_wallets`, `unique_buyers`, `unique_sellers`, `buy_count`, `sell_count`, `total_trades`, `buy_sol`, `sell_sol`, `net_sol`, and `trades_per_wallet`. PRO+ (keyed). Also: `deployer_alerts()` items now carry `deployer_sol_balance` (float | None) — the deployer wallet's SOL balance at alert time (`None` for historical rows).
>
> **New in 1.16.0** — **Live token snapshot + Signal Scorecard (keyless x402).** `client.token(mint)` returns a live `{ "token": {...} }` snapshot (`price_usd`/`price_sol`, `market_cap`, `fdv_usd`, `liquidity_usd`, `liquidity_to_mc_ratio`, `primary_dex`, `is_token_2022`, `transfer_fee_bps`, `top_buyers=[{name, sol_amount}]`). `client.signal_performance(name, history=False)` returns out-of-sample reliability for a named signal (`hit_rate`, `base_rate`, `lift`, `sample_n`, `window_days`, `test_from`/`test_to`, plus `metric_type`, `outcome`, `methodology`, `as_of`; per-day series when `history=True`) — valid names: `dump_cluster_count`, `runner_rate`, `recycled_early_buyer_count`, `coordination_count`. `client.signals()` (free) lists the signal catalog.
>
> **New in 1.15.0** — **Token OHLCV candles.** `rest.token_candles(mint, tf="1h", limit=200, from_=None, to=None)` returns 1-minute-derived OHLCV candles aggregated to a timeframe (`'1m'` | `'5m'` | `'15m'` | `'1h'` | `'4h'` | `'1d'`). Each candle has `t`, `open`, `high`, `low`, `close`, `volume_usd`, `trades`, and `market_cap_usd`. PRO returns OHLCV over the last 30 days; ULTRA adds per-candle net-flow fields (`buy_volume_usd`, `sell_volume_usd`, `net_volume_usd`, `buy_count`, `sell_count`, `volume_mev_usd`, `open_liquidity_usd`, `close_liquidity_usd`, `high_mc_usd`, `low_mc_usd`) and full history.
>
> **New in 1.14.0** — **Token risk score.** `rest.token_risk(mint)` returns a transparent 0–100 rug-risk/safety score (higher = riskier) with a `band` (`'safe'` | `'caution'` | `'danger'`), an explainable `factors` array, and the raw `inputs` (mint/freeze authority, liquidity, liq-to-MC ratio, transfer fee, launch cohort, deployer bond rate, KOL signal, blacklist). PRO/ULTRA only.
>
> **New in 1.13.0** — `rest.tokens_list()` gains three new filter params: `min_liq_mc_ratio`, `max_liq_mc_ratio`, and `deployer_tier` (`'elite'` | `'good'` | `'moderate'` | `'rising'` | `'cold'` | `'unranked'`). Response items now include `liquidity_to_mc_ratio` and `deployer_tier`. KOL leaderboard entries now include `median_hold_minutes_30d` and `percentile_early_entry_30d`. `/token/{mint}` and `/token/batch` responses now include `liquidity_to_mc_ratio`, `launch_cohort_sol`, and `launch_cohort_size`.
>
> **New in 1.12.1** — Deployer alerts/profiles now carry `runner_rate` + `labeled_tokens` (fraction of a deployer's labeled tokens that ran vs dumped, gate on `labeled_tokens` >= 3) plus `avg_time_to_bond_minutes`.

> **New in 1.12** — **Graduation events + dump-cluster detection.** Subscribe `token:graduations` for every pump.fun bond in real time (tracked deployer or not). Buyer-quality `breakdown` adds `dump_cluster_count` (out-of-sample: 3+ → 94% dump vs 61% base) + `recycled_early_buyer_count`. DEX firehose: replay buffer deepened to ~5 min; mint-scoped subs get in-band `dex:graduations` frames.

> **New in 1.10** — **Deshred Sniper.** `client.rest.sniper_recent()` — deshred deploy feed ~500ms before on-chain confirmation. PRO: elite/good. ULTRA: all tiers + `sniper_watchlist_add()`. Use `sniper:deploys` WebSocket for push.
>
> **New in 1.9** — **Price alerts, scout leaderboard, coordination history.** `client.rest.price_alerts_create()` (PRO=5, ULTRA=25). `scout_leaderboard()`, `kol_consensus()`, `peak_history()`, `coordination_history()`. `wallet_stats()` now returns `derived`: win_rate, roi, verdict, biggest_miss.
>
> **New in 1.8** — **Universal Wallet API.** `client.rest.wallet_stats()`, `wallet_pnl()`, `wallet_positions()`, `wallet_trades()` — FIFO cost-basis PnL for any Solana wallet. PRO+. Cache hits free.
>
> **New in 1.7.1** *(2026-05-13)* — Velocity field shape corrected to match the API: `mc_change_pct`, `volume_usd`, `mev_volume_pct` are top-level on the token response, each keyed by `'5m'`/`'15m'`/`'1h'`/`'2h'`/`'4h'`. The 1.7.0 README documented a `velocity[window]` shape that didn't match the wire format.
>
> **New in 1.7.0** *(2026-05-12)* — **Token directory + account inspection.** `client.rest.tokens_list(min_liq=10000, min_volume_1h_usd=5000, max_mev_share_pct=60, mc_change_1h_min_pct=20, sort="mc_desc", min_liq_mc_ratio=0.05, deployer_tier="elite")` filters every active mint by MC band, liquidity floor, primary DEX, authority/safety flags, computed 1h volume, MEV-share ceiling, MC-change deltas, liq/MC ratio, and deployer tier. Response items include `liquidity_to_mc_ratio` and `deployer_tier`. Default `min_liq=2000` skips phantom-MC dust; pass `min_liq=0` to opt out. `client.rest.me()` — read your tier, daily/burst quota state, and per-feature usage in one call (no header parsing). Velocity / MEV-share fields added to every token response: `mc_change_pct`, `volume_usd`, `mev_volume_pct` (each keyed by `'5m'`/`'15m'`/`'1h'`/`'2h'`/`'4h'`) plus `history_age_seconds`. `/token/{mint}` 400s now ship structured `code`, `reason`, `received_length`, `example`, and `docs`. Deprecated `avg_entry_mc_usd` fully removed.

## Quick start (10 seconds)

```bash
pip install madeonsol-x402
```

```python
from madeonsol_x402 import MadeOnSolClient
client = MadeOnSolClient(api_key="msk_...")  # free tier at https://madeonsol.com/pricing
trades = client.kol_feed(limit=5, action="buy")
```

## Authentication

Two options:

| Method | Parameter / Env var | Best for |
|---|---|---|
| **MadeOnSol API key** (recommended) | `api_key` / `MADEONSOL_API_KEY` | Developers — [get a free key](https://madeonsol.com/pricing) |
| x402 micropayments | `private_key` / `SVM_PRIVATE_KEY` | AI agents with Solana wallets |

> **x402 paid-result recovery (PAY-05): NOT supported by this package.** The `private_key` mode pays through the upstream `x402` Python client. It has no payment policy (SDK-01), no `PAYMENT-RECOVERY` signer and no bounded recovery of a paid call whose answer was lost or is still pending. It does not provide PAY-05-compatible automatic recovery, and calling again after a failed paid call creates a new payment. Until this package moves to the SDK-01 payment policy, use an `api_key`, or pay keyless through the TypeScript `madeonsol-x402`, which implements PAY-05 recovery.

> **v1.0 breaking change:** RapidAPI auth has been removed. The MadeOnSol RapidAPI marketplace was retired on 2026-04-19. If you were using `rapidapi_key=` or `RAPIDAPI_KEY`, get a free `msk_` key at [madeonsol.com/pricing](https://madeonsol.com/pricing).

## Install

```bash
pip install madeonsol-x402                    # core SDK
pip install madeonsol-x402[langchain]         # + LangChain tools
pip install madeonsol-x402[crewai]            # + CrewAI tools
```

> x402 dependencies are only needed when using `private_key` / `SVM_PRIVATE_KEY`.

## Quick Start

```python
from madeonsol_x402 import MadeOnSolClient

client = MadeOnSolClient(api_key="msk_your_api_key_here")

# Real-time KOL trades — each trade now includes
# market_cap_usd_at_trade and price_usd_at_trade (real-time MC at the
# moment the swap fired, sourced from our in-memory price tracker).
trades = client.kol_feed(limit=10, action="buy")
for t in trades["trades"]:
    print(f'{t["kol_name"]} bought {t["token_symbol"]} for {t["sol_amount"]:.2f} SOL @ MC ${t.get("market_cap_usd_at_trade") or "?"}')

# KOL convergence signals
signals = client.kol_coordination(period="24h", min_kols=3)

# KOL leaderboard — 180 days of history
leaders = client.kol_leaderboard(period="7d")  # today | 7d | 30d | 90d | 180d

# Deployer alerts (all tiers can filter by tier)
# Each alert carries deployer_sol_balance (float | None) — deployer wallet
# SOL balance at alert time (None for historical rows).
alerts = client.deployer_alerts(limit=10)
elite_only = client.deployer_alerts(limit=10, tier="elite")

# Token money-flow over a rolling window (PRO+) — sync
flow = client.token_flow("So11111111111111111111111111111111111111112", window="1h")
print(flow["net_sol"], flow["unique_buyers"], flow["unique_sellers"])

# Alpha wallet leaderboard (REST)
top = client.rest.alpha_leaderboard(period="30d", sort="win_rate")

# Wallet Tracker (REST)
client.rest.wallet_tracker_add("WALLET_ADDRESS", label="whale")
events = client.rest.wallet_tracker_trades(limit=50)

# Inspect rate-limit headers from the most recent REST call
print(client.rest.last_rate_limit)
# {'limit': 100, 'remaining': 92, 'reset': 1714000000, 'request_id': 'rid_abc123'}
```

### Token money-flow *(new in 1.17)*

`token_flow(mint, window="1h")` aggregates buy/sell pressure for a token over a rolling `'1h'` or `'24h'` window. PRO+ (keyed). Both sync and async variants are available:

```python
import asyncio
from madeonsol_x402 import MadeOnSolClient

client = MadeOnSolClient(api_key="msk_...")
mint = "So11111111111111111111111111111111111111112"

# Sync
flow = client.token_flow(mint, window="1h")
print(flow["net_sol"], flow["buy_sol"], flow["sell_sol"])

# Async
async def main():
    flow = await client.token_flow_async(mint, window="24h")
    print(flow["unique_wallets"], flow["trades_per_wallet"])

asyncio.run(main())
```

Response keys: `mint`, `window`, `from`, `unique_wallets`, `unique_buyers`, `unique_sellers`, `buy_count`, `sell_count`, `total_trades`, `buy_sol`, `sell_sol`, `net_sol`, `trades_per_wallet`.

## Real-time streaming *(new in 1.11)*

Managed WebSocket stream — auto-reconnect, token fetch (the token never expires; `get_stream_token()` is called on every (re)connect), and typed callbacks handled for you. Needs the `stream` extra: `pip install "madeonsol-x402[stream]"`.

```python
import asyncio
from madeonsol_x402 import MadeOnSolREST

client = MadeOnSolREST(api_key="msk_...")

async def main():
    stream = client.stream()

    @stream.on("kol:trade")
    async def on_trade(data, evt):
        print(data["token_symbol"], data["action"])

    stream.subscribe(["kol:trades", "deployer:alerts"])
    await stream.run()   # blocks; manages connection + reconnects

asyncio.run(main())
```

Channels: `kol:trades`, `kol:coordination`, `kol:first_touches`, `deployer:alerts`, `wallet_tracker:events`, `copytrade:signals`, `price_alert:events`, `sniper:deploys`, `token:graduations` (every pump.fun graduation in real time, tracked deployer or not), `token:prices` (event `token:price` — per-mint price/MC ticks; PRO+, REQUIRES `filters={"mints": [...]}` — PRO 25 / ULTRA 100 / BUSINESS 250 per connection; a state stream, never replayed), `token:locks` (event `token:lock` — every NEW Streamflow / Jupiter Lock / Bonfida lock or vesting contract, PRO+; LP locks not included), `token:fee_claims` (event `token:fee_claim` — every pump.fun fee event: distributions, social-handle claims, SharingConfig changes, PRO+; history starts 2026-08-17), `token:surges` (events `token:surge` — a token < 30 min old running ≥3× / ≥6× / ≥8× its launch MC, `tier` early / strong / breakout, each once per mint, sustained — and `token:revival` — ≥24 h with no trade candle, then confirmed buys on the tape, `tier` `None`; the same row as `rest.tokens_surges()` minus `outcome`, `risk_flags` included; subscribe filters `kinds`, `tiers`, `launchpads`, `exclude_flags`, `min_mc_usd` / `max_mc_usd`, `deployer_tier`; PRO+), `token:candles`, `token:risk`, `wallet:scores` (server 2026-09-23 — live 1-minute candles, risk-input changes and wallet-score changes; all PRO+ and scoped, see below). Lifecycle events: `open`, `close`, `reconnect`, `subscribed`, `heartbeat`, `warning`, `cursor`, `replay`, `gap`, `fatal`, `error`.

### Recovery: cursor, resume, de-duplication *(new in 1.31.0)*

The stream keeps a **resume cursor** `{"instance", "seq", "ts"}` — the position of the last frame your handlers finished — and on every reconnect asks the server to resume after it (`subscribe {…, "resume": …}`).

- **"Processed"** means the handler returned (sync) or was awaited to completion (async). Handlers run one frame at a time, so the cursor never passes a frame still being handled. A handler that raises still counts as processed; the exception goes to `error`.
- **At-least-once, never exactly-once.** After a reconnect a frame can arrive again. The client drops ids it delivered recently (the last 10,000, `dedupe_size=`); anything you persist should still dedupe on `evt["id"]`. Replayed frames carry `evt["replayed"] is True`.
- **Persistence.** The cursor lives in memory. Save `stream.get_cursor()` (or on every `cursor` event) and pass it back as `client.stream(resume=saved)` to continue after a process restart. Persist the committed cursor, never `get_progress()`.
- **Committed cursor vs progress.** `get_cursor()` is the COMMITTED, safe cursor — persist and resume from this one. `get_progress()` is what has been received and handled (replayed frames included) and is not safe to resume from. Live frames commit as they are handled. Replayed frames never commit: the server replays channel by channel, so only a `replay_end` the server calls complete — or one whose gaps are all final — commits, at the server's `last_seq` / `last_ts`. If a recovery is incomplete, or the socket closes mid-replay, the committed cursor stays at the pre-resume point, and live frames after it are delivered but not committed until a later recovery completes (`is_recovery_incomplete()`). **Trade-off:** the next reconnect re-requests the unrecovered range from the old cursor, and what arrives twice is dropped by id. Call `accept_gap()` once you have backfilled the range the `gap` event named, or decided to skip it. A gap the server calls final is handled by `on_unrecoverable_gap` (below).
- **Gaps: what is known, and who decides.** A `gap` event says which channels the server could not rebuild, the **range that may be incomplete** (`skipped["from"]` → `skipped["to"]`, plus `from`), the server's `reason`, whether it is `permanent`, and the bounds the server reported (`limits`, and per-channel `time_basis` / `truncated_at_ts` / `retry_after_ms` under `channels`). Events in that range **may** be missing — the number cannot be known, so it is never stated. Backfill the range from REST if you need certainty.
  - **Transient** (`retryable: True` — `backpressure`, `closed`, `source_busy`, `source_error`, `late_ingest_possible`, `row_cap`): the committed cursor stays put, live frames do not commit, and the client resumes again after the server's `retry_after_ms` (for `row_cap`, from `resume_ts_hint`), then on every reconnect. `resume_ts_hint` is used only when every incomplete **retryable** channel is `row_cap` (a channel whose gap is final does not block it, and is still reported); otherwise the retry asks from the committed cursor again. The client asks again after the server’s `retry_after_ms` at most `max_resume_retries` times per connection (default 5; the budget resets on every reconnect); when that budget is spent the gap event says `exhausted: True`, the cursor stays where it is, and the next reconnect resumes again.
  - **Final** (`retryable: False` — `not_reconstructable`, `window_exceeded`, and an older server's `ring_truncated` / `instance_changed`): asking again can never fill it. **The SDK then decides to continue** — that is the client's decision, not your approval — and reports it on the same `gap` event with `advanced_past_gap: True`, `source: "auto"` and the range being skipped, *before* the cursor moves. Pass `on_unrecoverable_gap="stop"` to keep the cursor instead: the stream stops and emits `fatal` with the gap, and you decide (`accept_gap()` then `run()` again continues; `accept_gap()` reports the same gap with `source: "manual"`).
- **Older servers.** Against a server that does not understand `resume` yet, the client falls back to `replay_since_seq` (same server process) or `replay_since_ts` (the server restarted). That only covers the server's in-memory buffer (minutes), and a restart is reported as a `gap` with `instance_changed`.
- **Close codes.** `4001` → the token is re-fetched and the client reconnects (`max_auth_retries=`, default 3, then `fatal` and `run()` returns); `4002` connection limit → `error` (`StreamConnectionLimitError`) plus a wait of at least 60 s (`connection_limit_backoff=`); `4003` → `fatal`, `run()` returns; `4008` slow consumer → reconnect and resume. The backoff resets only when the server acks a subscribe. `stream.last_close` keeps the last `(code, reason)`, and the `close` event carries `{"code", "reason"}`.
- **Warnings.** `warning` fires for every server warning frame, including `channels_rejected` and `channels_revoked` (revoked channels are removed from the subscription so reconnects do not re-request them); with no handler registered they go through `warnings.warn`.

```python
stream = client.stream(resume=load_cursor())   # None on first run

@stream.on("*")
async def persist(data, evt):
    await store.upsert(evt["id"], data)        # the cursor advances after this returns

stream.on("cursor", save_cursor)               # {"instance", "seq", "ts"}
stream.on("gap", lambda g: print("may be missing:", g["reasons"], g["skipped"]))  # g["advanced_past_gap"]
stream.on("fatal", lambda f: print("stream stopped:", f["code"], f["reason"]))
```

### Named subscriptions *(new in 1.32.0)*

One socket can hold several independent subscriptions, each with its own channels and filters; the server caps the total per connection, the default one included (PRO 5, ULTRA 10, BUSINESS 20). `subscribe(channels, filters)` stays the connection's `"default"` subscription and its wire is unchanged; `subscribe(channels, filters, sub_id="...")` opens a named one (1-64 characters of `A-Z a-z 0-9 _ . -`). A frame delivered under a named subscription carries `evt["sub_id"]`. **An event that matches several subscriptions is delivered once per matching subscription**, each copy stamped with its `sub_id`: the client dedupes per `(sub_id, id)`, so the same event can legitimately reach a handler twice, under two sub_ids. Filters of one subscription never affect another. `update_subscription(sub_id, filters)` REPLACES that subscription's filters (`"default"` addresses the plain one), `unsubscribe("my-sub")` / `unsubscribe(sub_id="my-sub")` removes it, `get_subscriptions()` is the local view and `await stream.list_subscriptions()` asks the server (`list` / `subscriptions`). Server refusals arrive as `warning` frames carrying the `sub_id` and one of `invalid_sub_id`, `too_many_subscriptions`, `unknown_sub_id`, `invalid_filters`, `channels_rejected`, `channels_revoked`, `replay_in_progress`; a subscription refused as `too_many_subscriptions` or `invalid_sub_id` is dropped locally so reconnects stop re-requesting it. Lifecycle events `updated` and `unsubscribed` surface the server acks.

**Resume with several subscriptions** is per subscription: on every reconnect each subscription is re-sent with the same cursor, the server serves one replay per subscription, one after another (`replay_start` … `replay_end` each carry the `sub_id`; live frames are held until the last one ends), and the cursor commits once ALL of them have ended, at the smallest `last_seq` / `last_ts` across them. The `replay` event lists `subscriptions` and the raw `ends` per subscription; a gap's `channels` entries are keyed `sub_id/channel` for named subscriptions, and an incomplete retryable replay is retried for those subscriptions only. Against an older server that ignores `sub_id`, the client emits `warning` `named_subscriptions_unsupported` once.

```python
stream = client.stream()
stream.subscribe(["kol:trades"], {"action": "buy", "min_sol": 1}, sub_id="kol-buys")
stream.subscribe(["deployer:alerts"], {"deployer_tier": ["elite"]}, sub_id="deploys")

@stream.on("kol:trade")
def on_event(data, evt):
    print(evt.get("sub_id"), data)   # "kol-buys"

stream.update_subscription("kol-buys", {"action": "buy", "min_sol": 5})
stream.unsubscribe("deploys")
await stream.run()
```

### Lock lifecycle on `token:locks` *(server 2026-09-23)*

Add `"lifecycle": True` to a `token:locks` subscription's filters to also receive what happens to a lock after it is created: `token:lock_claimed`, `token:lock_cancelled`, `token:lock_closed`, `token:lock_updated` (`change`: `topup` / `extended` / `schedule_changed` / `recipient_changed`, one event per change), `token:unlock_upcoming` (the lock's next unlock is within 24 h) and `token:unlock_available` (the unlock passed within the last 30 min — **claimable per the schedule, not claimed**). Without `lifecycle` the channel is unchanged. Optional filters: `events`, `unlock_kinds` (`cliff` / `period` / `final` / `tranche`), `mints` (≤ 500) and `include_automatic_claims` (default `False`: Streamflow keeper-cranked withdrawals, about 90 % of all claims, are hidden unless set). Payload TypedDicts live in `madeonsol_x402.stream` (`TokenLockClaimedEvent`, `TokenUnlockScheduleEvent`, …). Raw string amounts, no USD field; server-side dedupe, and no events for history.

```python
stream = client.stream()
stream.subscribe(["token:locks"], {"lifecycle": True, "events": ["token:lock_claimed", "token:unlock_available"], "mints": [MINT]}, sub_id="locks")

@stream.on("token:unlock_available")
def on_unlock(data, evt):
    print(data["unlock_kind"], data["amount_raw"], "claimable, not claimed")

await stream.run()
```

### Candles, risk inputs and wallet scores *(server 2026-09-23)*

Three PRO+ channels, all **scoped** (per-connection cap PRO 25 / ULTRA 100 / BUSINESS 250 across named subscriptions; over the cap or without a scope the channel is rejected, never truncated). `token:candles` needs `"mints"` (a budget separate from `token:prices`): `candle:closed` is the stored 1-minute row (resumes durably; flat zero-trade minutes are skipped) and `"updates": True` adds `candle:update`, the in-progress minute (≤ 1 per mint per second, a state stream, never replayed). `token:risk` needs `"mints"`: `risk:authority_changed` (mint / freeze authority revoked, transfer fee changed), `risk:supply_inflated` (first 0.5 % / 5 % supply-drift crossing) and a `risk:inputs` snapshot per mint (`snapshot=True`) unless `"risk_snapshot": False`; optional `"risk_events"`. `wallet:scores` needs `"wallets"` (base58): `deployer:tier_changed` and `kol:score_state_changed`, each with `computed_at` + `source` ("recomputed at T", not "changed at T"); optional `"score_events"`. Payload TypedDicts in `madeonsol_x402.stream` (`TokenCandleClosedEvent`, `TokenRiskAuthorityChangedEvent`, `DeployerTierChangedEvent`, …).

```python
stream = client.stream()
stream.subscribe(["token:candles"], {"mints": [MINT], "updates": True}, sub_id="candles")
stream.subscribe(["token:risk"], {"mints": [MINT]}, sub_id="risk")
stream.subscribe(["wallet:scores"], {"wallets": [DEPLOYER]}, sub_id="scores")

@stream.on("candle:closed")
def on_candle(data, evt):
    print(data["bucket_start"], data["open_price_usd"], data["close_price_usd"], data["volume_usd"])

@stream.on("risk:authority_changed")
def on_risk(data, evt):
    print(data["mint"], data["field"], data["before"], "->", data["after"])

@stream.on("deployer:tier_changed")
def on_tier(data, evt):
    print(data["wallet"], data["tier_before"], "->", data["tier_after"], data["source"])

await stream.run()
```

## LangChain

```python
from madeonsol_x402.langchain_tools import ALL_TOOLS

# Set MADEONSOL_API_KEY or SVM_PRIVATE_KEY env var
agent = create_react_agent(llm, tools=ALL_TOOLS)
```

## CrewAI

```python
from madeonsol_x402.crewai_tools import ALL_TOOLS

agent = Agent(role="Solana Analyst", tools=ALL_TOOLS)
```

## Endpoints

### KOL Intelligence (x402-priced — also reachable via `msk_` API key)

| Method | Description |
|---|---|
| `kol_feed()` | Real-time KOL trade feed (1,000+ wallets) |
| `kol_coordination()` | Multi-KOL convergence signals |
| `kol_leaderboard()` | PnL and win rate rankings — windows: today, 7d, 30d, 90d, 180d (180-day retention) |
| `kol_pairs()` | KOL affinity matrix — which KOLs co-trade the same tokens |
| `kol_hot_tokens()` | KOL momentum tokens — accelerating buy interest |
| `kol_trending_tokens()` | Tokens ranked by KOL buy volume |
| `kol_token_entry_order(mint)` | Ranked KOL first-buyer order for a token |
| `kol_compare_wallets(wallets)` | Side-by-side comparison of 2–5 KOL wallets |
| `kol_alerts_recent()` | Live KOL alert feed — clusters, fresh-token buys, heating-up |
| `deployer_alerts()` | Pump.fun deployer launches with KOL enrichment |
| `wallet_stats(address)` | **New 1.8** · Universal wallet stats (90d) + cross-product flags. $0.005 |
| `wallet_pnl(address)` | **New 1.8** · FIFO cost-basis PnL: realized + unrealized, profit factor, drawdown, daily curve, closed + open positions. $0.02 |
| `wallet_positions(address)` | **New 1.8** · Open positions with live unrealized from market-cap tracker. Shares /pnl cache. $0.01 |
| `wallet_trades(address, ...)` | **New 1.8** · Cursor-paginated raw trades with action / token / since-until filters. $0.005 |
| `token(mint)` | **New 1.16** · Live token snapshot — price_usd/price_sol, market_cap, fdv_usd, liquidity_usd, liquidity_to_mc_ratio, primary_dex, is_token_2022, transfer_fee_bps, top_buyers |
| `signal_performance(name, history=False)` | **New 1.16** · Signal Scorecard — out-of-sample hit_rate/base_rate/lift/sample_n per signal (dump_cluster_count, runner_rate, recycled_early_buyer_count, coordination_count) |
| `signals()` | **New 1.16** · Free — signal catalog with per-signal methodology and performance_endpoint |
| `token_flow(mint, window="1h")` | **New 1.17** · PRO+ · Token money-flow over a rolling 1h/24h window — unique wallets/buyers/sellers, buy/sell counts + SOL, net SOL flow, trades per wallet. Async: `token_flow_async(mint, window="1h")` |
| `token_candles(mint, tf=, limit=, from_=, to=)` | **New 1.22** · OHLCV candles, keyless (PRO slice: 30d). $0.01 |
| `almost_bonded(**filters)` | **New 1.22** · Pre-bond pump.fun tokens near graduation, ranked by velocity. $0.01 |
| `token_top_traders(mint, limit=, sort=, window_days=, min_bought_sol=)` | **New 1.22** · Wallets ranked by realized PnL/ROI on a token, enriched with KOL/alpha identity. $0.02 |
| `token_cap_table(mint)` | **New 1.22** · Early-buyer cap table with PnL/exit/bundle/KOL flags. $0.02 |
| `sniper_recent(since=, deployer_tier=, min_bond_rate=, limit=)` | Retired in keyless mode; raises `KeylessNotAvailableError` before the request. Use an ULTRA/BUSINESS/ENTERPRISE API key. |
| `deployer_trajectory(wallet)` | **New 1.22** · Deployer skill curve — streaks, rolling bond rate, trend. $0.01 |
| `discovery()` | Free — list the current keyless endpoints and prices |

**API key only (not on the x402 rail):** `scout_leaderboard()`, `coordination_history()`, `kol_consensus()` and `peak_history()` have no keyless x402 route (the server answers 404). With an `api_key` they call `/api/v1/` as before; in private-key (x402) mode they raise `KeylessNotAvailableError` before any request or payment. The list is exported as `X402_UNAVAILABLE_PATHS`. `discovery()` returns the live x402 catalog.

### REST API — KOL/deployer detail

| Method | Description |
|---|---|
| `rest.kol_pnl(wallet, period=)` | Deep per-wallet PnL: equity curve, risk metrics, closed positions. ULTRA adds open positions (tokens bought but not yet sold). |
| `rest.kol_timing(wallet, period=)` | KOL entry/exit timing profile — available on all tiers |
| `rest.deployer_trajectory(wallet)` | Deployer skill curve — streaks, rolling bond rate, trend — available on all tiers |
| `rest.deployer_history(wallet, limit=90)` | **New 1.20** | Daily reputation time-series — backtest "was this deployer elite when it launched token X?" without look-ahead bias. `snapshots` array of per-day `tier`/`bonding_rate`/`avg_peak_mc`. `limit` 1–365 |

### Alpha Wallet Intelligence

Scored from 1.5M+ early-buyer records (wallets seen in the first 20 buyers of Pump.fun tokens).

| Method | Tier | Description |
|---|---|---|
| `rest.alpha_leaderboard(period=, min_tokens=, sort=, exclude_bots=)` | All | Up to 100 results on Free/Pro; ULTRA unlocks 500 + bot signals |
| `rest.alpha_wallet(wallet)` | ULTRA | Full per-token breakdown + bot_signals array |
| `rest.alpha_linked(wallet)` | ULTRA | Wallets behaviorally linked (co-bought 3+ tokens within 2s) |

### Token Quality

| Method | Tier | Description |
|---|---|---|
| `rest.token_cap_table(mint)` | PRO+ | First non-deployer early buyers, enriched with PnL/KOL/bot flags. PRO=10, ULTRA=20 |
| `rest.token_buyer_quality(mint)` | All | 0–100 buyer-quality score + full breakdown (5-min cached) |
| `rest.token_risk(mint)` | PRO+ | Transparent 0–100 risk score (evidence, not a verdict) with `band`, explainable `factors`, and raw `inputs` |
| `rest.token_bundle(mint)` | **New 1.20** · All | Bundle-cohort holdings — `bundle` summary with `held_pct_of_supply` (headline), `bundle_kind`, `fully_exited`. BASIC=summary only, PRO=+top-10 wallet flags, ULTRA=+identity |
| `rest.tokens_batch_risk(mints)` | **New 1.19** · PRO+ | Bulk risk scoring for 1–50 mints in one call (1 request). Each entry mirrors `token_risk` + `as_of`; untracked mints → `{mint, error: "not_tracked"}` |
| `rest.token_candles(mint, tf, limit, from_, to)` | PRO+ | 1-minute-derived OHLCV candles by timeframe. PRO=OHLCV/30d, ULTRA=+net flow/full history |
| `rest.token_trades(mint, limit=, cursor=, action=, wallet=, since=, until=)` | **New 1.22** · PRO+ | Mint-scoped trade tape — cursor-paginated raw trades, default FULL history (capture starts 2026-04-12, pump.fun-pipeline scoped; see the `coverage` block) |
| `rest.token_pools(mint)` | **New 1.20** · PRO+ | Per-venue liquidity map — every DEX pool a token trades in, live vs parked. `pools` array + `summary` with `total_liquidity_usd`, `primary_dex`, `top_pool_share_pct` |
| `rest.token_holders(mint)` | **New** · PRO+ | Live holder census + concentration — who holds NOW. `concentration.holder_count` is EXACT (mint-scoped `getProgramAccounts` census; `None` only when the provider refuses a mega-cap → top-20 fallback with `source.census_fallback_reason`, never trade-estimated). Each disclosed owner labelled `deployer`/`kol`/`early_buyer`/`bundle`/`bot`/`dump_cluster`; pools / bonding curves / burns EXCLUDED from the circulating denominator and NAMED in `excluded[]` (`pool`+`dex`+`pool_address` \| `bonding_curve` \| `burn` \| `program_account`). Amounts are raw u64 STRINGS. Disclosure PRO=10, ULTRA=50, BUSINESS=100. Big tokens: first call may be HTTP 503 `holder_scan_in_progress` (`retry_after_seconds: 20`) — scan continues + cached, retry is instant. Exposed as `madeonsol_token_holders` (LangChain) / "MadeOnSol Token Holders" (CrewAI). Key-mode only (not on the x402 rail) |
| `rest.token_locks(mint, status=, program=, limit=)` | **New 1.27** · PRO+ | Token locks & vesting on a mint — every Streamflow / Jupiter Lock / Bonfida vesting contract: schedule (start / cliff / period / end), terms (`cancelable_by_sender` / `_recipient`, `transferable`, `can_topup`), live-derived `locked_raw` / `unlocked` / `withdrawn` / `claimable` / `status` / `next_unlock`, and a `summary` (exact `lock_count`, `distinct_lockers`, locked / deposited raw + ui + usd + % of supply, `unlocking_7d_*` / `unlocking_30d_*`, `active_cancelable_by_sender`). Base-unit amounts are STRINGS; ui/usd/pct `None` when unknown. **LP locks NOT included.** Key-mode only |
| `rest.token_locks_feed(since=, before=, mint=, sender=, recipient=, program=, kind=, status=, min_usd=, min_pct_of_supply=, include_estimated=, limit=)` | **New 1.27** · PRO+ | Cross-token feed of NEW lock / vesting contracts, newest first (+ per-row `token` block). Cursors `pagination.next_since` / `next_before`; pushed live on WS `token:locks`. Backfilled Jupiter rows excluded unless `include_estimated=True`. Key-mode only |
| `rest.token_unlocks(within=, mint=, program=, kind=, min_usd=, min_pct_of_supply=, sort=, limit=)` | **New 1.27** · PRO+ | Upcoming unlock EVENTS (`cliff` / `period` / `final` / `tranche`) across all active contracts inside `within=1h…90d` (default `7d`): `unlock_at`, `in_seconds`, `amount_*`, `window_amount_*` (total release over the window), `token`, `lock`. `sort=soonest|largest_usd|largest_pct`. Key-mode only |
| `rest.token_fee_shares(mint)` | **New 1.27** · PRO+ | pump.fun creator-fee `SharingConfig` on a mint: `shareholders[]` with `share_bps`, `is_admin`, `is_social_pda` (+ `social.platform` 2 = X, `social.user_id` numeric id, lifetime claimed), `redirected_bps`, `social_bps`, `is_default` (`True` = 100% to creator), `config.source` `stream`\|`chain`; `distributions` rollup (per-recipient received, `past_recipients`), `history`, `recent_distributions`. Quote base-unit amounts are STRINGS. Fee history starts 2026-08-17. Key-mode only |
| `rest.token_fee_claims(type=, mint=, recipient=, actor=, social_platform=, social_user_id=, min_sol=, since=, before=, limit=)` | **New 1.27** · PRO+ | pump.fun fee-event feed, newest first: `distribution` (+ `payouts[]`), `social_claim`, `shares_created` / `shares_updated` / `shares_reset`, `creator_transferred`; `creator_claim` only when requested via `type=`. Cursor `pagination.next_since`; pushed live on WS `token:fee_claims`. History starts 2026-08-17. Key-mode only |
| `rest.tokens_surges(kind=, tier=, mint=, since=, before=, min_mc_usd=, max_mc_usd=, min_buys=, launchpad=, deployer_tier=, exclude_flags=, only_clean=, stats=, days=, limit=)` | **New 1.28** · PRO+ | Token momentum fires, newest first — `kind='surge'` (token < 30 min old vs its LAUNCH MC; `tier` `early` ≤10 min ≥$12k ≥3× · `strong` ≤30 min ≥$30k ≥6× and ≥2× the 3-min low · `breakout` ≤2 min ≥$45k ≥8×; each once per mint, sustained ≥10 s) or `'revival'` (no trade candle ≥24 h, then ≥5 buys / ≥$500 buy volume / ≥1.5× the pre-dormancy MC on the tape — never a price mark; `tier` `None`). Each row: burst `tape` (`unique_buyers` `None` outside trade coverage), `kol`, `early_buyers` (bundled / sold / sniper), `deployer`, `risk_flags`, and `outcome` (+1 h MC / peak / low) once ≥65 min old. `stats=True` = per-(kind, tier) hit-rates over `days`. `exclude_flags` drops rows carrying ANY listed flag; `only_clean=True` = no flags. Cursors `pagination.next_since` / `next_before`; pushed live on WS `token:surges`. Retention 60 d. Exposed as `madeonsol_tokens_surges` (LangChain) / "MadeOnSol Token Surges" (CrewAI). Key-mode only |

### Early sniper observations (ULTRA/BUSINESS/ENTERPRISE)

Receive early deploy instruction observations with an ULTRA, BUSINESS or ENTERPRISE API key. They may fail or remain unconfirmed. Use action identity and separate execution status. Live push is available through the documented sniper WebSocket/webhook routes; no fixed head start is guaranteed.

| Method | Tier | Description |
|---|---|---|
| `rest.sniper_recent(limit=, deployer_tier=, min_bond_rate=, since=, watchlist=)` | ULTRA/BUSINESS/ENTERPRISE | Early deploy observations with action identity and separate execution status; unknown enrichment stays null. |
| `rest.sniper_by_deployer(wallet, limit=)` | ULTRA | Deshred deploys for one deployer |
| `rest.sniper_watchlist()` | ULTRA | List your custom deployer watchlist (max 50) |
| `rest.sniper_watchlist_add(wallet=/wallets=, label=)` | ULTRA | Add one or many deployers |
| `rest.sniper_watchlist_remove(wallet)` | ULTRA | Remove a deployer |

```python
feed = client.rest.sniper_recent(limit=50, min_bond_rate=0.5)
client.rest.sniper_watchlist_add(wallets=["7dEx...4pQ8", "9aBc...2zZ1"], label="alpha devs")
tracked = client.rest.sniper_recent(watchlist=True)  # ULTRA — only your tracked deployers
```

### KOL Coordination Alerts (v1.1 — push signals)

Real-time push alerts when a cluster of KOLs co-buys the same token. Fires within ~1s of the triggering trade (pg_notify push, not polling). Delivered via WebSocket (`kol:coordination` channel, user-scoped) and/or HMAC-signed webhook. PRO=5 rules, ULTRA=20.

```python
res = client.rest.coordination_alerts_create(
    name="fresh pump cluster",
    min_kols=4,
    window_minutes=15,     # peak-density window (1-60)
    min_score=70,          # 0-100 composite score cutoff
    include_majors=False,  # filter WIF/BONK/POPCAT
    cooldown_min=60,       # one fire per (rule, token) per 60min...
    score_jump_break=10,   # ...unless score jumps +10 vs last fire
    delivery_mode="both",
    webhook_url="https://you.com/hooks/coord",
)
# store res["webhook_secret"] — shown ONCE
```

`coordination_alerts_list()`, `coordination_alerts_get(id)`, `coordination_alerts_update(id, **fields)`, `coordination_alerts_delete(id)`.

**Webhook signature:** `X-MadeOnSol-Signature` is the lowercase hex HMAC-SHA256 (no prefix) of `` `${X-MadeOnSol-Timestamp}.${rawBody}` ``, keyed with `webhook_secret` used as-is (do not hex-decode it). `X-MadeOnSol-Timestamp` is unix **milliseconds** and is regenerated on every retry. Compute the HMAC over the exact raw request bytes, compare in constant time over equal-length buffers (validate the header as 64 hex characters first), and reject stale deliveries (for example older than 5 minutes) on your side: the server does not enforce a replay window. Full Node.js and Python verifiers: https://madeonsol.com/api-docs#webhook-payload

**The `kol_coordination()` response** now includes v1.1 fields: `peak_window_start/end`, `peak_kols`, `peak_buys` (the busiest slice within the period), `exited_count` + per-KOL `exited` (net-flow-negative wallets), and `coordination_score` (0-100). Pass `min_score=`, `window_minutes=`, `include_majors=` to filter.

### KOL First-Touch Signal *(new in 1.3)*

Every "first KOL buy on a token mint" event — when a tracked KOL is the first of the cohort to touch a token. Filterable by **scout tier** (S/A/B/C from `mv_kol_scout_score`), KOL winrate, token age, mint suffix.

**Backtest:** S-tier scouts attract ≥3 follow-on KOLs within 4h ~50% of the time vs ~14% baseline (38d / 491k buys / 72,549 events). Public leaderboard at [madeonsol.com/kol/scouts](https://madeonsol.com/kol/scouts).

```python
# REST query — S-tier scouts on tokens younger than 1h
events = client.rest.first_touches(preset="scout", min_scout_tier="S", limit=20)
for e in events["events"]:
    fk = e["first_kol"]
    print(fk["name"], "scouted", e["token_symbol"], f"(scout_score={fk['scout_score']}%)")

# Webhook subscription (Ultra) — HMAC-signed push
res = client.rest.first_touch_subscriptions_create(
    name="S-tier scouts on pump tokens",
    filters={"min_scout_tier": "S", "mint_suffix": "pump"},
    delivery_mode="webhook",
    webhook_url="https://you.com/hooks/scout",
)
# store res["webhook_secret"] — shown ONCE
```

CRUD: `first_touch_subscriptions_list()`, `first_touch_subscriptions_get(id)`, `first_touch_subscriptions_update(id, **fields)`, `first_touch_subscriptions_delete(id)`. ULTRA only — up to 10 active.

> **Don't poll — push.** Median lead time before the second KOL is **12 seconds**. WebSocket channel: `kol:first_touches` (PRO+).

### Price Alerts *(new in 1.9)*

CRUD for token dip/recovery price alerts. Fires via WebSocket (`price_alert:events` channel) and/or HMAC-signed webhook when a token's market cap crosses your threshold. PRO=5 rules, ULTRA=25.

```python
res = client.rest.price_alerts_create(
    name="SOL dip buy",
    token_mint="So11111111111111111111111111111111111111112",
    condition="below",          # "below" | "above"
    threshold_mc_usd=5_000_000_000,
    cooldown_min=120,
    delivery_mode="both",
    webhook_url="https://you.com/hooks/price",
)
# store res["webhook_secret"] — shown ONCE
```

`price_alerts_list()`, `price_alerts_get(id)`, `price_alerts_update(id, **fields)`, `price_alerts_delete(id)`.

LangChain: `MadeOnSolPriceAlertsListTool`, `MadeOnSolPriceAlertsCreateTool`. CrewAI: same names via `ALL_TOOLS`.

### Scout Leaderboard & KOL Consensus *(new in 1.9)*

| Method | Tier | Description |
|---|---|---|
| `rest.scout_leaderboard(period=, limit=)` | PRO+ | Top scout-tier KOLs ranked by first-touch follow-on rate, win rate, and ROI |
| `rest.kol_consensus(min_kols=, period=)` | PRO+ | Tokens with the strongest KOL agreement signal — weighted by scout score and recent PnL |
| `rest.peak_history(mint)` | PRO+ | Historical peak-density windows for a token — every coordination spike with KOL breakdown |
| `rest.almost_bonded(min_progress=, min_velocity_pct_per_min=, deployer_tier=, sort=, limit=)` | **New 1.18** · PRO+ | Pre-bond pump.fun tokens near graduation, ranked by velocity (Δprogress/min) — progress_pct, velocity_pct_per_min, eta_minutes, stalled, deployer_tier |
| `rest.coordination_history(period=, limit=)` | PRO+ | Global coordination event log with token, KOL count, score, and outcome |

```python
leaders = client.rest.scout_leaderboard(period="30d", limit=25)
consensus = client.rest.kol_consensus(min_kols=5, period="24h")
```

### Wallet Derived Stats *(new in 1.9)*

`wallet_stats(address)` now includes a `stats` object with derived fields computed from the 90-day trade window:

```python
data = client.rest.wallet_stats("WALLET_ADDRESS")
s = data["stats"]
# s["win_rate"]      — fraction 0-1, tokens sold above cost basis
# s["roi"]           — aggregate return on invested SOL
# s["verdict"]       — "strong" | "profitable" | "neutral" | "losing"
# s["biggest_miss"]  — token with the highest post-exit gain the wallet missed
```

### Copy-Trade Rules (PRO+)

Server-side rules that fire signals when one of your source wallets trades. Delivered via webhook (HMAC-signed) and/or WebSocket. Limits: PRO 3 rules × 5 source wallets each, ULTRA 20 × 50, BUSINESS 100 × 250 (Enterprise follows Business). The server enforces them per tier. Any valid Solana wallet can be a source, KOL or not (server behaviour since 2026-10-04; KOL membership is optional enrichment and copy-trade sources do not use Wallet Tracker quota). Each rule reports `source_admission` (`any_wallet`) and `operational_state` (`eligible`, or an infrastructure state: `monitoring_pending`, `monitoring_unavailable`, `source_capacity_unavailable`). A source trade older than 10 s by chain time is not executed, and each trade produces at most one signal per rule. `only_action` defaults to `"buy"` when omitted. `min_mc_usd` / `max_mc_usd` restrict a rule to source trades inside a market-cap band (USD); when a bound is set, trades with an unknown market cap are dropped.

| Method | Description |
|---|---|
| `rest.copy_trade_list()` | List your rules |
| `rest.copy_trade_create(source_wallets, sizing_amount, ..., min_mc_usd=, max_mc_usd=)` | Create a rule. Returns `webhook_secret` **once** — store it |
| `rest.copy_trade_get(id)` | Get one rule |
| `rest.copy_trade_update(id, **fields)` | Update fields or toggle `is_active`; `min_mc_usd=None` / `max_mc_usd=None` clears a bound |
| `rest.copy_trade_delete(id)` | Delete permanently |
| `rest.copy_trade_signals(subscription_id=, since=, limit=, min_mc_usd=, max_mc_usd=)` | Recent fired signals (up to 7 days, 1–500) |

### Wallet Tracker

| Method | Description |
|---|---|
| `rest.wallet_tracker_watchlist()` | List tracked wallets and remaining capacity (Pro: 50, Ultra: 100, Business: 500; the Free tier has no wallet tracker) |
| `rest.wallet_tracker_add(wallet_address, label=)` | Add wallet to watchlist |
| `rest.wallet_tracker_remove(wallet_address)` | Remove wallet from watchlist |
| `rest.wallet_tracker_update_label(wallet_address, label)` | Update wallet label |
| `rest.wallet_tracker_trades(wallet=, action=, event_type=, limit=, order=, before_slot=, before=)` | Historical swap/transfer events (120-day retention). Returns `events` + `next_cursor` / `next_cursor_slot`. `action` is `"buy"` or `"sell"` (swaps only; transfers have `action` None, select them with `event_type="transfer"`). `order="slot"` (default) pages with `before_slot`; `order="block_time"` with the legacy `before` |
| `rest.wallet_tracker_summary(period=, wallet=)` | Per-wallet stats: swap counts, SOL bought/sold, last event |

### Universal Wallet API *(new in 1.8)*

Per-wallet endpoints that work on **any** Solana wallet, not just curated KOLs. FIFO cost-basis PnL over the last 90 days. PRO+ on every endpoint. Cache hits don't count against your daily quota.

| Method | Description |
|---|---|
| `rest.wallet_stats(address)` | Aggregate stats over 90d + cross-product flags (is_kol + kol_name, is_alpha_tracked + bot_confidence `'none'`/`'low'`/`'medium'`/`'high'`, is_deployer + tokens_deployed; **new 1.22:** is_sniper / is_bundler / is_dumper + `dump_cluster`) |
| `rest.wallet_batch_classify(wallets)` | **New 1.22** · Bulk reputation flags for 1–100 wallets in one call — is_sniper/is_bundler/is_dumper/is_kol, bot_confidence, dump_cluster. Pump.fun-pipeline scoped: `False` = not observed, not verified clean |
| `rest.wallet_pnl(address)` | Full FIFO cost-basis PnL: realized + unrealized SOL, profit factor, max drawdown, avg + median hold minutes, daily UTC PnL curve, closed positions sorted by pnl desc, open positions with live unrealized from mc-tracker |
| `rest.wallet_positions(address)` | Open lots only — shares /pnl cache, lighter response |
| `rest.wallet_trades(address, limit=, cursor=, action=, token_mint=, since=, until=)` | Cursor-paginated raw trades. Default window: last 90 days. limit 1-500 |

**Cost-basis honesty**: observable only inside the 90-day data window. Overflow sells (no matching buy in window) are silently discarded rather than fabricated. `notes.cost_basis_observable_from` makes the cutoff visible per call.

### Webhooks + Streaming

| Method | Description |
|---|---|
| `rest.create_webhook(url, events, filters=)` | Register webhook. Returns `secret` once — store it for HMAC verification |
| `rest.list_webhooks()` | List your webhooks |
| `rest.get_webhook(id)` | Get one + recent delivery log |
| `rest.update_webhook(id, **kwargs)` | Update URL, events, filters, or re-enable |
| `rest.delete_webhook(id)` | Delete permanently |
| `rest.test_webhook(id, event=)` | Send test payload; `event` picks which subscribed event to sample (default: the first). Newer servers echo the sampled `event` |
| `rest.get_stream_token(rotate=False)` | Issue your WebSocket streaming token (returns `ws_url` + `dex_ws_url`). **Never expires** (1.27.1) — same token on every call; `rotate=True` replaces it (old value works 60 s more). `expires_at` / `next_refresh_at` are always `None` |
| `rest.stream_sessions()` | **New 1.19** · PRO+ · List your live WebSocket sessions (`id`, `service`, `tier`, `channels`, `connected_at`, `remote_ip`, `messages_sent`) across both stream services |
| `rest.kill_stream_session(id)` | **New 1.19** · PRO+ · Force-terminate one of your sessions by `id` and free its slot — self-serve fix for a 4002 connection-limit lockout |

### Rate-limit headers

Every successful REST response captures rate-limit headers in `rest.last_rate_limit`:

```python
client.rest.alpha_leaderboard()
rl = client.rest.last_rate_limit
# {'limit': 100, 'remaining': 92, 'reset': 1714000000, 'request_id': 'rid_abc123'}
if rl['remaining'] is not None and rl['remaining'] < 5:
    print(f"Throttle warning — {rl['remaining']}/{rl['limit']} requests left")
```

### DEX Firehose (Ultra) — WebSocket

`rest.get_stream_token()` returns `dex_ws_url` (Ultra only). Connect with any WebSocket client (`websockets`, `websocket-client`, etc.) and use the multi-subscription protocol — up to **10 named subs per connection**, each with its own `sub_id`, server-side filters, and optional replay (up to 500 most recent matching trades) from a server-side buffer holding ~5 minutes of firehose history — it backfills trades from before your connection existed. Replayed trades arrive newest-first flagged `"replay": true`, then a `replay_done` frame; sort by `block_time` client-side.

```python
import asyncio, json, websockets
from madeonsol_x402 import MadeOnSolClient

client = MadeOnSolClient(api_key="msk_...")

async def main():
    token = client.rest.get_stream_token()  # {"token", "ws_url", "dex_ws_url", ...}

    # token MUST be appended as query param
    async with websockets.connect(f"{token['dex_ws_url']}?token={token['token']}") as ws:
        await ws.send(json.dumps({
            "type": "subscribe",
            "sub_id": "fresh-pumpfun",
            "replay": 50,                       # up to 500 from ring buffer
            "filters": {
                "dex": "pumpfun",
                "token_age_max_seconds": 300,
                "min_sol": 0.5,
                "action": "buy",
            },
        }))

        async for raw in ws:
            msg = json.loads(raw)
            if msg.get("channel") == "dex:trades":
                d = msg["data"]
                print(msg["sub_id"], d["dex"], d["action"], d["sol_amount"])

asyncio.run(main())
```

**Operations** (all carry `sub_id`): `subscribe`, `update` (replace filters in place), `unsubscribe`, `list`, `ping`. **Filters:** `token_mint(s)` (≤50), `wallet(s)` (≤50), `dex` (`pumpfun` | `pumpswap` | `raydium` | `jupiter` | `orca` | `meteora` | `launchlab` | `moonshot`), `program`, `deployer_tier`, `token_age_max_seconds`, `market_cap_min/max_sol`, `min_sol`, `max_sol`, `action`. At least one targeting filter is required. Inbound rate limit: 5 messages/sec.

**Liquidity events (server 2026-09-23).** Add `"liquidity": True` (in addition to trades) or `"liquidity": "only"` to a subscribe for `dex:liquidity` frames — one per liquidity instruction (`action` `pool_created` / `add` / `remove`), `id` = `<signature>:<ix>[.<inner>]`, with `pool`, `mints` (raw `amount_raw`, `side` in/out), `reserves_before` / `reserves_after`, `share_of_reserves` (constant-product pools only), `material` (a removal of ≥ 25 % of reserves) and `depth_effect`. Extra filters `pool(s)`, `actions`, `min_share_of_reserves`, `material_only`. Only fixture-verified instructions are emitted; concentrated pools (CLMM, Whirlpool, DLMM) report share/depth as unknown; DAMM v1, LaunchLab and Moonshot are not emitted; no USD field (`min_usd` is rejected); ring replay only.

```python
await ws.send(json.dumps({
    "type": "subscribe",
    "sub_id": "lp-pulls",
    "liquidity": "only",
    "filters": {"dex": ["raydium", "pumpswap", "meteora"], "actions": ["remove"], "material_only": True},
}))
# -> {"channel": "dex:liquidity", "sub_id": "lp-pulls", "id": ..., "data": {"action", "pool", "mints", "share_of_reserves", "material", ...}}
```

Full protocol reference: [madeonsol.com/api-docs#streaming](https://madeonsol.com/api-docs#streaming).

## Tiers

| Tier | Price | Wallets tracked | Requests/day |
|------|-------|-----------------|--------------|
| BASIC (free) | $0 | 10 | 200 |
| PRO | €43/mo (€430/yr) ≈ $49 | 50 | 10,000 |
| ULTRA | €131/mo (€1310/yr) ≈ $149 | 100 + WS events | 100,000 |
| BUSINESS | €400/mo (€4000/yr) ≈ $449 | 500 + WS events | 500,000 |

Free tier returns the full REST response shape on 40+ endpoints — real wallets, TX signatures, full precision — with live feeds delayed 5 minutes (delayed responses carry `delay`/`as_of` and an `X-Data-Delay` header). Paid tiers are real-time and unlock webhooks, WebSockets, rule engines, and ULTRA-only data depth; x402 pay-per-call is always real-time. Get a key at [madeonsol.com/pricing](https://madeonsol.com/pricing).

## Also Available

| Platform | Package |
|---|---|
| TypeScript SDK | [`madeonsol`](https://www.npmjs.com/package/madeonsol) on npm |
| Rust SDK | [`madeonsol`](https://crates.io/crates/madeonsol) on crates.io |
| MCP Server (Claude, Cursor) | [`mcp-server-madeonsol`](https://www.npmjs.com/package/mcp-server-madeonsol) · [Smithery](https://smithery.ai/servers/madeonsol/solana-kol-intelligence) · [Glama](https://glama.ai/mcp/servers/madeonsol/mcp-server-madeonsol) |
| ElizaOS | [`@madeonsol/plugin-madeonsol`](https://www.npmjs.com/package/@madeonsol/plugin-madeonsol) |
| Solana Agent Kit | [`solana-agent-kit-plugin-madeonsol`](https://www.npmjs.com/package/solana-agent-kit-plugin-madeonsol) |

## License

MIT

---

## Agent Intelligence Gateway compatibility (coordinated release)

**Scope:** Python keyed and x402 client — Solana.

MadeOnSol's [Agent Intelligence Gateway](https://madeonsol.com/agent-gateway) is
a separate six-action, read-only research layer: `discover_opportunities`,
`evaluate_token`, `inspect_wallet`, `inspect_deployer`, `evaluate_signal`,
and `changes_since`. These actions must not be inferred from a package's
existing endpoint tools or release number. Follow only an actually published
package manifest and the [six-tool reference](https://madeonsol.com/agent-gateway/docs).

Use the existing documented Python REST methods for current production endpoints. After the verified six-tool release, add a typed or explicitly validated six-action wrapper; never infer that installing an older PyPI version provides it. Python x402 clients pay only the routes listed in the actual Solana 402 catalog, not every composite research action.

**Access models:** a paid subscription authenticates a recurring API client;
individual x402 calls are available only for exact live priced routes and
accepted payment legs. MCP/Agent Kit/ElizaOS are tool integration layers, not
independent billing plans. An account's usage limits and commercial
redistribution entitlements still apply.

The coordinated release is tracked in the [MadeOnSol source repository](https://github.com/MadeOnSol/madeonsol).
Do not claim six-tool support from an unpublished local manifest or call a
not-yet-deployed public Agent Gateway route. Client packages must be published
and their registry versions synchronized before the site advertises support.
