diff --git a/skills/gmgn-market/SKILL.md b/skills/gmgn-market/SKILL.md index fa1490f..59a7036 100644 --- a/skills/gmgn-market/SKILL.md +++ b/skills/gmgn-market/SKILL.md @@ -437,9 +437,89 @@ Use field combinations to determine what stage a token is in. This affects how s | `--type` | No | Categories to query, repeatable: `new_creation` / `near_completion` / `completed` (default: all three) | | `--launchpad-platform` | No | Launchpad platform filter, repeatable (default: all platforms for the chain) | | `--limit` | No | Max results per category, max 80 (default: 80) | +| `--filter-preset` | No | Named server-side filter preset: `safe` / `smart-money` / `strict` | +| `--sort-by` | No | Client-side sort per category: `smart_degen_count` / `renowned_count` / `volume_24h` / `volume_1h` / `swaps_24h` / `swaps_1h` / `rug_ratio` / `holder_count` / `usd_market_cap` / `created_timestamp` | +| `--direction` | No | Sort direction: `asc` / `desc` (default: `desc`; `asc` for `rug_ratio`) | +| `--min-*` / `--max-*` | No | Server-side filter range flags — see Filter Fields Reference below | + +### Filter Presets + +Presets are applied server-side: the API filters tokens before returning results. + +| Preset | Server-side filters applied | +|--------|----------------------------| +| `safe` | `max_rug_ratio=0.3` + `max_bundler_rate=0.3` + `max_insider_ratio=0.3` | +| `smart-money` | `min_smart_degen_count=1` | +| `strict` | `max_rug_ratio=0.3` + `max_bundler_rate=0.3` + `max_insider_ratio=0.3` + `min_smart_degen_count=1` | + +**Preset + explicit flag interaction:** Explicit filter flags always override preset values. For example, `--filter-preset safe --max-rug-ratio 0.1` applies the `safe` preset but overrides rug_ratio threshold to `0.1`. + +**All filter flags are sent as part of the API request body (server-side)** — the server filters tokens before returning results. Use `--limit 80` (the default maximum) to maximise the pool. Response fields: `data.new_creation`, `data.pump`, `data.completed` — each is an array of `RankItem` (same fields as `market trending` rank items). **Important: `data.pump` in the response corresponds to `--type near_completion` in the request. The API always returns this category under the key `pump`, not `near_completion`.** +### Server-Side Filter Fields + +All filter flags are sent as part of the API request body — the server filters tokens before returning results. Flags follow the naming convention `--min-{field}` / `--max-{field}`. + +| Flag pair | Type | Description | +|-----------|------|-------------| +| `--min-visiting-count` / `--max-visiting-count` | int | Visitor count | +| `--min-progress` / `--max-progress` | float | Bonding curve progress (0–1) | +| `--min-marketcap` / `--max-marketcap` | float | Market cap (USD) | +| `--min-liquidity` / `--max-liquidity` | float | Liquidity (USD) | +| `--min-created` / `--max-created` | string | Token age (e.g. `1m` / `5m` / `1h` / `24h`) | +| `--min-holder-count` / `--max-holder-count` | int | Holder count | +| `--min-top-holder-rate` / `--max-top-holder-rate` | float | Top-10 holder concentration (0–1) | +| `--min-rug-ratio` / `--max-rug-ratio` | float | Rug pull risk score (0–1) | +| `--min-bundler-rate` / `--max-bundler-rate` | float | Bundle-bot trading ratio (0–1) | +| `--min-insider-ratio` / `--max-insider-ratio` | float | Insider trading ratio (0–1) | +| `--min-entrapment-ratio` / `--max-entrapment-ratio` | float | Entrapment trading ratio (0–1) | +| `--min-private-vault-hold-rate` / `--max-private-vault-hold-rate` | float | Private vault holding ratio (0–1) | +| `--min-top70-sniper-hold-rate` / `--max-top70-sniper-hold-rate` | float | Top-70 sniper holding ratio (0–1) | +| `--min-bot-count` / `--max-bot-count` | int | Bot wallet count | +| `--min-bot-degen-rate` / `--max-bot-degen-rate` | float | Bot-degen wallet ratio (0–1) | +| `--min-fresh-wallet-rate` / `--max-fresh-wallet-rate` | float | Fresh wallet ratio (0–1) | +| `--min-total-fee` / `--max-total-fee` | float | Total fee | +| `--min-smart-degen-count` / `--max-smart-degen-count` | int | Smart-money holder count | +| `--min-renowned-count` / `--max-renowned-count` | int | KOL / renowned wallet count | +| `--min-creator-balance-rate` / `--max-creator-balance-rate` | float | Creator holding ratio (0–1) | +| `--min-creator-created-count` / `--max-creator-created-count` | int | Creator's total token creation count | +| `--min-creator-created-open-count` / `--max-creator-created-open-count` | int | Creator's graduated token count | +| `--min-creator-created-open-ratio` / `--max-creator-created-open-ratio` | float | Creator's graduation ratio (0–1) | +| `--min-twitter-rename-count` / `--max-twitter-rename-count` | int | Twitter rename count | +| `--min-tg-call-count` / `--max-tg-call-count` | int | Telegram call count | + +### Trenches Filter Examples + +```bash +# Apply the safe preset (server-side) +gmgn-cli market trenches --chain sol --type new_creation --filter-preset safe + +# Require at least 1 smart money holder (server-side) +gmgn-cli market trenches --chain sol --type new_creation --min-smart-degen-count 1 + +# Safe preset + require smart money + sort by smart degen count (server-side filter, client-side sort) +gmgn-cli market trenches --chain sol --type new_creation \ + --filter-preset safe --min-smart-degen-count 1 --sort-by smart_degen_count + +# Strict preset — safe + smart money (server-side) +gmgn-cli market trenches --chain sol --type new_creation --type near_completion \ + --filter-preset strict --sort-by smart_degen_count + +# Manual range filters (all sent server-side) +gmgn-cli market trenches --chain sol --type new_creation \ + --max-rug-ratio 0.3 --max-bundler-rate 0.3 --max-insider-ratio 0.3 \ + --min-smart-degen-count 1 + +# Filter by token age: only tokens created within the last 30 minutes +gmgn-cli market trenches --chain sol --type new_creation --max-created 30m + +# Filter by market cap range +gmgn-cli market trenches --chain sol --type new_creation \ + --min-marketcap 10000 --max-marketcap 500000 +``` + ## `market trenches` Response Fields **Basic Info** @@ -526,6 +606,44 @@ Response fields: `data.new_creation`, `data.pump`, `data.completed` — each is **After fetching trenches results, apply the Token Quality Filter Criteria section before presenting tokens to the user.** Do not dump raw results — filter first, then surface the strongest candidates. +### Trenches Filter Examples + +```bash +# Quick safe screen — exclude rugs, wash trading, and bundlers +gmgn-cli market trenches --chain sol --type new_creation \ + --filter-preset safe --raw + +# Smart money screen — only tokens with smart money or KOL presence +gmgn-cli market trenches --chain sol \ + --type new_creation --type near_completion \ + --filter-preset smart-money \ + --sort-by smart_degen_count --raw + +# Strict screen — safe + smart money + minimum volume, sorted by smart degens +gmgn-cli market trenches --chain sol --type completed \ + --filter-preset strict --sort-by smart_degen_count --raw + +# Custom filter — no wash trading, rug_ratio <= 0.2, at least 1 smart degen +gmgn-cli market trenches --chain sol \ + --type new_creation --type near_completion \ + --exclude-wash-trading --max-rug-ratio 0.2 --min-smart-degen 1 \ + --sort-by smart_degen_count --raw + +# BSC — safe screen on new tokens from fourmeme +gmgn-cli market trenches --chain bsc --type new_creation \ + --launchpad-platform fourmeme --launchpad-platform fourmeme_agent \ + --filter-preset safe --sort-by volume_1h --raw + +# Sort by rug_ratio ascending (safest first), no other filters +gmgn-cli market trenches --chain sol --type completed \ + --sort-by rug_ratio --raw + +# Find tokens with many holders and strong 1h swap activity +gmgn-cli market trenches --chain sol --type completed \ + --min-holders 100 --min-swaps 50 \ + --sort-by swaps_1h --raw +``` + ### Solana Trenches Examples ```bash diff --git a/src/commands/market.ts b/src/commands/market.ts index 77814fc..e12f8a4 100644 --- a/src/commands/market.ts +++ b/src/commands/market.ts @@ -57,19 +57,192 @@ export function registerMarketCommands(program: Command): void { printResult(data, opts.raw); }); - market + const trenchesCmd = market .command("trenches") .description("Get Trenches token data (new creation, near completion, completed)") .requiredOption("--chain ", "Chain: sol / bsc / base") .option("--type ", "Categories to query, repeatable: new_creation / near_completion / completed (default: all three)") .option("--launchpad-platform ", "Launchpad platform filter, repeatable (default: all platforms for the chain)") .option("--limit ", "Max results per category, max 80 (default: 80)", parseInt) - .option("--raw", "Output raw JSON") - .action(async (opts) => { - validateChain(opts.chain); - const client = new OpenApiClient(getConfig()); - const data = await client.getTrenches(opts.chain, opts.type, opts.launchpadPlatform, opts.limit).catch(exitOnError); - printResult(data, opts.raw); - }); + .option("--filter-preset ", "Apply a named filter preset: safe / smart-money / strict") + .option("--sort-by ", "Client-side sort per category: smart_degen_count / renowned_count / volume_24h / volume_1h / swaps_24h / swaps_1h / rug_ratio / holder_count / usd_market_cap / created_timestamp") + .option("--direction ", "Sort direction: asc / desc (default: desc; asc for rug_ratio)") + .option("--raw", "Output raw JSON"); + + // Dynamically register all server-side filter flags + for (const def of TRENCHES_FILTER_FIELDS) { + const flag = def.api.replace(/_/g, '-'); + if (def.type === "int") { + trenchesCmd.option(`--${flag} <${def.type}>`, def.desc, parseInt); + } else if (def.type === "float") { + trenchesCmd.option(`--${flag} <${def.type}>`, def.desc, parseFloat); + } else { + trenchesCmd.option(`--${flag} `, def.desc); + } + } + + trenchesCmd.action(async (opts) => { + validateChain(opts.chain); + const client = new OpenApiClient(getConfig()); + + // Build server-side filter object + const filters: Record = {}; + + // Apply preset values first + if (opts.filterPreset != null) { + const preset = TRENCHES_FILTER_PRESETS[opts.filterPreset as string]; + if (!preset) { + console.error(`Unknown --filter-preset "${opts.filterPreset}". Valid options: ${Object.keys(TRENCHES_FILTER_PRESETS).join(", ")}`); + process.exit(1); + } + Object.assign(filters, preset); + } + + // Apply individual filter flags (override preset values) + const optsMap = opts as Record; + for (const def of TRENCHES_FILTER_FIELDS) { + const key = apiFieldToCliKey(def.api); + const val = optsMap[key]; + if (val != null) filters[def.api] = val as number | string; + } + + const data = await client + .getTrenches(opts.chain, opts.type, opts.launchpadPlatform, opts.limit, Object.keys(filters).length ? filters : undefined) + .catch(exitOnError); + + const result = opts.sortBy + ? sortTrenchesResult(data as Record, opts.sortBy as string, (opts.direction as string) ?? "") + : data; + printResult(result, opts.raw); + }); } +// ---- Trenches filter field definitions ---- + +type TrenchesFieldType = "int" | "float" | "string"; + +interface TrenchesFilterField { + api: string; + type: TrenchesFieldType; + desc: string; +} + +// All server-side filter fields for market trenches +// API field names map to CLI flags by replacing _ with - (e.g. min_volume_24h → --min-volume-24h) +const TRENCHES_FILTER_FIELDS: TrenchesFilterField[] = [ + // Visitor count (24h trading volume / swap counts are not supported by the API) + { api: "min_visiting_count", type: "int", desc: "Min visitor count" }, + { api: "max_visiting_count", type: "int", desc: "Max visitor count" }, + // Market & liquidity + { api: "min_progress", type: "float", desc: "Min bonding curve progress (0–1)" }, + { api: "max_progress", type: "float", desc: "Max bonding curve progress (0–1, 1 = completed)" }, + { api: "min_marketcap", type: "float", desc: "Min market cap (USD)" }, + { api: "max_marketcap", type: "float", desc: "Max market cap (USD)" }, + { api: "min_liquidity", type: "float", desc: "Min liquidity (USD)" }, + { api: "max_liquidity", type: "float", desc: "Max liquidity (USD)" }, + // Token age + { api: "min_created", type: "string", desc: "Min token age (e.g. 1m / 5m / 30m / 1h / 6h / 24h)" }, + { api: "max_created", type: "string", desc: "Max token age (e.g. 1m / 5m / 30m / 1h / 6h / 24h)" }, + // Holders + { api: "min_holder_count", type: "int", desc: "Min holder count" }, + { api: "max_holder_count", type: "int", desc: "Max holder count" }, + { api: "min_top_holder_rate", type: "float", desc: "Min top-10 holder concentration (0–1)" }, + { api: "max_top_holder_rate", type: "float", desc: "Max top-10 holder concentration (0–1)" }, + // Risk signals + { api: "min_rug_ratio", type: "float", desc: "Min rug pull risk score (0–1)" }, + { api: "max_rug_ratio", type: "float", desc: "Max rug pull risk score (0–1, e.g. 0.3 to exclude rugs)" }, + { api: "min_bundler_rate", type: "float", desc: "Min bundle-bot trading ratio (0–1)" }, + { api: "max_bundler_rate", type: "float", desc: "Max bundle-bot trading ratio (0–1)" }, + { api: "min_insider_ratio", type: "float", desc: "Min insider trading ratio (0–1)" }, + { api: "max_insider_ratio", type: "float", desc: "Max insider trading ratio (0–1)" }, + { api: "min_entrapment_ratio", type: "float", desc: "Min entrapment trading ratio (0–1)" }, + { api: "max_entrapment_ratio", type: "float", desc: "Max entrapment trading ratio (0–1)" }, + { api: "min_private_vault_hold_rate", type: "float", desc: "Min private vault holding ratio (0–1)" }, + { api: "max_private_vault_hold_rate", type: "float", desc: "Max private vault holding ratio (0–1)" }, + { api: "min_top70_sniper_hold_rate", type: "float", desc: "Min top-70 sniper holding ratio (0–1)" }, + { api: "max_top70_sniper_hold_rate", type: "float", desc: "Max top-70 sniper holding ratio (0–1)" }, + { api: "min_bot_count", type: "int", desc: "Min bot wallet count" }, + { api: "max_bot_count", type: "int", desc: "Max bot wallet count" }, + { api: "min_bot_degen_rate", type: "float", desc: "Min bot-degen wallet ratio (0–1)" }, + { api: "max_bot_degen_rate", type: "float", desc: "Max bot-degen wallet ratio (0–1)" }, + { api: "min_fresh_wallet_rate", type: "float", desc: "Min fresh wallet ratio (0–1)" }, + { api: "max_fresh_wallet_rate", type: "float", desc: "Max fresh wallet ratio (0–1)" }, + { api: "min_total_fee", type: "float", desc: "Min total fee" }, + { api: "max_total_fee", type: "float", desc: "Max total fee" }, + // Smart money + { api: "min_smart_degen_count", type: "int", desc: "Min smart-money holder count" }, + { api: "max_smart_degen_count", type: "int", desc: "Max smart-money holder count" }, + { api: "min_renowned_count", type: "int", desc: "Min KOL / renowned wallet count" }, + { api: "max_renowned_count", type: "int", desc: "Max KOL / renowned wallet count" }, + // Dev / creator + { api: "min_creator_balance_rate", type: "float", desc: "Min creator holding ratio (0–1)" }, + { api: "max_creator_balance_rate", type: "float", desc: "Max creator holding ratio (0–1)" }, + { api: "min_creator_created_count", type: "int", desc: "Min creator total token creation count" }, + { api: "max_creator_created_count", type: "int", desc: "Max creator total token creation count" }, + { api: "min_creator_created_open_count", type: "int", desc: "Min creator graduated token count" }, + { api: "max_creator_created_open_count", type: "int", desc: "Max creator graduated token count" }, + { api: "min_creator_created_open_ratio", type: "float", desc: "Min creator graduation ratio (0–1)" }, + { api: "max_creator_created_open_ratio", type: "float", desc: "Max creator graduation ratio (0–1)" }, + // Social (x_follower is not supported by the API — silently ignored) + { api: "min_twitter_rename_count", type: "int", desc: "Min Twitter rename count (high = suspicious)" }, + { api: "max_twitter_rename_count", type: "int", desc: "Max Twitter rename count" }, + { api: "min_tg_call_count", type: "int", desc: "Min Telegram call count" }, + { api: "max_tg_call_count", type: "int", desc: "Max Telegram call count" }, +]; + +// Named filter presets using actual server-side API field names +const TRENCHES_FILTER_PRESETS: Record> = { + safe: { + max_rug_ratio: 0.3, + max_bundler_rate: 0.3, + max_insider_ratio: 0.3, + }, + "smart-money": { + min_smart_degen_count: 1, + }, + strict: { + max_rug_ratio: 0.3, + max_bundler_rate: 0.3, + max_insider_ratio: 0.3, + min_smart_degen_count: 1, + }, +}; + +// Convert API snake_case field to Commander.js opts key +// Commander.js camelCase: only converts -[a-z] patterns, digits stay as-is +// e.g. min_volume_24h → min-volume-24h → minVolume-24h (digit prefix -24 is NOT converted) +// e.g. min_smart_degen_count → min-smart-degen-count → minSmartDegenCount (all letters, converts fully) +function apiFieldToCliKey(apiField: string): string { + return apiField + .replace(/_/g, '-') + .replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); +} + +// Client-side sort helpers (API does not support server-side sort for trenches) +interface TrenchesCategory { + [key: string]: unknown; +} + +const TRENCHES_SORT_ASC_DEFAULTS = new Set(["rug_ratio"]); +const TRENCHES_STRING_NUMERIC_FIELDS = new Set(["usd_market_cap", "liquidity", "volume_1h", "volume_24h"]); + +function sortTrenchesCategory(items: TrenchesCategory[], sortBy: string, direction: string): TrenchesCategory[] { + const dir = direction || (TRENCHES_SORT_ASC_DEFAULTS.has(sortBy) ? "asc" : "desc"); + return [...items].sort((a, b) => { + const aVal = TRENCHES_STRING_NUMERIC_FIELDS.has(sortBy) + ? parseFloat(String(a[sortBy] ?? 0)) + : Number(a[sortBy] ?? 0); + const bVal = TRENCHES_STRING_NUMERIC_FIELDS.has(sortBy) + ? parseFloat(String(b[sortBy] ?? 0)) + : Number(b[sortBy] ?? 0); + return dir === "asc" ? aVal - bVal : bVal - aVal; + }); +} + +function sortTrenchesResult(data: Record, sortBy: string, direction: string): Record { + const result: Record = {}; + for (const [key, val] of Object.entries(data)) { + result[key] = Array.isArray(val) ? sortTrenchesCategory(val as TrenchesCategory[], sortBy, direction) : val; + } + return result; +}