← back to Polymtrx Skill
polymtrx skill: read-only Polymarket opportunity scanner (MTRX/@Polymtrx workflow)
30f0a8d9a9351db12249d445c367cb0f5e0783a0 · 2026-07-13 16:46:43 -0700 · Steve Abrams
- zero-dep, no-key, $0-local Python engine over Gamma + CLOB public APIs
- scan/trending/market/doctor commands; 6-signal opportunity score (0-100)
- read-only hard rail: never trades, no wallet/key
- symlinked live at ~/.claude/skills/polymtrx
Files touched
A .gitignoreA LICENSEA README.mdA skills/polymtrx/SKILL.mdA skills/polymtrx/references/api.mdA skills/polymtrx/scripts/polymtrx.py
Diff
commit 30f0a8d9a9351db12249d445c367cb0f5e0783a0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 13 16:46:43 2026 -0700
polymtrx skill: read-only Polymarket opportunity scanner (MTRX/@Polymtrx workflow)
- zero-dep, no-key, $0-local Python engine over Gamma + CLOB public APIs
- scan/trending/market/doctor commands; 6-signal opportunity score (0-100)
- read-only hard rail: never trades, no wallet/key
- symlinked live at ~/.claude/skills/polymtrx
---
.gitignore | 15 ++
LICENSE | 21 ++
README.md | 42 +++
skills/polymtrx/SKILL.md | 119 +++++++++
skills/polymtrx/references/api.md | 66 +++++
skills/polymtrx/scripts/polymtrx.py | 497 ++++++++++++++++++++++++++++++++++++
6 files changed, 760 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..514709f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# Steve standing .gitignore
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+
+# Python
+__pycache__/
+*.pyc
+.venv/
+venv/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7181d72
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Steve Abrams
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e7bcf20
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+# polymtrx
+
+A Claude Code skill that scans **live Polymarket** prediction markets and ranks
+them by tradeable-edge signals — reproducing the MTRX / [@Polymtrx](https://x.com/polymtrx)
+(PolyMatrix) "prediction markets + signals → find the best opportunities"
+workflow.
+
+- **Zero-dependency** — Python 3.9+ stdlib only.
+- **No API keys** — hits Polymarket's free public Gamma + CLOB endpoints.
+- **$0 (local)** — no metered calls.
+- **Read-only** — surfaces opportunities; never trades, never touches a wallet or key.
+
+## Layout
+```
+skills/polymtrx/
+ SKILL.md the skill contract (frontmatter + workflow)
+ scripts/polymtrx.py the engine: fetch → normalize → score → rank → emit
+ references/api.md Polymarket endpoint shapes + coercion gotchas
+```
+Symlinked live at `~/.claude/skills/polymtrx → skills/polymtrx`.
+
+## Usage
+```bash
+python3 skills/polymtrx/scripts/polymtrx.py scan "fed rate cut" --emit markdown
+python3 skills/polymtrx/scripts/polymtrx.py trending
+python3 skills/polymtrx/scripts/polymtrx.py market fed-decision-in-july-181
+python3 skills/polymtrx/scripts/polymtrx.py doctor
+```
+
+## Opportunity score
+Blended 0–100 from six signals: `volume` (0.30), `timing` (0.20), `contest`
+(0.20), `liquidity` (0.15), `movement` (0.10), `longshot` (0.05). Tune in
+`WEIGHTS` / `score_market()`.
+
+## Provenance
+Seed idea: `x.com/polymtrx/status/2076318987441357033`. X paywalls automated
+reads, so the exact post text couldn't be captured; this implements the
+confirmed workflow category (Polymarket opportunity intelligence). Refine the
+scoring in `scripts/polymtrx.py` if the source describes a sharper method.
+
+## License
+MIT.
diff --git a/skills/polymtrx/SKILL.md b/skills/polymtrx/SKILL.md
new file mode 100644
index 0000000..7e1e637
--- /dev/null
+++ b/skills/polymtrx/SKILL.md
@@ -0,0 +1,119 @@
+---
+name: polymtrx
+description: 'Scan LIVE Polymarket prediction markets and rank them by tradeable-edge signals — real-money volume, order-book liquidity/spread, time-to-resolution, price contest (near 50/50), longshot mispricing, and recent movement — then synthesize a grounded read of where the best opportunity is right now. Runs a zero-dependency, no-key, $0-local Python engine over Polymarket''s public Gamma + CLOB APIs. READ-ONLY: it surfaces opportunities, it never trades, never touches a wallet or key. This skill should be used when the user asks what is happening on Polymarket, wants prediction-market odds/opportunities on a topic, asks "where is the edge / best bet", wants to see trending or most-active markets, asks about a specific market''s price/liquidity/spread, or invokes /polymtrx or mentions MTRX / Polymtrx / PolyMatrix. Not for placing trades, wallet operations, or non-Polymarket markets.'
+argument-hint: 'polymtrx scan "fed rate cut" | polymtrx trending | polymtrx market <slug> | polymtrx scan bitcoin --no-clob'
+allowed-tools: Bash, Read, WebSearch, AskUserQuestion
+license: MIT
+user-invocable: true
+metadata:
+ type: reference
+ inspired-by: MTRX (@Polymtrx) on X — the "prediction markets + AI to find the best opportunities" idea
+ engine: scripts/polymtrx.py
+ tags:
+ - polymarket
+ - prediction-markets
+ - opportunity-scanner
+ - odds
+ - trading-signals
+ - engagement-ranked
+ - read-only
+ - no-key
+ - zero-cost
+---
+
+# /polymtrx
+
+## Overview
+
+`/polymtrx` answers **"where is the best opportunity on Polymarket right now?"**
+It reproduces the MTRX / @Polymtrx (PolyMatrix) workflow — *prediction markets +
+signal analysis to surface the best opportunities* — as a deterministic engine
+that pulls **live** Polymarket data, scores each market on tradeable-edge
+signals, and hands back the raw numbers for the agent to reason over.
+
+This is a **research + signal contract**, not a keyword to improvise against.
+Always run the engine (never answer Polymarket odds from memory — they move by
+the minute). Then synthesize.
+
+> **Provenance note.** The seed idea is the tweet at
+> `x.com/polymtrx/status/2076318987441357033`. X paywalls automated reads, so
+> the exact technique in that specific post could not be captured verbatim; this
+> skill implements the confirmed workflow category (Polymarket opportunity
+> intelligence). If the source describes a sharper, more specific method,
+> tighten `scripts/polymtrx.py`'s scoring to match — the scaffold is built to be
+> refined, not frozen.
+
+## HARD RAIL — read-only
+
+The engine **only fetches public market data**. It never places an order, never
+touches a wallet / private key / CLOB credential, and never signs anything.
+Surfacing an opportunity is the whole job; deciding and acting is a human's (or a
+separately-gated step's). Do not extend this skill to auto-trade.
+
+## Cost
+
+**$0 (local).** Every call hits Polymarket's free public endpoints (Gamma +
+CLOB). No API keys. State `$0 (local)` when reporting results, per Steve's
+always-show-costs rule.
+
+## Workflow
+
+### Step 1 — pick the command
+| Intent | Command |
+| --- | --- |
+| Opportunities on a **topic** ("fed", "election", "bitcoin", a team) | `scan "<topic>"` |
+| **Most active** markets right now, no topic | `trending` |
+| Deep detail on **one** market (price, liquidity, spread, timing) | `market <slug-or-id>` |
+| Are the endpoints healthy? | `doctor` |
+
+### Step 2 — run the engine
+```bash
+python3 scripts/polymtrx.py scan "fed rate cut" --limit 12
+python3 scripts/polymtrx.py trending --emit markdown
+python3 scripts/polymtrx.py market fed-decision-in-july-181
+python3 scripts/polymtrx.py scan "election" --no-clob # faster, skips order-book depth
+```
+- `--emit markdown` for a human-readable brief; default `json` for machine consumption / chaining.
+- `--no-clob` skips the per-market order-book round-trip (faster; loses live spread/depth refinement).
+- `--limit N` caps how many ranked markets come back (default 12).
+
+The `market` command resolves a **market slug**, a **numeric id**, or falls back
+to an **event slug** (returns all of that event's markets). Slugs come from any
+Polymarket URL: `polymarket.com/event/<event-slug>`.
+
+### Step 3 — read the opportunity score
+Each market gets an `opportunity` score 0–100, blended from six signals (each
+0–1), and a `signals` breakdown so the reasoning is transparent:
+
+| Signal | Weight | Means |
+| --- | --- | --- |
+| `volume` | 0.30 | real money traded — conviction / it matters |
+| `timing` | 0.20 | near resolution — the edge window is concrete (0–45d) |
+| `contest` | 0.20 | price near 50/50 on a live binary — genuinely undecided |
+| `liquidity` | 0.15 | order-book depth + tight spread — you can actually get filled |
+| `movement` | 0.10 | |24h price change| — momentum / news just hit |
+| `longshot` | 0.05 | price hugging 0/1 — potential mispriced tail |
+
+Tune weights/thresholds in `scripts/polymtrx.py` (`WEIGHTS` + `score_market`) if
+the ranking needs to bias differently (e.g. weight `movement` higher for a
+news-reaction lens).
+
+### Step 4 — synthesize
+Do not just dump the JSON. Read the top markets and write a short grounded brief:
+which markets carry real money, what the current odds imply, where the price
+looks contestable vs. where it's a longshot, and **always cite the market URL**.
+Flag when a top-scorer resolves in `0d` (score is inflated by imminent timing —
+the edge may already be priced in).
+
+## When to use / not use
+- **Use** for: "what's Polymarket saying about X", "prediction-market odds on Y",
+ "where's the edge", "trending markets", a specific market's price/liquidity,
+ `/polymtrx`, or any mention of MTRX / Polymtrx / PolyMatrix.
+- **Do not use** for: placing trades, wallet/key operations, non-Polymarket
+ markets (Kalshi → the `Ken` project), or evergreen facts.
+
+## Resources
+- `scripts/polymtrx.py` — the zero-dependency engine (fetch → normalize → score
+ → rank → emit). Runnable standalone; also readable/patchable for tuning.
+- `references/api.md` — the public Polymarket endpoints used, their shapes, and
+ the field-coercion gotchas (JSON-encoded price arrays, string volumes).
diff --git a/skills/polymtrx/references/api.md b/skills/polymtrx/references/api.md
new file mode 100644
index 0000000..a91cc8b
--- /dev/null
+++ b/skills/polymtrx/references/api.md
@@ -0,0 +1,66 @@
+# Polymarket public API reference (as used by polymtrx)
+
+All endpoints below are **public and key-less**. `polymtrx` is **read-only** —
+it only issues GETs. No auth, no wallet, no order placement.
+
+## Gamma API — `https://gamma-api.polymarket.com`
+
+Market + event metadata. This is the primary source.
+
+### `GET /public-search?q=<topic>&limit_per_type=<n>&events_status=active`
+Full-text search. Returns `{ "events": [ { ..., "markets": [ ... ] } ], ... }`.
+Used by `scan`. Each event carries child `markets`; we flatten them.
+
+### `GET /markets?active=true&closed=false&archived=false&order=volume24hr&ascending=false&limit=<n>`
+List markets, orderable. Used by `trending` (ordered by 24h volume). Returns a
+JSON array (sometimes an object with a `markets` key — the engine handles both).
+
+### `GET /markets?slug=<market-slug>` · `GET /markets/<numeric-id>`
+Single-market lookup. Used by `market`. If the slug matches no market, the
+engine falls back to `GET /events?slug=<event-slug>` and returns that event's
+markets.
+
+### Key fields on a market object
+| Field | Notes |
+| --- | --- |
+| `question` / `groupItemTitle` | the market's headline |
+| `slug`, and parent event `slug` | build `polymarket.com/event/<event-slug>` |
+| `outcomePrices` | **often a JSON-encoded string** like `"[\"0.62\", \"0.38\"]"` — must be `json.loads`-ed; index 0 = YES price |
+| `outcomes` | same JSON-encoded-string gotcha |
+| `lastTradePrice` | fallback when `outcomePrices` absent |
+| `volume`, `liquidity` | **may be strings** — coerce to float; may live on the event, not the market |
+| `spread` | Gamma's own spread estimate (0..1); refined by CLOB when available |
+| `oneDayPriceChange` | signed 24h change (0..1) → `movement` signal |
+| `endDate` | ISO8601, may end in `Z` → resolution timing |
+| `clobTokenIds` | JSON-encoded string array of CLOB token ids; index 0 = YES token |
+| `active`, `closed` | filter out `closed` markets |
+
+## CLOB API — `https://clob.polymarket.com`
+
+Live order book. Used **best-effort** to refine `spread` + `liquidity` for the
+top-ranked markets only (one round-trip each, threaded). Never core — if the
+shape shifts, the engine degrades gracefully (`clob_ok: false`) and keeps the
+Gamma-derived numbers.
+
+### `GET /ok`
+Health ping (used by `doctor`).
+
+### `GET /book?token_id=<clob-token-id>`
+Returns `{ "bids": [ {price,size}, ... ], "asks": [ ... ] }`.
+- best bid / best ask → spread = `|best_ask - best_bid|`
+- Σ(price × size) across both sides → a depth proxy blended into `liquidity`
+
+## Field-coercion gotchas (why `_parse_float` exists)
+Polymarket mixes types freely across and within endpoints:
+- prices arrive as `float`, `"0.62"` (string), or `"[\"0.62\",\"0.38\"]"` (JSON string).
+- `volume` / `liquidity` arrive as numbers **or** strings.
+- token ids arrive as a JSON-encoded string array.
+
+`_parse_float` and the JSON-string unwrapping in `_normalize_market` absorb all
+of these. If Polymarket adds a new encoding, extend those two spots.
+
+## Endpoints deliberately NOT used
+- **Data API** (`data-api.polymarket.com`) — user positions / trade history.
+ Out of scope: `polymtrx` scans *markets*, not accounts.
+- **Any authenticated / order-placement endpoint** — forbidden by the read-only
+ hard rail.
diff --git a/skills/polymtrx/scripts/polymtrx.py b/skills/polymtrx/scripts/polymtrx.py
new file mode 100644
index 0000000..2fea01a
--- /dev/null
+++ b/skills/polymtrx/scripts/polymtrx.py
@@ -0,0 +1,497 @@
+#!/usr/bin/env python3
+"""
+polymtrx — Polymarket prediction-market opportunity scanner.
+
+Reproduces the MTRX / @Polymtrx (PolyMatrix) workflow: pull LIVE Polymarket
+markets, rank them by tradeable-edge signals, and emit the raw numbers so an
+agent can synthesize a grounded read of "where is the best opportunity right
+now?".
+
+Signals (each 0..1, weighted into one opportunity score):
+ volume real-money $ traded → conviction / it matters
+ liquidity order-book depth + tight → you can actually get filled
+ spread (via CLOB, best-effort)
+ timing near resolution → the edge window is concrete
+ contest price near 50/50 on a → genuinely undecided, high info
+ high-volume binary
+ longshot price near 0/1 → potential mispriced tail
+ movement recent price change → momentum / news just hit
+
+HARD RAIL: read-only. This engine ONLY fetches public market data. It never
+places an order, never touches a wallet, private key, or CLOB credential, and
+never signs anything. It surfaces opportunities; a human (or a separately-gated
+step) decides and acts.
+
+Zero dependencies (Python 3.9+ stdlib only). No API keys. $0 — every call hits
+Polymarket's free public endpoints.
+
+Commands:
+ polymtrx.py scan "<topic>" rank markets matching a topic by opportunity
+ polymtrx.py trending rank the most active live markets (no topic)
+ polymtrx.py market <slug|id> deep detail on one market (prices, spread)
+ polymtrx.py doctor health-check every endpoint
+
+Usage:
+ polymtrx.py scan "fed rate cut" --limit 15
+ polymtrx.py trending --emit markdown
+ polymtrx.py scan "election" --no-clob # skip order-book depth (faster)
+ polymtrx.py market will-x-happen-2026
+
+Inspired by MTRX (@Polymtrx) on X. Independent, no-key, $0-local reimplementation
+of the public-data half of the "find the best opportunities" idea. See
+references/api.md for endpoint lineage.
+"""
+
+from __future__ import annotations
+
+import argparse
+import concurrent.futures
+import json
+import sys
+import urllib.error
+import urllib.parse
+import urllib.request
+from datetime import datetime, timezone
+
+USER_AGENT = "polymtrx/1.0 (+https://polymarket.com public data; read-only)"
+HTTP_TIMEOUT = 20 # seconds per request
+
+GAMMA = "https://gamma-api.polymarket.com"
+CLOB = "https://clob.polymarket.com"
+
+# Opportunity-score weights (sum need not be 1; score is normalized to 0..100).
+WEIGHTS = {
+ "volume": 0.30,
+ "liquidity": 0.15,
+ "timing": 0.20,
+ "contest": 0.20,
+ "longshot": 0.05,
+ "movement": 0.10,
+}
+
+
+# --------------------------------------------------------------------------- #
+# HTTP helpers (stdlib only) #
+# --------------------------------------------------------------------------- #
+def _get(url: str, timeout: int = HTTP_TIMEOUT) -> bytes:
+ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
+ return resp.read()
+
+
+def _get_json(url: str, timeout: int = HTTP_TIMEOUT):
+ return json.loads(_get(url, timeout).decode("utf-8", "replace"))
+
+
+def _now() -> datetime:
+ return datetime.now(timezone.utc)
+
+
+def _parse_float(v):
+ """Polymarket returns numbers, strings, and JSON-encoded lists. Coerce."""
+ if v is None:
+ return None
+ if isinstance(v, (int, float)):
+ return float(v)
+ if isinstance(v, str):
+ s = v.strip()
+ if s.startswith("["):
+ try:
+ arr = json.loads(s)
+ return float(arr[0]) if arr else None
+ except Exception:
+ return None
+ try:
+ return float(s)
+ except Exception:
+ return None
+ if isinstance(v, list) and v:
+ return _parse_float(v[0])
+ return None
+
+
+def _parse_dt(v):
+ if not v or not isinstance(v, str):
+ return None
+ s = v.replace("Z", "+00:00")
+ try:
+ dt = datetime.fromisoformat(s)
+ return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
+ except Exception:
+ return None
+
+
+# --------------------------------------------------------------------------- #
+# Scoring #
+# --------------------------------------------------------------------------- #
+def _clamp01(x: float) -> float:
+ return 0.0 if x < 0 else 1.0 if x > 1 else x
+
+
+def _log_scale(value: float, full: float) -> float:
+ """0 at value<=0, ~1 as value approaches `full` (log-ish, saturating)."""
+ if value <= 0:
+ return 0.0
+ import math
+
+ return _clamp01(math.log10(1 + value) / math.log10(1 + full))
+
+
+def score_market(m: dict) -> dict:
+ """Compute per-signal scores + a blended opportunity score for one market.
+
+ `m` is the normalized dict produced by _normalize_market().
+ """
+ vol = m.get("volume") or 0.0
+ liq = m.get("liquidity") or 0.0
+ spread = m.get("spread") # 0..1, may be None
+ price = m.get("yes_price") # 0..1, may be None
+ end = m.get("end_dt")
+ change = m.get("price_change_24h") # signed 0..1, may be None
+
+ s = {}
+
+ # volume: real money is the strongest signal. $1M volume ~= saturated.
+ s["volume"] = _log_scale(vol, 1_000_000)
+
+ # liquidity: depth (log) blended with tightness of spread if we have it.
+ depth = _log_scale(liq, 200_000)
+ if spread is not None:
+ tight = _clamp01(1.0 - (spread / 0.10)) # 10c spread -> 0
+ s["liquidity"] = 0.6 * depth + 0.4 * tight
+ else:
+ s["liquidity"] = depth
+
+ # timing: nearer resolution = sharper, concrete edge window. 0..45 days.
+ if end is not None:
+ days = (end - _now()).total_seconds() / 86400.0
+ if days < 0:
+ s["timing"] = 0.0 # already resolved / past
+ else:
+ s["timing"] = _clamp01(1.0 - (days / 45.0))
+ else:
+ s["timing"] = 0.0
+
+ # contest: a binary sitting near 50/50 is maximally undecided → high info.
+ if price is not None:
+ s["contest"] = _clamp01(1.0 - abs(price - 0.5) / 0.5)
+ # longshot: price hugging 0 or 1 → potential mispriced tail.
+ s["longshot"] = _clamp01((abs(price - 0.5) - 0.4) / 0.1)
+ else:
+ s["contest"] = 0.0
+ s["longshot"] = 0.0
+
+ # movement: |24h change|, 20c move ~= saturated.
+ if change is not None:
+ s["movement"] = _clamp01(abs(change) / 0.20)
+ else:
+ s["movement"] = 0.0
+
+ blended = sum(WEIGHTS[k] * s.get(k, 0.0) for k in WEIGHTS)
+ m["signals"] = {k: round(v, 3) for k, v in s.items()}
+ m["opportunity"] = round(100 * blended, 1)
+ return m
+
+
+# --------------------------------------------------------------------------- #
+# Normalization #
+# --------------------------------------------------------------------------- #
+def _normalize_market(mkt: dict, event: dict | None = None) -> dict:
+ """Flatten a Gamma market (+ optional parent event) into our shape."""
+ outcomes = mkt.get("outcomes")
+ if isinstance(outcomes, str):
+ try:
+ outcomes = json.loads(outcomes)
+ except Exception:
+ outcomes = None
+
+ prices = mkt.get("outcomePrices")
+ if isinstance(prices, str):
+ try:
+ prices = json.loads(prices)
+ except Exception:
+ prices = None
+
+ yes_price = None
+ if isinstance(prices, list) and prices:
+ yes_price = _parse_float(prices[0])
+ if yes_price is None:
+ yes_price = _parse_float(mkt.get("lastTradePrice"))
+
+ slug = mkt.get("slug") or (event.get("slug") if event else "") or ""
+ ev_slug = (event.get("slug") if event else "") or slug
+
+ vol = _parse_float(mkt.get("volume")) or (
+ _parse_float(event.get("volume")) if event else None
+ ) or 0.0
+ liq = _parse_float(mkt.get("liquidity")) or (
+ _parse_float(event.get("liquidity")) if event else None
+ ) or 0.0
+
+ return {
+ "question": mkt.get("question")
+ or mkt.get("groupItemTitle")
+ or (event.get("title") if event else "")
+ or "",
+ "slug": slug,
+ "event_slug": ev_slug,
+ "url": f"https://polymarket.com/event/{ev_slug}" if ev_slug else "",
+ "yes_price": yes_price,
+ "outcomes": outcomes,
+ "outcome_prices": [p for p in (prices or [])] if prices else None,
+ "volume": vol,
+ "liquidity": liq,
+ "spread": _parse_float(mkt.get("spread")),
+ "price_change_24h": _parse_float(mkt.get("oneDayPriceChange")),
+ "end": mkt.get("endDate") or (event.get("endDate") if event else None),
+ "end_dt": _parse_dt(mkt.get("endDate") or (event.get("endDate") if event else None)),
+ "clob_token_ids": mkt.get("clobTokenIds"),
+ "active": mkt.get("active"),
+ "closed": mkt.get("closed"),
+ }
+
+
+def _enrich_clob(m: dict) -> dict:
+ """Best-effort order-book depth + spread from CLOB for a YES token.
+
+ Never raises — CLOB shape shifts and this is a bonus signal, not core.
+ """
+ token_ids = m.get("clob_token_ids")
+ if isinstance(token_ids, str):
+ try:
+ token_ids = json.loads(token_ids)
+ except Exception:
+ token_ids = None
+ if not (isinstance(token_ids, list) and token_ids):
+ return m
+ tok = token_ids[0]
+ try:
+ book = _get_json(f"{CLOB}/book?token_id={urllib.parse.quote(str(tok))}", timeout=12)
+ bids = book.get("bids") or []
+ asks = book.get("asks") or []
+ best_bid = max((_parse_float(b.get("price")) for b in bids), default=None)
+ best_ask = min((_parse_float(a.get("price")) for a in asks), default=None)
+ if best_bid is not None and best_ask is not None:
+ m["spread"] = round(abs(best_ask - best_bid), 4)
+ depth = 0.0
+ for side in (bids, asks):
+ for lvl in side:
+ p = _parse_float(lvl.get("price")) or 0.0
+ sz = _parse_float(lvl.get("size")) or 0.0
+ depth += p * sz
+ if depth > 0:
+ # Blend CLOB depth into liquidity if Gamma gave us nothing.
+ m["liquidity"] = max(m.get("liquidity") or 0.0, depth)
+ m["clob_ok"] = True
+ except Exception as e: # noqa: BLE001
+ m["clob_ok"] = False
+ m["clob_error"] = str(e)[:120]
+ return m
+
+
+# --------------------------------------------------------------------------- #
+# Fetchers #
+# --------------------------------------------------------------------------- #
+def fetch_scan(topic: str, limit: int) -> list[dict]:
+ """Search live markets matching a topic via Gamma public-search."""
+ q = urllib.parse.quote(topic)
+ url = (
+ f"{GAMMA}/public-search?q={q}"
+ f"&limit_per_type={max(limit * 2, 20)}&events_status=active"
+ )
+ data = _get_json(url)
+ events = data.get("events", []) if isinstance(data, dict) else []
+ out: list[dict] = []
+ for ev in events:
+ for mkt in (ev.get("markets") or [])[:6]:
+ if mkt.get("closed"):
+ continue
+ out.append(_normalize_market(mkt, ev))
+ return out
+
+
+def fetch_trending(limit: int) -> list[dict]:
+ """Most active live markets by volume via Gamma /markets."""
+ url = (
+ f"{GAMMA}/markets?active=true&closed=false&archived=false"
+ f"&order=volume24hr&ascending=false&limit={max(limit * 2, 30)}"
+ )
+ data = _get_json(url)
+ markets = data if isinstance(data, list) else data.get("markets", [])
+ return [_normalize_market(m) for m in markets]
+
+
+def fetch_market(slug_or_id: str) -> list[dict]:
+ """Detail for one market by slug (preferred) or numeric id."""
+ if slug_or_id.isdigit():
+ url = f"{GAMMA}/markets/{slug_or_id}"
+ data = _get_json(url)
+ mk = data if isinstance(data, dict) else None
+ return [_normalize_market(mk)] if mk else []
+ url = f"{GAMMA}/markets?slug={urllib.parse.quote(slug_or_id)}"
+ data = _get_json(url)
+ markets = data if isinstance(data, list) else data.get("markets", [])
+ if not markets:
+ # Fall back to event slug → its markets.
+ ev = _get_json(f"{GAMMA}/events?slug={urllib.parse.quote(slug_or_id)}")
+ evs = ev if isinstance(ev, list) else ev.get("events", [])
+ out = []
+ for e in evs:
+ for m in e.get("markets") or []:
+ out.append(_normalize_market(m, e))
+ return out
+ return [_normalize_market(m) for m in markets]
+
+
+# --------------------------------------------------------------------------- #
+# Pipeline #
+# --------------------------------------------------------------------------- #
+def rank(markets: list[dict], limit: int, use_clob: bool) -> list[dict]:
+ # De-dupe by slug+question.
+ seen = set()
+ uniq = []
+ for m in markets:
+ key = (m.get("slug"), m.get("question"))
+ if key in seen:
+ continue
+ seen.add(key)
+ uniq.append(m)
+
+ # Pre-score to pick the top-N worth an (expensive) CLOB round-trip.
+ for m in uniq:
+ score_market(m)
+ uniq.sort(key=lambda x: x.get("opportunity", 0), reverse=True)
+ top = uniq[: max(limit, 1)]
+
+ if use_clob and top:
+ with concurrent.futures.ThreadPoolExecutor(max_workers=6) as ex:
+ list(ex.map(_enrich_clob, top))
+ for m in top:
+ score_market(m) # re-score with CLOB-enriched liquidity/spread
+ top.sort(key=lambda x: x.get("opportunity", 0), reverse=True)
+ return top
+
+
+# --------------------------------------------------------------------------- #
+# Emitters #
+# --------------------------------------------------------------------------- #
+def _fmt_pct(p):
+ return "—" if p is None else f"{p * 100:.0f}%"
+
+
+def emit_json(cmd: str, arg: str, rows: list[dict]) -> str:
+ return json.dumps(
+ {
+ "tool": "polymtrx",
+ "command": cmd,
+ "query": arg,
+ "generated_at": _now().isoformat(),
+ "cost": "$0 (local; Polymarket public APIs)",
+ "count": len(rows),
+ "markets": rows,
+ },
+ indent=2,
+ default=str, # datetimes (end_dt) → ISO string
+ )
+
+
+def emit_markdown(cmd: str, arg: str, rows: list[dict]) -> str:
+ lines = [
+ f"# polymtrx {cmd}" + (f' — "{arg}"' if arg else ""),
+ f"_generated {_now():%Y-%m-%d %H:%M UTC} · $0 (local) · {len(rows)} markets · read-only_",
+ "",
+ ]
+ if not rows:
+ lines.append("_No live markets matched._")
+ return "\n".join(lines)
+ for i, m in enumerate(rows, 1):
+ sig = m.get("signals", {})
+ end = m.get("end_dt")
+ when = f"{(end - _now()).days}d" if end else "—"
+ lines.append(f"### {i}. {m['question']} · **{m.get('opportunity', 0)}/100**")
+ lines.append(
+ f"- YES **{_fmt_pct(m.get('yes_price'))}** · vol ${m.get('volume', 0):,.0f} · "
+ f"liq ${m.get('liquidity', 0):,.0f} · "
+ f"spread {('—' if m.get('spread') is None else f'{m['spread']*100:.1f}c')} · "
+ f"resolves in {when}"
+ )
+ top_sig = sorted(sig.items(), key=lambda kv: kv[1], reverse=True)[:3]
+ lines.append("- top signals: " + ", ".join(f"{k} {v:.2f}" for k, v in top_sig))
+ if m.get("url"):
+ lines.append(f"- {m['url']}")
+ lines.append("")
+ return "\n".join(lines)
+
+
+# --------------------------------------------------------------------------- #
+# doctor #
+# --------------------------------------------------------------------------- #
+def doctor() -> int:
+ checks = [
+ ("gamma public-search", f"{GAMMA}/public-search?q=test&limit_per_type=1&events_status=active"),
+ ("gamma markets", f"{GAMMA}/markets?active=true&closed=false&limit=1"),
+ ("clob ok", f"{CLOB}/ok"),
+ ]
+ ok = True
+ print("polymtrx doctor — endpoint health ($0, read-only)\n")
+ for name, url in checks:
+ try:
+ _get(url, timeout=12)
+ print(f" ✅ {name}")
+ except Exception as e: # noqa: BLE001
+ ok = False
+ print(f" ❌ {name}: {str(e)[:100]}")
+ print("\nall good" if ok else "\nsome endpoints failed — see above")
+ return 0 if ok else 1
+
+
+# --------------------------------------------------------------------------- #
+# CLI #
+# --------------------------------------------------------------------------- #
+def main(argv: list[str]) -> int:
+ p = argparse.ArgumentParser(prog="polymtrx", add_help=True, description=__doc__)
+ p.add_argument("command", choices=["scan", "trending", "market", "doctor"])
+ p.add_argument("query", nargs="?", default="", help="topic (scan) or slug/id (market)")
+ p.add_argument("--limit", type=int, default=12, help="max markets to return")
+ p.add_argument("--emit", choices=["json", "markdown"], default="json")
+ p.add_argument("--no-clob", action="store_true", help="skip order-book depth (faster)")
+ args = p.parse_args(argv)
+
+ if args.command == "doctor":
+ return doctor()
+
+ use_clob = not args.no_clob
+ try:
+ if args.command == "scan":
+ if not args.query:
+ print("error: scan needs a topic, e.g. polymtrx scan \"fed rate\"", file=sys.stderr)
+ return 2
+ markets = fetch_scan(args.query, args.limit)
+ rows = rank(markets, args.limit, use_clob)
+ elif args.command == "trending":
+ markets = fetch_trending(args.limit)
+ rows = rank(markets, args.limit, use_clob)
+ elif args.command == "market":
+ if not args.query:
+ print("error: market needs a slug or id", file=sys.stderr)
+ return 2
+ markets = fetch_market(args.query)
+ rows = rank(markets, max(len(markets), 1), use_clob)
+ else: # pragma: no cover
+ return 2
+ except urllib.error.HTTPError as e:
+ print(f"HTTP {e.code} from Polymarket: {e.reason}", file=sys.stderr)
+ return 1
+ except Exception as e: # noqa: BLE001
+ print(f"error: {e}", file=sys.stderr)
+ return 1
+
+ out = emit_markdown(args.command, args.query, rows) if args.emit == "markdown" else emit_json(
+ args.command, args.query, rows
+ )
+ print(out)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))
(oldest)
·
back to Polymtrx Skill
·
(newest)