diff --git a/docs/README.skills.md b/docs/README.skills.md index 24473fa6f..e762dced4 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -273,6 +273,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [meeting-minutes](../skills/meeting-minutes/SKILL.md)
`gh skills install github/awesome-copilot meeting-minutes` | Generate concise, actionable meeting minutes for internal meetings. Includes metadata, attendees, agenda, decisions, action items (owner + due date), and follow-up steps. | None | | [memory-merger](../skills/memory-merger/SKILL.md)
`gh skills install github/awesome-copilot memory-merger` | Merges mature lessons from a domain memory file into its instruction file. Syntax: `/memory-merger >domain [scope]` where scope is `global` (default), `user`, `workspace`, or `ws`. | None | | [mentoring-juniors](../skills/mentoring-juniors/SKILL.md)
`gh skills install github/awesome-copilot mentoring-juniors` | Socratic mentoring for junior developers and AI newcomers. Guides through questions, never answers. Triggers: "help me understand", "explain this code", "I'm stuck", "Im stuck", "I'm confused", "Im confused", "I don't understand", "I dont understand", "can you teach me", "teach me", "mentor me", "guide me", "what does this error mean", "why doesn't this work", "why does not this work", "I'm a beginner", "Im a beginner", "I'm learning", "Im learning", "I'm new to this", "Im new to this", "walk me through", "how does this work", "what's wrong with my code", "what's wrong", "can you break this down", "ELI5", "step by step", "where do I start", "what am I missing", "newbie here", "junior dev", "first time using", "how do I", "what is", "is this right", "not sure", "need help", "struggling", "show me", "help me debug", "best practice", "too complex", "overwhelmed", "lost", "debug this", "/socratic", "/hint", "/concept", "/pseudocode". Progressive clue systems, teaching techniques, and success metrics. | None | +| [meteora-dlmm-pool-screening](../skills/meteora-dlmm-pool-screening/SKILL.md)
`gh skills install github/awesome-copilot meteora-dlmm-pool-screening` | Screen and rank Meteora DLMM pools for LP quality using public Meteora APIs (fee/TVL, bin step, organic score, TVL, volume). Use whenever the user asks to screen pools, find the best Meteora/DLMM pool, rank candidates, compare fee/TVL, pick a bin step, or asks which pool to LP on Solana. Also trigger for dex-pool-screening, trending pools, and Meteora candidate lists. Read-only: never deploy, swap, sign, or touch wallets. | `evals/evals.json`
`how_to_use.md`
`references/meteora-apis.md`
`scripts/screen.py` | | [microsoft-agent-framework](../skills/microsoft-agent-framework/SKILL.md)
`gh skills install github/awesome-copilot microsoft-agent-framework` | Create, update, refactor, explain, or review Microsoft Agent Framework solutions using shared guidance plus language-specific references for .NET and Python. | `references/dotnet.md`
`references/python.md` | | [microsoft-code-reference](../skills/microsoft-code-reference/SKILL.md)
`gh skills install github/awesome-copilot microsoft-code-reference` | Look up Microsoft API references, find working code samples, and verify SDK code is correct. Use when working with Azure SDKs, .NET libraries, or Microsoft APIs—to find the right method, check parameters, get working examples, or troubleshoot errors. Catches hallucinated methods, wrong signatures, and deprecated patterns by querying official docs. | None | | [microsoft-docs](../skills/microsoft-docs/SKILL.md)
`gh skills install github/awesome-copilot microsoft-docs` | Query official Microsoft documentation to find concepts, tutorials, and code examples across Azure, .NET, Agent Framework, Aspire, VS Code, GitHub, and more. Uses Microsoft Learn MCP as the default, with Context7 and Aspire MCP for content that lives outside learn.microsoft.com. | None | diff --git a/skills/meteora-dlmm-pool-screening/SKILL.md b/skills/meteora-dlmm-pool-screening/SKILL.md new file mode 100644 index 000000000..c1c542233 --- /dev/null +++ b/skills/meteora-dlmm-pool-screening/SKILL.md @@ -0,0 +1,102 @@ +--- +name: meteora-dlmm-pool-screening +description: > + Screen and rank Meteora DLMM pools for LP quality using public Meteora APIs (fee/TVL, + bin step, organic score, TVL, volume). Use whenever the user asks to screen pools, + find the best Meteora/DLMM pool, rank candidates, compare fee/TVL, pick a bin step, + or asks which pool to LP on Solana. Also trigger for dex-pool-screening, trending + pools, and Meteora candidate lists. Read-only: never deploy, swap, sign, or touch wallets. +metadata: + version: "1.0.0" + author: etemaro +license: MIT +compatibility: Network access to Meteora public datapi. No API key. Python 3 stdlib for scripts/screen.py. +--- + +# Meteora DLMM pool screening + +Rank Meteora DLMM pools the way an LP screener should: **hard-filter first, then sort by +windowed fee / active TVL**. Public APIs only. No keys, no transactions. + +Prefer the bundled script. It encodes the gates below so every run uses the same numbers. + +```bash +python3 scripts/screen.py # trending volatile (default) +python3 scripts/screen.py --preset stable +python3 scripts/screen.py --query BONK # pair search; preset defaults to loose +python3 scripts/screen.py --query BONK --preset volatile +python3 scripts/screen.py --json --limit 8 +``` + +If the script is not on disk, curl the same endpoints in [references/meteora-apis.md](references/meteora-apis.md). +Always send a `User-Agent` — unauthenticated requests without one get `403`. + +## When to use + +- User wants a ranked Meteora DLMM candidate list (trending or a token/pair). +- User asks which bin step / pool to LP for a pair. +- User wants a fee/TVL screen, not a single-pool deep dive. + +Not this skill: deploying, claiming, closing, swapping, wallet hygiene, or a full +token-holder / narrative research dump. Stop after the ranked table and verdicts. + +## Method + +1. **Universe** — trending discovery (`category=trending`) unless the user named a token, + then query that mint/symbol. Pair query defaults to `--preset loose` so bin-step + tradeoffs stay visible; pass `--preset volatile` only when the user wants that gate. +2. **Hard filters** — reject before ranking. A high fee/TVL pool that fails a gate is a + skip, not a "maybe". +3. **Score** — `fee_active_tvl_ratio * 1000 + organic * 10 + volume / 100 + holders / 100`. + Fee/TVL dominates; organic and activity break ties. +4. **Verdict** — `pass` (clears gates, top of list), `watch` (clears gates but thin + activity, unverified token, or awkward bin step), `skip` (failed a gate). +5. **Stop** — print the table. Do not fetch a wallet, do not build a tx, do not call Etemaro CLI. + +## Presets + +Defaults match a volatile/narrative Solana LP screen (wide bin step, mid TVL). Change +preset when the user says stable pair or blue-chip. + +| Preset | bin_step | TVL USD | min fee/active TVL | min organic | min holders | min volume | +| :--- | :--- | :--- | :--- | :--- | :--- | :--- | +| `volatile` (default) | 80–125 | 10k–150k | 0.05 | 60 | 500 | 500 | +| `stable` | 1–50 | 100k–5m | 0.02 | 70 | 2000 | 5000 | +| `bluechip` | 1–25 | 500k–10m | 0.01 | 80 | 5000 | 10000 | +| `loose` | any | ≥1k | 0 | 0 | 0 | 0 | + +Always reject: dead pools (zero volume and zero fee/TVL). Any preset except `loose` also +rejects critical token warnings, high single-ownership, non-DLMM pool type. + +Timeframe: `30m` default. `5m` is noisier (spikes look like yield). `24h` is smoother but +lags a dead pool. State the timeframe in the report — windowed fee/TVL is not 24h APR. + +## Report shape + +``` +# Meteora DLMM screening +Universe: trending | query= Timeframe: 30m Preset: volatile +Protocol: tvl=$… vol_24h=$… pools=… + +## Ranked +| # | name | bin | fee/TVL | tvl | vol | organic | holders | verdict | why | +... + +## Rejects (sample) +- NAME — reason +``` + +Keep `why` to one clause (e.g. "fee/TVL 0.24, organic 67, bin 80"). Cite pool address. +If the API returns zero rows, say so and loosen one gate at a time (usually `maxTvl` or +`minFeeActiveTvlRatio`) — do not invent pools. + +## Read-only safety + +This skill only **GET**s public Meteora JSON. No `.env`, no keystore, no signing, no +`deploy` / `swap` / `claim` / `close`. If the user wants live execution, point them at +[Etemaro](https://etemaro.com) (repo: https://github.com/romankurnovskii/etemaro) and stop. + +## Go deeper — Etemaro + +Etemaro runs this screen on a cron, adds holder/bot/launchpad gates, pool memory, and +can deploy. The skill is the analysis half; the product is the loop. diff --git a/skills/meteora-dlmm-pool-screening/evals/evals.json b/skills/meteora-dlmm-pool-screening/evals/evals.json new file mode 100644 index 000000000..07d920829 --- /dev/null +++ b/skills/meteora-dlmm-pool-screening/evals/evals.json @@ -0,0 +1,23 @@ +{ + "skill_name": "meteora-dlmm-pool-screening", + "evals": [ + { + "id": 1, + "prompt": "screen trending Meteora DLMM pools for LP and give me a ranked pass/watch/skip table", + "expected_output": "Uses public Meteora APIs or scripts/screen.py, reports timeframe, applies hard filters, ranks by fee/TVL, no deploy/swap/wallet actions", + "files": [] + }, + { + "id": 2, + "prompt": "which Meteora DLMM pool should I LP for BONK? compare fee/TVL and bin step", + "expected_output": "Pair query for BONK, table of pools with bin_step and windowed fee/TVL, verdicts, read-only", + "files": [] + }, + { + "id": 3, + "prompt": "dex-pool-screening on Solana, volatile meme preset, top 5 only", + "expected_output": "Triggers this skill, volatile preset gates (bin 80-125, TVL 10k-150k), limit 5, no Etemaro CLI deploy", + "files": [] + } + ] +} diff --git a/skills/meteora-dlmm-pool-screening/how_to_use.md b/skills/meteora-dlmm-pool-screening/how_to_use.md new file mode 100644 index 000000000..73947e51f --- /dev/null +++ b/skills/meteora-dlmm-pool-screening/how_to_use.md @@ -0,0 +1,60 @@ +# How to Use: Meteora DLMM Pool Screening + +## What This Skill Does + +Ranks Meteora DLMM pools for LP quality from public APIs — fee/TVL, bin step, TVL, organic +score — and returns a pass / watch / skip table. It does not trade. + +## When to Use + +- Find trending Meteora DLMM pools worth LPing. +- Compare all DLMM pools for a token (BONK, SOL-USDC, …). +- Check whether a pool's fee/TVL and bin step fit a volatile vs stable screen. +- Someone says "dex pool screening" but means Solana / Meteora. + +## Prompt Examples + +```text +screen trending Meteora DLMM pools for LP +``` + +```text +which Meteora pool should I LP for BONK? +``` + +```text +rank SOL-USDC DLMM pools by fee/TVL and bin step +``` + +```text +dex-pool-screening on Meteora, volatile preset, top 8 +``` + +```text +is this pool's 30m fee/active TVL actually good? JBGqmRZB4csWcQnTMaJGsKBoo4zC1dAoMs7LYGMKqEjD +``` + +## What You Get + +A ranked markdown table plus reject reasons. Optional JSON from the script: + +```bash +python3 skills/meteora-dlmm-pool-screening/scripts/screen.py --query BONK --limit 5 +``` + +``` +skills/meteora-dlmm-pool-screening/ +├── SKILL.md +├── how_to_use.md +├── scripts/screen.py +└── references/meteora-apis.md +``` + +## Tips + +- Always send a User-Agent to Meteora datapi or you get HTTP 403. +- `fee_active_tvl_ratio` is **windowed** (default 30m), not 24h APR. Label the window. +- Read-only. Deploying is Etemaro, not this skill. +- Pair query (`--query BONK`) defaults to `loose` so you can compare bin steps. Dead pools + (zero volume and fee/TVL) are still dropped. +- `loose` is for pair comparison / empty-result debugging, not a live LP pick. diff --git a/skills/meteora-dlmm-pool-screening/references/meteora-apis.md b/skills/meteora-dlmm-pool-screening/references/meteora-apis.md new file mode 100644 index 000000000..897cdb8da --- /dev/null +++ b/skills/meteora-dlmm-pool-screening/references/meteora-apis.md @@ -0,0 +1,74 @@ +# Meteora public APIs used by this skill + +No API key. Send `User-Agent` and `Accept: application/json` or Cloudflare returns 403. + +Base hosts: + +- Discovery (trending + filters): `https://pool-discovery-api.datapi.meteora.ag` +- DLMM datapi (pair query, protocol stats, pool detail): `https://dlmm.datapi.meteora.ag` + +## Endpoints + +### Trending / filtered universe + +``` +GET /pools?page_size=50&timeframe=30m&category=trending&filter_by= +``` + +`filter_by` is `&&`-joined predicates. Example (volatile preset): + +``` +pool_type=dlmm +&&base_token_has_critical_warnings=false +&"e_token_has_critical_warnings=false +&&base_token_has_high_single_ownership=false +&&tvl>=10000&&tvl<=150000 +&&dlmm_bin_step>=80&&dlmm_bin_step<=125 +&&fee_active_tvl_ratio>=0.05 +&&base_token_organic_score>=60 +&&base_token_holders>=500 +&&volume>=500 +``` + +Response: `{ total, page_size, data[], has_more }`. + +Useful `data[]` fields: `pool_address`, `name`, `pool_type`, `tvl`, `active_tvl`, +`fee_active_tvl_ratio`, `volume`, `volatility`, `base_token_holders`, +`dlmm_params.bin_step`, `token_x.{symbol,address,organic_score,market_cap,warnings}`. + +### Pair / token query + +``` +GET /pools?query=&sort_by=tvl:desc +``` + +on `dlmm.datapi.meteora.ag`. Response `{ total, data[] }`. + +Fee/TVL and volume are **buckets**: `{ "30m", "1h", "2h", "4h", "12h", "24h" }`. +Use the same key as `--timeframe`. Bin step is `pool_config.bin_step`. Address is `address`. + +### Protocol snapshot + +``` +GET https://dlmm.datapi.meteora.ag/stats/protocol_metrics +``` + +`total_tvl`, `volume_24h`, `fee_24h`, `total_pools`. One-line context only. + +### Single pool + +``` +GET https://dlmm.datapi.meteora.ag/pools/{poolAddress} +GET https://pool-discovery-api.datapi.meteora.ag/pools?page_size=1&filter_by=pool_address={poolAddress}&timeframe=30m +``` + +Discovery lag: a brand-new pool may exist on DLMM datapi before discovery indexes it. + +## Score (keep in sync with scripts/screen.py) + +``` +score = fee_tvl * 1000 + organic * 10 + volume / 100 + holders / 100 +``` + +`fee_tvl` is the windowed ratio (`fee_active_tvl_ratio` on discovery, bucketed +`fee_tvl_ratio[timeframe]` on pair query). diff --git a/skills/meteora-dlmm-pool-screening/scripts/screen.py b/skills/meteora-dlmm-pool-screening/scripts/screen.py new file mode 100755 index 000000000..029419b7c --- /dev/null +++ b/skills/meteora-dlmm-pool-screening/scripts/screen.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +"""Rank Meteora DLMM pools from public datapi. Stdlib only. Read-only GET.""" + +from __future__ import annotations + +import argparse +import json +import sys +import urllib.error +import urllib.parse +import urllib.request +from typing import Any + +DISCOVERY = "https://pool-discovery-api.datapi.meteora.ag" +DLMM = "https://dlmm.datapi.meteora.ag" +UA = "Mozilla/5.0 (compatible; etemaro-skill/1.0; +https://etemaro.com)" +TIMEOUT = 25 + +PRESETS: dict[str, dict[str, Any]] = { + "volatile": { + "min_bin": 80, + "max_bin": 125, + "min_tvl": 10_000, + "max_tvl": 150_000, + "min_fee_tvl": 0.05, + "min_organic": 60, + "min_holders": 500, + "min_volume": 500, + "hard_warnings": True, + }, + "stable": { + "min_bin": 1, + "max_bin": 50, + "min_tvl": 100_000, + "max_tvl": 5_000_000, + "min_fee_tvl": 0.02, + "min_organic": 70, + "min_holders": 2_000, + "min_volume": 5_000, + "hard_warnings": True, + }, + "bluechip": { + "min_bin": 1, + "max_bin": 25, + "min_tvl": 500_000, + "max_tvl": 10_000_000, + "min_fee_tvl": 0.01, + "min_organic": 80, + "min_holders": 5_000, + "min_volume": 10_000, + "hard_warnings": True, + }, + "loose": { + "min_bin": None, + "max_bin": None, + "min_tvl": 1_000, + "max_tvl": None, + "min_fee_tvl": 0.0, + "min_organic": 0, + "min_holders": 0, + "min_volume": 0, + "hard_warnings": False, + }, +} + + +def get_json(url: str) -> Any: + req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=TIMEOUT) as resp: + return json.loads(resp.read().decode()) + except urllib.error.HTTPError as exc: + body = exc.read().decode(errors="replace")[:200] + raise SystemExit(f"HTTP {exc.code} {url}\n{body}") from exc + except urllib.error.URLError as exc: + raise SystemExit(f"network error {url}: {exc.reason}") from exc + + +def num(value: Any) -> float | None: + try: + n = float(value) + except (TypeError, ValueError): + return None + return n if n == n else None # NaN check + + +def bucket(obj: Any, timeframe: str) -> float | None: + if isinstance(obj, dict): + return num(obj.get(timeframe)) + return num(obj) + + +def score(fee_tvl: float, organic: float, volume: float, holders: float) -> float: + return fee_tvl * 1000 + organic * 10 + volume / 100 + holders / 100 + + +def discovery_filters(p: dict[str, Any]) -> str: + parts = ["pool_type=dlmm"] + if p["hard_warnings"]: + parts += [ + "base_token_has_critical_warnings=false", + "quote_token_has_critical_warnings=false", + "base_token_has_high_single_ownership=false", + ] + if p["min_tvl"] is not None: + parts.append(f"tvl>={int(p['min_tvl'])}") + if p["max_tvl"] is not None: + parts.append(f"tvl<={int(p['max_tvl'])}") + if p["min_bin"] is not None: + parts.append(f"dlmm_bin_step>={int(p['min_bin'])}") + if p["max_bin"] is not None: + parts.append(f"dlmm_bin_step<={int(p['max_bin'])}") + if p["min_fee_tvl"]: + parts.append(f"fee_active_tvl_ratio>={p['min_fee_tvl']}") + if p["min_organic"]: + parts.append(f"base_token_organic_score>={int(p['min_organic'])}") + if p["min_holders"]: + parts.append(f"base_token_holders>={int(p['min_holders'])}") + if p["min_volume"]: + parts.append(f"volume>={int(p['min_volume'])}") + return "&&".join(parts) + + +def reject_reason(row: dict[str, Any], p: dict[str, Any]) -> str | None: + fee = row.get("fee_tvl") + vol = row.get("volume") + if (fee is None or fee <= 0) and (vol is None or vol <= 0): + return "dead pool (zero volume and fee/TVL)" + if p["hard_warnings"] and row.get("critical"): + return "critical token warning" + bin_step = row.get("bin_step") + tvl = row.get("tvl") + organic = row.get("organic") + holders = row.get("holders") + if p["min_bin"] is not None and (bin_step is None or bin_step < p["min_bin"]): + return f"bin_step {bin_step} < {p['min_bin']}" + if p["max_bin"] is not None and bin_step is not None and bin_step > p["max_bin"]: + return f"bin_step {bin_step} > {p['max_bin']}" + if tvl is None or tvl < p["min_tvl"]: + return f"tvl {tvl} < {p['min_tvl']}" + if p["max_tvl"] is not None and tvl is not None and tvl > p["max_tvl"]: + return f"tvl {tvl} > {p['max_tvl']}" + if fee is None or fee < p["min_fee_tvl"]: + return f"fee/TVL {fee} < {p['min_fee_tvl']}" + if vol is None or vol < p["min_volume"]: + return f"volume {vol} < {p['min_volume']}" + if organic is not None and organic < p["min_organic"]: + return f"organic {organic} < {p['min_organic']}" + if holders is not None and holders < p["min_holders"]: + return f"holders {holders} < {p['min_holders']}" + return None + + +def verdict(row: dict[str, Any], p: dict[str, Any]) -> str: + if reject_reason(row, p): + return "skip" + warnings = row.get("warnings") or [] + unverified = any("NOT_VERIFIED" in str(w) for w in warnings) + thin = (row.get("volume") or 0) < p["min_volume"] * 2 if p["min_volume"] else False + no_yield = (row.get("fee_tvl") or 0) <= 0 + if unverified or thin or no_yield: + return "watch" + return "pass" + + +def from_discovery(raw: dict[str, Any], timeframe: str) -> dict[str, Any]: + token_x = raw.get("token_x") or {} + warnings = token_x.get("warnings") or [] + critical = bool(raw.get("base_token_has_critical_warnings")) or any( + (w.get("severity") if isinstance(w, dict) else None) == "critical" for w in warnings if isinstance(w, dict) + ) + bin_step = num((raw.get("dlmm_params") or {}).get("bin_step")) + return { + "name": raw.get("name"), + "pool": raw.get("pool_address"), + "bin_step": bin_step, + "fee_tvl": num(raw.get("fee_active_tvl_ratio")), + "tvl": num(raw.get("tvl")), + "active_tvl": num(raw.get("active_tvl")), + "volume": num(raw.get("volume")), + "organic": num(token_x.get("organic_score")), + "holders": num(raw.get("base_token_holders")), + "mcap": num(token_x.get("market_cap")), + "volatility": num(raw.get("volatility")), + "mint": token_x.get("address"), + "warnings": warnings, + "critical": critical, + "source": "discovery", + "timeframe": timeframe, + } + + +def from_dlmm(raw: dict[str, Any], timeframe: str) -> dict[str, Any]: + token_x = raw.get("token_x") or {} + cfg = raw.get("pool_config") or {} + return { + "name": raw.get("name"), + "pool": raw.get("address"), + "bin_step": num(cfg.get("bin_step")), + "fee_tvl": bucket(raw.get("fee_tvl_ratio"), timeframe), + "tvl": num(raw.get("tvl")), + "active_tvl": num(raw.get("tvl")), + "volume": bucket(raw.get("volume"), timeframe), + "organic": None, + "holders": num(token_x.get("holders")), + "mcap": num(token_x.get("market_cap")), + "volatility": None, + "mint": token_x.get("address"), + "warnings": [], + "critical": bool(raw.get("is_blacklisted")), + "source": "dlmm", + "timeframe": timeframe, + } + + +def fmt(n: float | None, digits: int = 2) -> str: + if n is None: + return "—" + if abs(n) >= 1_000_000: + return f"{n / 1_000_000:.{digits}f}M" + if abs(n) >= 1_000: + return f"{n / 1_000:.{digits}f}k" + return f"{n:.{digits}f}" + + +def fmt_bin(n: float | None) -> str: + if n is None: + return "—" + return str(int(n)) if float(n).is_integer() else fmt(n, 1) + + +def print_table(rows: list[dict[str, Any]], rejects: list[tuple[str, str]], meta: dict[str, Any]) -> None: + print(f"# Meteora DLMM screening") + print( + f"Universe: {meta['universe']} Timeframe: {meta['timeframe']} " + f"Preset: {meta['preset']} Source: {meta['source']}" + ) + proto = meta.get("protocol") or {} + if proto: + print( + f"Protocol: tvl=${fmt(proto.get('total_tvl'))} " + f"vol_24h=${fmt(proto.get('volume_24h'))} pools={proto.get('total_pools')}" + ) + print(f"Fetched: {meta['fetched']} Ranked: {len(rows)} Rejected: {len(rejects)}") + print() + print("## Ranked") + print("| # | name | bin | fee/TVL | tvl | vol | organic | holders | verdict | why |") + print("|---|---|---|---|---|---|---|---|---|---|") + for i, row in enumerate(rows, 1): + why = ( + f"fee/TVL {fmt(row.get('fee_tvl'), 3)}, " + f"bin {fmt_bin(row.get('bin_step'))}, " + f"{row.get('pool')}" + ) + print( + f"| {i} | {row.get('name') or '?'} | {fmt_bin(row.get('bin_step'))} | " + f"{fmt(row.get('fee_tvl'), 3)} | {fmt(row.get('tvl'))} | {fmt(row.get('volume'))} | " + f"{fmt(row.get('organic'), 1)} | {fmt(row.get('holders'), 0)} | " + f"{row.get('verdict')} | {why} |" + ) + if not rows: + print("_no pools passed gates — loosen max TVL or min fee/TVL, or try --preset loose_") + if rejects: + print() + print("## Rejects (sample)") + for name, reason in rejects[:12]: + print(f"- {name} — {reason}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Screen Meteora DLMM pools (read-only).") + parser.add_argument("--preset", choices=sorted(PRESETS), default="volatile") + parser.add_argument("--query", help="Token symbol or mint (pair search via DLMM datapi)") + parser.add_argument("--timeframe", default="30m", choices=["5m", "30m", "1h", "2h", "4h", "12h", "24h"]) + parser.add_argument("--limit", type=int, default=10) + parser.add_argument("--page-size", type=int, default=50) + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + # Pair search is "which pool for this token", not a trending meme screen. + if args.query and "--preset" not in sys.argv: + args.preset = "loose" + preset = PRESETS[args.preset] + + protocol = get_json(f"{DLMM}/stats/protocol_metrics") + + if args.query: + qs = urllib.parse.urlencode({"query": args.query, "sort_by": "tvl:desc"}) + payload = get_json(f"{DLMM}/pools?{qs}") + raw_list = payload.get("data") or [] + rows = [from_dlmm(p, args.timeframe) for p in raw_list if isinstance(p, dict)] + source = "dlmm" + universe = f"query={args.query}" + fetched = payload.get("total", len(rows)) + else: + qs = urllib.parse.urlencode( + { + "page_size": args.page_size, + "timeframe": args.timeframe, + "category": "trending", + "filter_by": discovery_filters(preset), + } + ) + payload = get_json(f"{DISCOVERY}/pools?{qs}") + raw_list = payload.get("data") or [] + rows = [from_discovery(p, args.timeframe) for p in raw_list if isinstance(p, dict)] + source = "discovery" + universe = "trending" + fetched = payload.get("total", len(rows)) + + rejects: list[tuple[str, str]] = [] + kept: list[dict[str, Any]] = [] + for row in rows: + reason = reject_reason(row, preset) + if reason: + rejects.append((str(row.get("name") or row.get("pool") or "?"), reason)) + continue + row["score"] = score( + row.get("fee_tvl") or 0, + row.get("organic") or 0, + row.get("volume") or 0, + row.get("holders") or 0, + ) + row["verdict"] = verdict(row, preset) + kept.append(row) + + kept.sort(key=lambda r: r.get("score") or 0, reverse=True) + kept = kept[: max(1, args.limit)] + + meta = { + "universe": universe, + "timeframe": args.timeframe, + "preset": args.preset, + "source": source, + "fetched": fetched, + "protocol": protocol if isinstance(protocol, dict) else {}, + } + if args.json: + json.dump({"meta": meta, "ranked": kept, "rejects": rejects[:20]}, sys.stdout, indent=2) + sys.stdout.write("\n") + return + print_table(kept, rejects, meta) + + +if __name__ == "__main__": + main()