mcp-server-madeonsol 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 MadeOnSol
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MadeOnSol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -15,6 +15,8 @@ MCP server for [MadeOnSol](https://madeonsol.com) Solana KOL intelligence API. U
15
15
 
16
16
  > 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**, detect multi-KOL coordination, surface bundle-cohort holdings (which same-slot wallets still hold a token's supply), verify any wallet's CURRENT on-chain holdings straight from its token accounts, and stream every DEX trade across 9+ programs. 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).
17
17
 
18
+ > **New in 3.1.0 — copy-trade and Wallet Tracker tools match the API.** `madeonsol_copytrade_create` / `_update` accept up to 250 `source_wallets` (the BUSINESS per-rule limit; before, the tool refused anything over 50 before the server could answer). The server still enforces your own tier's limit (PRO 5, ULTRA 50, BUSINESS 250). Create, update and `madeonsol_copytrade_signals` gain the market-cap band `min_mc_usd` / `max_mc_usd`. The `only_action` description now states the real default (`buy`), and the tool descriptions say that signals fire only for tracked KOL wallets. `madeonsol_wallet_tracker_trades` gains `order` (`slot` | `block_time`) and the `before_slot` cursor, and its `action` filter is `buy` | `sell` only: `transfer_in` / `transfer_out` were always rejected by the API with 400. Use `event_type: "transfer"` for transfers. `madeonsol_copytrade_update` documents that a PATCH which sets a `webhook_url` on a rule without a signing secret returns `webhook_secret` once, and the copy-trade tools describe the `source_wallets_untracked` / `warnings[]` fields newer servers return. `madeonsol_test_webhook` gains an optional `event` (which subscribed event to sample).
19
+
18
20
  > **New in 3.0.0 — BREAKING for x402 (keyless) mode only: five env vars are now required and startup FAILS instead of silently degrading (security fix, SDK-01).** Set `X402_PAY_TO`, `X402_FEE_PAYER`, `X402_MAX_AMOUNT_ATOMIC`, `X402_MAX_TOTAL_AMOUNT_ATOMIC` and `SVM_RPC_URL` alongside `SVM_PRIVATE_KEY`. Before, keyless mode signed whatever Solana USDC amount, recipient and fee payer a 402 challenge asked for. Now every challenge is checked BEFORE signing against a trusted merchant `payTo`, a trusted facilitator `feePayer` (which must differ from your wallet), the USDC mint, `solana:5eykt…` mainnet, the `exact` scheme, a per-call cap and a lifetime cap. Use the canonical values in the keyless section below; caps must be at least `20000` (0.02 USDC) per call to reach every endpoint. The budget is per client instance / process: not wallet-wide, not shared between processes, reset on a new instance or restart. Keyless requires the base URL exactly `https://madeonsol.com`. **API-key (`msk_`) users: no change, no new config.** HTTP mode still refuses wallet signers (SEC-02, unchanged).
19
21
 
20
22
  > **New in 2.0.0 — BREAKING: HTTP mode now requires a separate token (security fix, SEC-02).** If you run this server with `MCP_TRANSPORT=http`, you must now also set `MCP_HTTP_TOKEN` (a random secret you generate) and send it as `Authorization: Bearer <token>` on **every** request, including `GET /health` and the server-card endpoint — previously, anything that could reach the bound port could call every tool using the operator's own API key with no authentication at all. HTTP mode is now hard-restricted to literal loopback (`127.0.0.1` / `::1`), rejects `Origin`/`X-Forwarded-*` headers and duplicate `Authorization`/`Host` headers outright, exposes only `POST /mcp` plus the two GET routes, and **refuses to start** if a wallet/payment signer is configured — HTTP mode can never carry a payer key. **If you use the default `stdio` transport (Claude Desktop, Cursor, most MCP clients), nothing changes — no action needed.** This is a single shared-operator-token fix, not a new multi-user or OAuth system; each HTTP caller still shares the operator's own MadeOnSol API key. Full writeup: `docs/audit/SEC02_PRIVATE_HTTP_MCP.md`.
@@ -261,10 +263,10 @@ Pre-confirm pump.fun deploy feed reconstructed from shred-level (**deshred**) da
261
263
 
262
264
  | Tool | Description |
263
265
  |---|---|
264
- | `madeonsol_wallet_tracker_watchlist` | List your tracked wallets and remaining capacity (Free: 10, Pro: 50, Ultra: 100) |
266
+ | `madeonsol_wallet_tracker_watchlist` | List your tracked wallets and remaining capacity (Pro: 50, Ultra: 100, Business: 500; the Free tier has no wallet tracker) |
265
267
  | `madeonsol_wallet_tracker_add` | Add a wallet to your watchlist |
266
268
  | `madeonsol_wallet_tracker_remove` | Remove a wallet from your watchlist |
267
- | `madeonsol_wallet_tracker_trades` | Historical swap/transfer events for watched wallets (120-day retention) |
269
+ | `madeonsol_wallet_tracker_trades` | Historical swap/transfer events for watched wallets (120-day retention). Returns `events[]` + `next_cursor` / `next_cursor_slot`. `action` is `buy` or `sell` (swaps only; transfers have `action: null`, select them with `event_type: transfer`). `order` = `slot` (default) or `block_time`; page with `before_slot` or the legacy `before` |
268
270
  | `madeonsol_wallet_tracker_summary` | Per-wallet stats: swap counts, SOL bought/sold, last event |
269
271
 
270
272
  ### Universal Wallet *(new in 1.8 — any wallet, not just curated KOLs, PRO+)*
@@ -314,18 +316,18 @@ Scored from 1.5M+ early-buyer records (wallets seen in the first 20 buyers of Pu
314
316
  | `madeonsol_token_flow` | PRO+ | Trade-flow aggregate (organic-vs-fake volume) over a 1h/24h `window` — unique wallets/buyers/sellers, buy/sell counts + SOL, `net_sol`, `trades_per_wallet` wash-trading proxy |
315
317
  | `madeonsol_token_trades` | **New 1.19** · PRO+ | Mint-scoped trade tape — cursor-paginated raw trades for one token (action / wallet / since–until filters, default FULL history). History starts 2026-04-12; `coverage` block marks scope |
316
318
 
317
- ### Copy-Trade Rules (PRO/ULTRA)
319
+ ### Copy-Trade Rules (PRO+)
318
320
 
319
- Server-side rules that fire signals when a watched source wallet trades. Delivered via webhook (HMAC-signed) and/or WebSocket.
321
+ Server-side rules that fire signals when a source wallet trades. Delivered via webhook (HMAC-signed) and/or WebSocket. Limits: PRO 3 rules × 5 source wallets, ULTRA 20 × 50, BUSINESS 100 × 250 (Enterprise follows Business). The server enforces your tier's limit. Signals fire only for trades by wallets MadeOnSol tracks as KOLs (the roster at `GET /api/v1/kol/wallets`): a rule accepts any valid Solana address, but an untracked wallet never produces a signal. `only_action` defaults to `buy`. `min_mc_usd` / `max_mc_usd` restrict a rule (or the signals query) to a market-cap band on the source trade; when a bound is set, trades with an unknown market cap are dropped.
320
322
 
321
323
  | Tool | Description |
322
324
  |---|---|
323
325
  | `madeonsol_copytrade_list` | List your rules |
324
- | `madeonsol_copytrade_create` | Create a rule. Returns `webhook_secret` once — store it |
326
+ | `madeonsol_copytrade_create` | Create a rule (optional `min_mc_usd` / `max_mc_usd`). Returns `webhook_secret` once — store it |
325
327
  | `madeonsol_copytrade_get` | Get one rule |
326
- | `madeonsol_copytrade_update` | Update fields or toggle `is_active` |
328
+ | `madeonsol_copytrade_update` | Update fields or toggle `is_active`; `null` clears an MC bound |
327
329
  | `madeonsol_copytrade_delete` | Delete permanently |
328
- | `madeonsol_copytrade_signals` | Recent fired signals (up to 7 days) |
330
+ | `madeonsol_copytrade_signals` | Recent fired signals (up to 7 days). Filters `subscription_id`, `since`, `limit`, `min_mc_usd` / `max_mc_usd` |
329
331
 
330
332
  ### KOL Coordination Alerts (PRO/ULTRA — v1.1 push signals)
331
333
 
package/dist/index.js CHANGED
@@ -440,7 +440,7 @@ function registerTools(server) {
440
440
  return JSON.stringify(await res.json(), null, 2);
441
441
  }
442
442
  if (hasRestAuth) {
443
- server.tool("madeonsol_wallet_tracker_watchlist", "List your tracked wallets with labels and remaining watchlist capacity. BASIC=10, PRO=50, ULTRA=100.", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({
443
+ server.tool("madeonsol_wallet_tracker_watchlist", "List your tracked wallets with labels and remaining watchlist capacity. PRO 50, ULTRA 100, BUSINESS 500 wallets (the Free tier has no wallet tracker).", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({
444
444
  content: [{ type: "text", text: await walletTrackerRequest("GET", "/wallet-tracker/watchlist") }],
445
445
  }));
446
446
  server.tool("madeonsol_wallet_tracker_add", "Add a Solana wallet to your watchlist. Returns 409 if already tracked or limit reached.", {
@@ -463,13 +463,15 @@ function registerTools(server) {
463
463
  }, { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ wallet_address, label }) => ({
464
464
  content: [{ type: "text", text: await walletTrackerRequest("PATCH", `/wallet-tracker/watchlist/${encodeURIComponent(wallet_address)}`, { label }) }],
465
465
  }));
466
- server.tool("madeonsol_wallet_tracker_trades", "Historical swap and transfer events for all your watched wallets. BASIC: truncated wallets, no tx_signature.", {
466
+ server.tool("madeonsol_wallet_tracker_trades", "Historical swap and transfer events for all your watched wallets (PRO+). Returns { events[], count, ordered_by, next_cursor, next_cursor_slot }; each event has wallet_address, label, event_type, action (buy/sell on swaps, null on transfers), token_mint/symbol/name, sol_amount, token_amount, tx_signature, block_time (ingest clock), slot (chain order), replayed, ingested_at. Page with before_slot = next_cursor_slot (order=slot, the default) or before = next_cursor (order=block_time).", {
467
467
  wallet: z.string().optional().describe("Filter to a specific wallet address"),
468
- action: z.enum(["buy", "sell", "transfer_in", "transfer_out"]).optional().describe("Filter by action type"),
468
+ action: z.enum(["buy", "sell"]).optional().describe("Swaps only: buy or sell. Transfers have action null; select them with event_type='transfer'"),
469
469
  event_type: z.enum(["swap", "transfer"]).optional().describe("Filter by event type: swap (token trade) or transfer (SOL moved)"),
470
470
  limit: z.number().min(1).max(200).default(50).describe("Max results (1–200)"),
471
- before: z.number().optional().describe("Pagination cursor: block_time of the last event from previous page"),
472
- }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ wallet, action, event_type, limit, before }) => {
471
+ order: z.enum(["slot", "block_time"]).optional().describe("Sort by on-chain slot (default on a first page) or by block_time, the ingest clock (default when 'before' is passed)"),
472
+ before_slot: z.number().int().optional().describe("Cursor for order=slot: next_cursor_slot of the previous page"),
473
+ before: z.number().optional().describe("Legacy cursor for order=block_time: next_cursor of the previous page"),
474
+ }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ wallet, action, event_type, limit, order, before_slot, before }) => {
473
475
  const params = { limit };
474
476
  if (wallet)
475
477
  params.wallet = wallet;
@@ -477,6 +479,10 @@ function registerTools(server) {
477
479
  params.action = action;
478
480
  if (event_type)
479
481
  params.event_type = event_type;
482
+ if (order)
483
+ params.order = order;
484
+ if (before_slot !== undefined)
485
+ params.before_slot = before_slot;
480
486
  if (before !== undefined)
481
487
  params.before = before;
482
488
  const url = new URL(`${BASE_URL}/api/v1/wallet-tracker/trades`);
@@ -634,10 +640,11 @@ function registerTools(server) {
634
640
  body.is_active = is_active;
635
641
  return { content: [{ type: "text", text: await restQuery("PATCH", `/webhooks/${id}`, body) }] };
636
642
  });
637
- server.tool("madeonsol_test_webhook", "Send a sample event payload to a webhook URL to verify it works. Returns status code and response time.", {
643
+ server.tool("madeonsol_test_webhook", "Send a sample event payload to a webhook URL to verify it works. Returns status code, response time and (newer servers) the event type sampled.", {
638
644
  webhook_id: z.number().describe("ID of the webhook to test"),
639
- }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ webhook_id }) => ({
640
- content: [{ type: "text", text: await restQuery("POST", "/webhooks/test", { webhook_id }) }],
645
+ event: z.string().optional().describe("Which of the webhook's subscribed events to sample (e.g. 'kol:trade', 'wallet_tracker:event'). Omit for the first subscribed event; an unsubscribed event is answered with 400."),
646
+ }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ webhook_id, event }) => ({
647
+ content: [{ type: "text", text: await restQuery("POST", "/webhooks/test", event ? { webhook_id, event } : { webhook_id }) }],
641
648
  }));
642
649
  server.tool("madeonsol_stream_token", "Get your WebSocket streaming token. Stream tokens NEVER EXPIRE: every call returns the same token until your subscription lapses or you pass rotate=true, which replaces it (the previous value keeps working for 60 s). expires_at / next_refresh_at are always null (kept for wire compatibility); the response also carries rotated (boolean) and lifetime (string). The server never rotates on its own and never sends token_refresh unless you rotated; a 4001 close means 'mint again', never a timer. Authenticate the handshake with 'Authorization: Bearer <token>' (?token= still works, masked in logs). Includes ws_url for KOL/deployer streaming (Pro/Ultra) and dex_ws_url for all-DEX trade streaming (Ultra only). PRO+ channels now also include token:locks (event token:lock — every NEW Streamflow / Jupiter Lock / Bonfida lock or vesting contract) and token:fee_claims (event token:fee_claim — every pump.fun fee event: distributions to shareholders, social/X claims, config changes, creator transfers) and token:surges (events token:surge — a token < 30 min old running >= 3x / 6x / 8x its launch MC, tier early / strong / breakout, each once per mint, sustained — and token:revival — no trade candle for >= 24 h, then confirmed buys on the tape; every frame carries tape / kol / early_buyers / deployer / risk_flags[]; subscribe filters kinds[], tiers[], launchpads[], exclude_flags[], min_mc_usd / max_mc_usd, deployer_tier[]).", { rotate: z.boolean().optional().describe("Replace the current token with a new one (the old value keeps working for 60 s). Default false — returns the existing token.") }, { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ rotate }) => ({
643
650
  content: [{ type: "text", text: await restQuery("POST", "/stream/token", rotate ? { rotate: true } : undefined) }],
@@ -730,7 +737,7 @@ function registerTools(server) {
730
737
  server.tool("madeonsol_token_buyer_quality", "0–100 buyer-quality score for a token's first-buyer cohort. 5-min cached. Full breakdown on all tiers, incl. dump_cluster_count (3+ dump-cluster wallets in the first-20 → 94% historical dump rate vs 61% base) and recycled_early_buyer_count.", { mint: z.string().describe("Token mint address (base58)") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mint }) => ({
731
738
  content: [{ type: "text", text: await restQuery("GET", `/tokens/${encodeURIComponent(mint)}/buyer-quality`) }],
732
739
  }));
733
- server.tool("madeonsol_token_risk", "Transparent 0–100 token rug-risk/safety score (higher = riskier). Returns a band (safe/caution/danger), an explainable factors[] array (mint authority, freeze authority, liquidity, transfer fee, token-2022, burn, launch cohort, deployer bond rate, KOL signal, blacklist) each with status/points/detail, and the raw inputs that produced the score. inputs now includes sniper_footprint — the slot-window launch-snipe rollup ({ buys, buyers, sol, supply_pct|null, sniper_wallet_buys, data_available, as_of } | null; buys landing in slots deploy-1..deploy+3). data_available=false means the mint isn't observable in the trade pipeline — NOT zero snipes; null means no rollup yet. Also returns a top-level dev block (deployer self-activity, null when the mint has no deployer-pipeline row): wallet, launchpad, deployed_at, create-tx self-buy snapshot (buy_sol / buy_tokens / buy_supply_pct), post-create rollup (bought_tokens_after — catches the same-second-separate-tx dev buy — sold_tokens, sold_sol, first_sell_at, last_sell_at), LIVE on-chain holdings (holdings_tokens, holdings_supply_pct — pump.fun 1B denominator, null elsewhere — wallet_empty: is the dev wallet empty NOW), and transferred_out (tokens left without a sell; null = unknown, never a guess). PRO/ULTRA only — BASIC receives HTTP 403.", { mint: z.string().describe("Token mint address (base58)") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mint }) => ({
740
+ server.tool("madeonsol_token_risk", "Transparent 0–100 token rug-risk/safety score (higher = riskier). Returns a band (safe/caution/danger), an explainable factors[] array (mint authority, freeze authority, liquidity, transfer fee, token-2022, burn, launch cohort, deployer bond rate, KOL signal, blacklist) each with status/points/detail, and the raw inputs that produced the score. inputs now includes sniper_footprint — the slot-window launch-snipe rollup ({ buys, buyers, sol, supply_pct|null, sniper_wallet_buys, data_available, as_of } | null; buys landing in slots deploy-1..deploy+3). data_available=false means the mint isn't observable in the trade pipeline — NOT zero snipes; null means no rollup yet. Also returns a top-level dev block (deployer self-activity, null when the mint has no deployer-pipeline row): wallet, launchpad, deployed_at, create-tx self-buy snapshot (buy_sol / buy_tokens / buy_supply_pct), post-create rollup (bought_tokens_after — catches the same-second-separate-tx dev buy — sold_tokens, sold_sol, first_sell_at, last_sell_at), LIVE on-chain holdings (holdings_tokens, holdings_supply_pct — pump.fun 1B denominator, null elsewhere — wallet_empty: is the dev wallet empty NOW), and transfer_status (suspected / none_detected / unknown; transferred_out is its deprecated boolean view). score_version v2 (2026-09-21): the LP factor is 'unknown' unless LP custody is verified — a token-supply burn (token_supply_burn_detected, alias burn_detected) is never LP evidence; assessment.unknown_inputs lists inputs that could not be observed (the band is then never 'safe') and assessment.not_assessed lists dimensions not measured for this token. A failed score-critical read returns HTTP 503 code risk_inputs_unavailable (retry) instead of a score. PRO/ULTRA only — BASIC receives HTTP 403.", { mint: z.string().describe("Token mint address (base58)") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mint }) => ({
734
741
  content: [{ type: "text", text: await restQuery("GET", `/tokens/${encodeURIComponent(mint)}/risk`) }],
735
742
  }));
736
743
  server.tool("madeonsol_token_bundle", "Bundle-cohort holdings for a token — which same-slot bundle wallets bought it and how much of supply they still hold (held_pct_of_supply). Rug/insider signal. Returns a `bundle` block (wallet_count, bundle_kind atomic_tx/same_slot/none, held_ratio, held_pct_of_supply [the headline — net held / circulating supply, null if unknown], fully_exited, buy_volume, tokens_held) plus a `wallets[]` array (rank, wallet, held_ratio, has_sold, atomic, is_kol). BASIC get the bundle block only (empty wallets[]); PRO adds top-10 flags-only wallets; ULTRA returns the full cohort with enriched identities (kol_name, win_rate, bot_confidence, tokens_held).", { mint: z.string().describe("Token mint address (base58)") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mint }) => ({
@@ -971,22 +978,33 @@ function registerTools(server) {
971
978
  server.tool("madeonsol_token_batch", "Bulk lookup of up to 50 mints in one request. Returns the same per-mint shape as madeonsol_token_get. DB queries batched with IN(...); dex-stream + RPC fan-outs run in parallel. ~10-20× cheaper than N sequential calls — ideal for sniper pipelines scoring many tokens at once.", { mints: z.array(z.string()).min(1).max(50).describe("1–50 base58 Solana token mints") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ mints }) => ({
972
979
  content: [{ type: "text", text: await restQuery("POST", "/token/batch", { mints }) }],
973
980
  }));
974
- // ── Copy-Trade rules (PRO/ULTRA) ──
975
- server.tool("madeonsol_copytrade_list", "List your copy-trade rules. PRO=3 rules, ULTRA=20 rules.", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({
981
+ // ── Copy-Trade rules (PRO+) ──
982
+ //
983
+ // Tier limits (rules per account, source wallets per rule) are enforced by
984
+ // the SERVER per tier: copytradeLimits() in the API (PRO 3×5, ULTRA 20×50,
985
+ // BUSINESS 100×250, ENTERPRISE = BUSINESS). The schema below only carries the
986
+ // structural ceiling so it never rejects a valid request from the highest
987
+ // tier before the server can answer; a smaller tier gets the server's 400.
988
+ const COPYTRADE_SOURCE_WALLETS_MAX = 250;
989
+ const COPYTRADE_SCOPE_NOTE = "source_wallets must be wallets MadeOnSol tracks as KOLs (the roster at GET /api/v1/kol/wallets): any valid Solana address is accepted into a rule, but signals fire only for trades by tracked KOL wallets, so an untracked wallet never produces a signal.";
990
+ const MC_BAND_NOTE = "Market-cap band in USD (0 to 1e12, min <= max) on the source trade's market cap at trade time; when a bound is set, trades with an unknown market cap are dropped.";
991
+ server.tool("madeonsol_copytrade_list", "List your copy-trade rules. Rule limits: PRO 3, ULTRA 20, BUSINESS 100 (Enterprise follows Business).", {}, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async () => ({
976
992
  content: [{ type: "text", text: await restQuery("GET", "/copytrade/subscriptions") }],
977
993
  }));
978
- server.tool("madeonsol_copytrade_create", "Create a copy-trade rule. Returns webhook_secret ONCE on creation when delivery_mode includes 'webhook' — store it to verify HMAC signatures. PRO=5 source_wallets/rule, ULTRA=50.", {
979
- source_wallets: z.array(z.string()).min(1).max(50).describe("Wallets to mirror (base58)"),
980
- sizing_amount: z.number().describe("Amount used by the chosen sizing_mode"),
994
+ server.tool("madeonsol_copytrade_create", `Create a copy-trade rule. Returns webhook_secret ONCE on creation when delivery_mode includes 'webhook' — store it to verify HMAC signatures. Source wallets per rule: PRO 5, ULTRA 50, BUSINESS 250 (Enterprise follows Business); the server enforces your tier's limit. Responses may carry source_wallets_tracked / source_wallets_untracked and warnings[] (code untracked_source_wallets or source_wallet_tracking_unavailable). ${COPYTRADE_SCOPE_NOTE}`, {
995
+ source_wallets: z.array(z.string()).min(1).max(COPYTRADE_SOURCE_WALLETS_MAX).describe("Tracked KOL wallets to mirror (base58). Per-rule limit depends on tier (PRO 5, ULTRA 50, BUSINESS 250); the server rejects more than yours."),
996
+ sizing_amount: z.number().describe("SOL when sizing_mode is 'fixed'; otherwise a multiplier / fraction of the source size (0.25 = a quarter), never a percent"),
981
997
  name: z.string().optional().describe("Optional human label"),
982
998
  min_trade_sol: z.number().optional().describe("Minimum source-wallet trade size to fire a signal"),
983
- only_action: z.enum(["buy", "sell", "both"]).optional().describe("Filter to one side (default 'both')"),
984
- sizing_mode: z.enum(["fixed", "proportional", "percent_source"]).optional().describe("How sizing_amount is interpreted"),
999
+ only_action: z.enum(["buy", "sell", "both"]).optional().describe("Which side fires a signal (default 'buy' when omitted)"),
1000
+ sizing_mode: z.enum(["fixed", "proportional", "percent_source"]).optional().describe("'fixed' = sizing_amount SOL; 'proportional' and 'percent_source' both = source size × sizing_amount (default 'fixed')"),
985
1001
  delivery_mode: z.enum(["webhook", "websocket", "both"]).optional().describe("Where to deliver fired signals"),
986
1002
  webhook_url: z.string().url().optional().describe("Required when delivery_mode includes 'webhook'"),
1003
+ min_mc_usd: z.number().min(0).max(1e12).nullable().optional().describe(`Lower bound. ${MC_BAND_NOTE}`),
1004
+ max_mc_usd: z.number().min(0).max(1e12).nullable().optional().describe(`Upper bound. ${MC_BAND_NOTE}`),
987
1005
  }, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async (args) => {
988
1006
  const body = { source_wallets: args.source_wallets, sizing_amount: args.sizing_amount };
989
- for (const k of ["name", "min_trade_sol", "only_action", "sizing_mode", "delivery_mode", "webhook_url"]) {
1007
+ for (const k of ["name", "min_trade_sol", "only_action", "sizing_mode", "delivery_mode", "webhook_url", "min_mc_usd", "max_mc_usd"]) {
990
1008
  if (args[k] !== undefined)
991
1009
  body[k] = args[k];
992
1010
  }
@@ -995,10 +1013,10 @@ function registerTools(server) {
995
1013
  server.tool("madeonsol_copytrade_get", "Get one copy-trade rule by id.", { id: z.number().describe("Subscription id") }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ id }) => ({
996
1014
  content: [{ type: "text", text: await restQuery("GET", `/copytrade/subscriptions/${id}`) }],
997
1015
  }));
998
- server.tool("madeonsol_copytrade_update", "Update fields on a copy-trade rule, including is_active toggle.", {
1016
+ server.tool("madeonsol_copytrade_update", `Update fields on a copy-trade rule, including is_active toggle. Omit a field to leave it unchanged; pass null to clear an MC bound. If this update sets a webhook_url on a rule that had no signing secret yet (e.g. a websocket-only rule), the response returns webhook_secret ONCE — store it; an existing secret is never re-shown. Responses may carry source_wallets_tracked / source_wallets_untracked and warnings[] (code untracked_source_wallets or source_wallet_tracking_unavailable): tell the user which wallets can never fire. ${COPYTRADE_SCOPE_NOTE}`, {
999
1017
  id: z.number().describe("Subscription id"),
1000
1018
  name: z.string().nullable().optional(),
1001
- source_wallets: z.array(z.string()).optional(),
1019
+ source_wallets: z.array(z.string()).min(1).max(COPYTRADE_SOURCE_WALLETS_MAX).optional().describe("Replaces the rule's wallets. Per-rule limit depends on tier (PRO 5, ULTRA 50, BUSINESS 250)."),
1002
1020
  min_trade_sol: z.number().optional(),
1003
1021
  only_action: z.enum(["buy", "sell", "both"]).optional(),
1004
1022
  sizing_mode: z.enum(["fixed", "proportional", "percent_source"]).optional(),
@@ -1006,6 +1024,8 @@ function registerTools(server) {
1006
1024
  delivery_mode: z.enum(["webhook", "websocket", "both"]).optional(),
1007
1025
  webhook_url: z.string().url().nullable().optional(),
1008
1026
  is_active: z.boolean().optional(),
1027
+ min_mc_usd: z.number().min(0).max(1e12).nullable().optional().describe(`Lower bound (null clears it). ${MC_BAND_NOTE}`),
1028
+ max_mc_usd: z.number().min(0).max(1e12).nullable().optional().describe(`Upper bound (null clears it). ${MC_BAND_NOTE}`),
1009
1029
  }, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ id, ...patch }) => {
1010
1030
  const body = {};
1011
1031
  for (const [k, v] of Object.entries(patch)) {
@@ -1017,17 +1037,23 @@ function registerTools(server) {
1017
1037
  server.tool("madeonsol_copytrade_delete", "Delete a copy-trade rule permanently.", { id: z.number().describe("Subscription id") }, { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, async ({ id }) => ({
1018
1038
  content: [{ type: "text", text: await restQuery("DELETE", `/copytrade/subscriptions/${id}`) }],
1019
1039
  }));
1020
- server.tool("madeonsol_copytrade_signals", "Recent fired copy-trade signals (up to 7 days). Filter by subscription_id, since (ISO8601), and limit (1–500).", {
1040
+ server.tool("madeonsol_copytrade_signals", "Recent fired copy-trade signals (up to 7 days). Filter by subscription_id, since (ISO8601), limit (1–500), and min_mc_usd / max_mc_usd on the source trade's market cap.", {
1021
1041
  subscription_id: z.number().optional().describe("Filter to one rule"),
1022
1042
  since: z.string().optional().describe("ISO8601 timestamp — only signals fired at-or-after this time"),
1023
1043
  limit: z.number().min(1).max(500).default(50).describe("Max signals to return (1–500)"),
1024
- }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ subscription_id, since, limit }) => {
1044
+ min_mc_usd: z.number().min(0).max(1e12).optional().describe("Keep signals whose source trade's market cap (USD) was at least this; drops unknown-MC signals"),
1045
+ max_mc_usd: z.number().min(0).max(1e12).optional().describe("Keep signals whose source trade's market cap (USD) was at most this; drops unknown-MC signals"),
1046
+ }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ subscription_id, since, limit, min_mc_usd, max_mc_usd }) => {
1025
1047
  const url = new URL(`${BASE_URL}/api/v1/copytrade/signals`);
1026
1048
  url.searchParams.set("limit", String(limit));
1027
1049
  if (subscription_id !== undefined)
1028
1050
  url.searchParams.set("subscription_id", String(subscription_id));
1029
1051
  if (since)
1030
1052
  url.searchParams.set("since", since);
1053
+ if (min_mc_usd !== undefined)
1054
+ url.searchParams.set("min_mc_usd", String(min_mc_usd));
1055
+ if (max_mc_usd !== undefined)
1056
+ url.searchParams.set("max_mc_usd", String(max_mc_usd));
1031
1057
  const res = await fetch(url.toString(), { headers: { "Content-Type": "application/json", ...apiKeyHeaders() } });
1032
1058
  const text = res.ok ? JSON.stringify(await res.json(), null, 2) : `Error ${res.status}: ${await res.text().catch(() => "")}`;
1033
1059
  return { content: [{ type: "text", text }] };
@@ -1349,12 +1375,12 @@ async function main() {
1349
1375
  { name: "madeonsol_tokens_batch_risk", description: "Bulk rug-risk/safety scoring for up to 50 mints — same shape as madeonsol_token_risk + as_of; untracked mints don't fail the batch. PRO+." },
1350
1376
  { name: "madeonsol_token_get", description: "Comprehensive per-mint snapshot: price, MC, volume, deployer, KOL, age, blacklist." },
1351
1377
  { name: "madeonsol_token_batch", description: "Bulk token snapshot for up to 50 mints — ~10-20× cheaper than N sequential calls." },
1352
- { name: "madeonsol_copytrade_list", description: "List your copy-trade rules. PRO/ULTRA." },
1353
- { name: "madeonsol_copytrade_create", description: "Create a copy-trade rule with webhook + WS delivery. PRO/ULTRA." },
1354
- { name: "madeonsol_copytrade_get", description: "Get one copy-trade rule. PRO/ULTRA." },
1355
- { name: "madeonsol_copytrade_update", description: "Update a copy-trade rule. PRO/ULTRA." },
1356
- { name: "madeonsol_copytrade_delete", description: "Delete a copy-trade rule. PRO/ULTRA." },
1357
- { name: "madeonsol_copytrade_signals", description: "Recent fired copy-trade signals (up to 7 days). PRO/ULTRA." },
1378
+ { name: "madeonsol_copytrade_list", description: "List your copy-trade rules. PRO+." },
1379
+ { name: "madeonsol_copytrade_create", description: "Create a copy-trade rule with webhook + WS delivery. PRO+." },
1380
+ { name: "madeonsol_copytrade_get", description: "Get one copy-trade rule. PRO+." },
1381
+ { name: "madeonsol_copytrade_update", description: "Update a copy-trade rule. PRO+." },
1382
+ { name: "madeonsol_copytrade_delete", description: "Delete a copy-trade rule. PRO+." },
1383
+ { name: "madeonsol_copytrade_signals", description: "Recent fired copy-trade signals (up to 7 days). PRO+." },
1358
1384
  { name: "madeonsol_kol_first_touches", description: "Recent first-KOL-touch events on tokens — backtested scout signal. Filterable by scout tier S/A/B/C, KOL winrate, token age, mint suffix." },
1359
1385
  { name: "madeonsol_first_touch_subscriptions_list", description: "List your first-touch webhook subscriptions. ULTRA only." },
1360
1386
  { name: "madeonsol_first_touch_subscriptions_create", description: "Create a first-touch webhook subscription with HMAC signing. ULTRA only." },
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "3.0.0";
1
+ export declare const VERSION = "3.1.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // AUTO-GENERATED by ../gen-version.mjs (npm prebuild) from package.json. Do not edit.
2
- export const VERSION = "3.0.0";
2
+ export const VERSION = "3.1.0";
package/glama.json CHANGED
@@ -5,7 +5,7 @@
5
5
  ],
6
6
  "display_name": "MadeOnSol — Solana memecoin intelligence",
7
7
  "description": "Real-time Solana memecoin trading intelligence — track 2,000+ KOL wallets with sub-second latency on paid keys and x402 (free-tier live feeds are 5-min delayed), score 85,000+ reputation-profiled Pump.fun deployers out of 1.2M+ indexed, surface multi-KOL coordination signals, run server-side copy-trade rules, and stream every DEX trade across 14 programs. Backtested first-touch scout signal (S-tier scouts attract ≥3 follow-on KOLs ~50% of the time vs 14% baseline). Free tier 200 requests per day (live feeds 5-min delayed), no payment to start; auth via msk_ API key or x402 micropayments.",
8
- "version": "1.27.1",
8
+ "version": "3.1.0",
9
9
  "homepage": "https://madeonsol.com/solana-api",
10
10
  "repository": "https://github.com/madeonsol/mcp-server-madeonsol",
11
11
  "license": "MIT",
@@ -130,7 +130,7 @@
130
130
  },
131
131
  {
132
132
  "name": "madeonsol_wallet_tracker_watchlist",
133
- "description": "List tracked wallets and remaining capacity (Free 10 / Pro 50 / Ultra 100)."
133
+ "description": "List tracked wallets and remaining capacity (Pro 50 / Ultra 100 / Business 500)."
134
134
  },
135
135
  {
136
136
  "name": "madeonsol_wallet_tracker_add",
@@ -142,7 +142,7 @@
142
142
  },
143
143
  {
144
144
  "name": "madeonsol_wallet_tracker_trades",
145
- "description": "Historical swap/transfer events for watched wallets (120-day retention)."
145
+ "description": "Historical swap/transfer events for watched wallets (120-day retention). action buy|sell (swaps only), order slot|block_time, before_slot cursor."
146
146
  },
147
147
  {
148
148
  "name": "madeonsol_wallet_tracker_summary",
@@ -322,7 +322,7 @@
322
322
  },
323
323
  {
324
324
  "name": "madeonsol_copytrade_create",
325
- "description": "Create a copy-trade rule with webhook + WS delivery (HMAC-signed)."
325
+ "description": "Create a copy-trade rule with webhook + WS delivery (HMAC-signed). Optional MC band min_mc_usd/max_mc_usd; signals fire only for tracked KOL wallets."
326
326
  },
327
327
  {
328
328
  "name": "madeonsol_copytrade_get",
@@ -338,7 +338,7 @@
338
338
  },
339
339
  {
340
340
  "name": "madeonsol_copytrade_signals",
341
- "description": "Recent fired copy-trade signals (up to 7 days, 1-500)."
341
+ "description": "Recent fired copy-trade signals (up to 7 days, 1-500). Optional min_mc_usd/max_mc_usd."
342
342
  },
343
343
  {
344
344
  "name": "madeonsol_coordination_alerts_list",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-madeonsol",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "mcpName": "io.github.MadeOnSol/madeonsol",
5
5
  "description": "MCP server for MadeOnSol Solana KOL intelligence API — use from Claude, Cursor, or any MCP client. Free tier 200 req/day (live feeds 5-min delayed; paid is real-time), no payment to start.",
6
6
  "type": "module",