madeonsol_kol_first_touches
Retrieve first-KOL-touch events to identify token mints where top scouts buy first, enabling early trading signals with high follow-on probability.
Instructions
Recent first-KOL-touch events — every time a tracked KOL was the first to buy a token mint. Filterable by scout tier (S/A/B/C from mv_kol_scout_score), KOL winrate, token age, etc. Backtest: top scouts attract ≥3 follow-on KOLs within 4h ~50% of the time vs ~14% baseline. Median lead time before second KOL is 12s — for trading this signal, use the WebSocket channel rather than polling.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Number of events to return (1-100, default 50) | |
| since | No | ISO timestamp — events strictly newer than this. Polling cursor. | |
| before | No | ISO timestamp — events strictly older than this. Pagination cursor. | |
| kol | No | Filter to a single KOL wallet address (base58) | |
| min_kol_winrate_7d | No | Minimum 7d winrate of the first-touch KOL (0-100) | |
| min_scout_tier | No | Restrict to first-touch KOLs of this scout tier or better. Requires n_first_touches_30d >= 30. | |
| min_n_touches | No | Lower the minimum sample size for scout scoring (default 30) | |
| strategy | No | Filter by first-touch KOL's auto-tagged strategy | |
| token_age_max_min | No | Only events on tokens younger than N minutes (uses token_first_seen) | |
| min_first_buy_sol | No | Minimum size of the first KOL buy in SOL | |
| mint_suffix | No | Suffix-filter the token mint (e.g. 'pump', 'bonk') | |
| preset | No | Shortcut filter: 'scout' = min_scout_tier=B + min_n_touches=30 + token_age_max_min=60. 'fresh_launch' = token_age_max_min=15. | |
| include | No | Comma-separated includes — currently 'followers_4h' (computed for events >=4h old) |
Implementation Reference
- src/index.ts:856-880 (registration)Registration of the 'madeonsol_kol_first_touches' tool via server.tool() call, including its description, schema, and handler.
server.tool( "madeonsol_kol_first_touches", "Recent first-KOL-touch events — every time a tracked KOL was the first to buy a token mint. Filterable by scout tier (S/A/B/C from mv_kol_scout_score), KOL winrate, token age, etc. Backtest: top scouts attract ≥3 follow-on KOLs within 4h ~50% of the time vs ~14% baseline. Median lead time before second KOL is 12s — for trading this signal, use the WebSocket channel rather than polling.", { limit: z.number().min(1).max(100).optional().describe("Number of events to return (1-100, default 50)"), since: z.string().optional().describe("ISO timestamp — events strictly newer than this. Polling cursor."), before: z.string().optional().describe("ISO timestamp — events strictly older than this. Pagination cursor."), kol: z.string().optional().describe("Filter to a single KOL wallet address (base58)"), min_kol_winrate_7d: z.number().min(0).max(100).optional().describe("Minimum 7d winrate of the first-touch KOL (0-100)"), min_scout_tier: z.enum(["S", "A", "B", "C"]).optional().describe("Restrict to first-touch KOLs of this scout tier or better. Requires n_first_touches_30d >= 30."), min_n_touches: z.number().min(1).optional().describe("Lower the minimum sample size for scout scoring (default 30)"), strategy: z.enum(["scalper", "day_trader", "swing_trader", "hodler", "mixed"]).optional().describe("Filter by first-touch KOL's auto-tagged strategy"), token_age_max_min: z.number().min(1).optional().describe("Only events on tokens younger than N minutes (uses token_first_seen)"), min_first_buy_sol: z.number().min(0).optional().describe("Minimum size of the first KOL buy in SOL"), mint_suffix: z.string().optional().describe("Suffix-filter the token mint (e.g. 'pump', 'bonk')"), preset: z.enum(["scout", "fresh_launch"]).optional().describe("Shortcut filter: 'scout' = min_scout_tier=B + min_n_touches=30 + token_age_max_min=60. 'fresh_launch' = token_age_max_min=15."), include: z.string().optional().describe("Comma-separated includes — currently 'followers_4h' (computed for events >=4h old)"), }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async (args) => { const params: Record<string, string | number> = {}; for (const [k, v] of Object.entries(args)) if (v !== undefined) params[k] = v as string | number; return { content: [{ type: "text" as const, text: await restQuery("GET", `/kol/first-touches?${new URLSearchParams(params as Record<string, string>).toString()}`) }] }; } ); - src/index.ts:875-880 (handler)The handler function that executes the tool logic — calls restQuery to GET /kol/first-touches with the provided filter parameters.
async (args) => { const params: Record<string, string | number> = {}; for (const [k, v] of Object.entries(args)) if (v !== undefined) params[k] = v as string | number; return { content: [{ type: "text" as const, text: await restQuery("GET", `/kol/first-touches?${new URLSearchParams(params as Record<string, string>).toString()}`) }] }; } ); - src/index.ts:859-873 (schema)Zod input schema for the tool — defines all filter parameters (limit, since, before, kol, winrate, scout tier, strategy, token age, SOL size, mint suffix, preset, includes).
{ limit: z.number().min(1).max(100).optional().describe("Number of events to return (1-100, default 50)"), since: z.string().optional().describe("ISO timestamp — events strictly newer than this. Polling cursor."), before: z.string().optional().describe("ISO timestamp — events strictly older than this. Pagination cursor."), kol: z.string().optional().describe("Filter to a single KOL wallet address (base58)"), min_kol_winrate_7d: z.number().min(0).max(100).optional().describe("Minimum 7d winrate of the first-touch KOL (0-100)"), min_scout_tier: z.enum(["S", "A", "B", "C"]).optional().describe("Restrict to first-touch KOLs of this scout tier or better. Requires n_first_touches_30d >= 30."), min_n_touches: z.number().min(1).optional().describe("Lower the minimum sample size for scout scoring (default 30)"), strategy: z.enum(["scalper", "day_trader", "swing_trader", "hodler", "mixed"]).optional().describe("Filter by first-touch KOL's auto-tagged strategy"), token_age_max_min: z.number().min(1).optional().describe("Only events on tokens younger than N minutes (uses token_first_seen)"), min_first_buy_sol: z.number().min(0).optional().describe("Minimum size of the first KOL buy in SOL"), mint_suffix: z.string().optional().describe("Suffix-filter the token mint (e.g. 'pump', 'bonk')"), preset: z.enum(["scout", "fresh_launch"]).optional().describe("Shortcut filter: 'scout' = min_scout_tier=B + min_n_touches=30 + token_age_max_min=60. 'fresh_launch' = token_age_max_min=15."), include: z.string().optional().describe("Comma-separated includes — currently 'followers_4h' (computed for events >=4h old)"), }, - src/index.ts:361-451 (helper)The restQuery helper function used by the handler to make authenticated REST API calls to the MadeOnSol backend.
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 () => ({ content: [{ type: "text" as const, text: await walletTrackerRequest("GET", "/wallet-tracker/watchlist") }], }) ); server.tool( "madeonsol_wallet_tracker_add", "Add a Solana wallet to your watchlist. Returns 409 if already tracked or limit reached.", { wallet_address: z.string().describe("Solana wallet address (base58) to track"), label: z.string().optional().describe("Optional human-readable label for this wallet"), }, { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, async ({ wallet_address, label }) => { const body: Record<string, unknown> = { wallet_address }; if (label) body.label = label; return { content: [{ type: "text" as const, text: await walletTrackerRequest("POST", "/wallet-tracker/watchlist", body) }] }; } ); server.tool( "madeonsol_wallet_tracker_remove", "Remove a wallet from your watchlist.", { wallet_address: z.string().describe("Solana wallet address to remove from watchlist"), }, { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, async ({ wallet_address }) => ({ content: [{ type: "text" as const, text: await walletTrackerRequest("DELETE", `/wallet-tracker/watchlist/${encodeURIComponent(wallet_address)}`) }], }) ); server.tool( "madeonsol_wallet_tracker_trades", "Historical swap and transfer events for all your watched wallets. BASIC: truncated wallets, no tx_signature.", { wallet: z.string().optional().describe("Filter to a specific wallet address"), action: z.enum(["buy", "sell", "transfer_in", "transfer_out"]).optional().describe("Filter by action type"), event_type: z.enum(["swap", "transfer"]).optional().describe("Filter by event type: swap (token trade) or transfer (SOL moved)"), limit: z.number().min(1).max(200).default(50).describe("Max results (1–200)"), before: z.number().optional().describe("Pagination cursor: block_time of the last event from previous page"), }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ wallet, action, event_type, limit, before }) => { const params: Record<string, string | number> = { limit }; if (wallet) params.wallet = wallet; if (action) params.action = action; if (event_type) params.event_type = event_type; if (before !== undefined) params.before = before; const url = new URL(`${BASE_URL}/api/v1/wallet-tracker/trades`); for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v)); const res = await fetch(url.toString(), { headers: { "Content-Type": "application/json", ...apiKeyHeaders() } }); const text = res.ok ? JSON.stringify(await res.json(), null, 2) : `Error ${res.status}: ${await res.text().catch(() => "")}`; return { content: [{ type: "text" as const, text }] }; } ); server.tool( "madeonsol_wallet_tracker_summary", "Per-wallet stats: swap counts, SOL bought/sold, and last activity time across your watchlist.", { period: z.enum(["24h", "7d", "30d"]).default("7d").describe("Time window for stats"), wallet: z.string().optional().describe("Filter to a specific wallet address"), }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, async ({ period, wallet }) => { const url = new URL(`${BASE_URL}/api/v1/wallet-tracker/summary`); url.searchParams.set("period", period); if (wallet) url.searchParams.set("wallet", wallet); const res = await fetch(url.toString(), { headers: { "Content-Type": "application/json", ...apiKeyHeaders() } }); const text = res.ok ? JSON.stringify(await res.json(), null, 2) : `Error ${res.status}: ${await res.text().catch(() => "")}`; return { content: [{ type: "text" as const, text }] }; } ); console.error("[madeonsol-mcp] Wallet tracker tools enabled"); } else { console.error("[madeonsol-mcp] Wallet tracker tools disabled (requires MADEONSOL_API_KEY)"); } } // ── Webhook & Streaming tools (require MadeOnSol API key — Pro/Ultra tier) ── const hasRestAuth = authMode === "madeonsol"; if (hasRestAuth) { async function restQuery(method: string, path: string, body?: unknown): Promise<string> {