skills-ws 1.5.5 → 1.6.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "skills-ws",
3
- "version": "1.5.5",
4
- "description": "83 agent skills for AI coding assistants \u2014 marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.",
3
+ "version": "1.6.1",
4
+ "description": "85 agent skills for AI coding assistants \u2014 marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.",
5
5
  "scripts": {
6
6
  "test": "node test/cli.test.mjs"
7
7
  },
@@ -1,411 +1,244 @@
1
1
  ---
2
- name: polymarket-trading
3
- description: Polymarket prediction market trading — market analysis, edge calculation, bookmaker cross-referencing, order placement via CLOB API, position management, and redemption. Covers sports betting strategy, risk management, and the full Polymarket SDK workflow.
4
- version: 1.0.0
2
+ name: polymarket
3
+ description: >
4
+ Sports betting on Polymarket prediction markets. Scan bookmaker odds via The Odds API,
5
+ find high-conviction favorites (>70% implied probability), match to Polymarket markets,
6
+ and execute trades for better payouts than traditional bookmakers. Use when asked about
7
+ "polymarket", "sports betting", "place a bet", "scan for bets", "check my positions",
8
+ "redeem bets", "NBA odds", "football odds", "betting picks", or "what should I bet on".
5
9
  ---
6
10
 
7
- # Polymarket Trading
11
+ # Polymarket Sports Betting
8
12
 
9
- Complete framework for analyzing, trading, and managing positions on Polymarket — the world's largest prediction market.
13
+ ## ⚠️ CRITICAL RULES (Non-Negotiable)
10
14
 
11
- ## Table of Contents
15
+ 1. **SPORTS ONLY** — Never bet on crypto, politics, or geopolitics. Crypto markets are manipulated by insiders.
16
+ 2. **Favorites only** — Minimum **70% implied probability on bookmakers** (averaged across 20+ books via The Odds API).
17
+ 3. **No long shots** — Ever. Long shots = money pit.
18
+ 4. **The best trade is sometimes no trade** — If nothing qualifies, say so.
19
+ 5. **Every bet must be high conviction** — Target 70%+ win rate across all bets.
20
+ 6. **Verify Polymarket availability** — Many matches aren't listed on PM. Never recommend a bet without confirming the PM market exists and resolving the token ID.
21
+ 7. **Football 3-way markets are harder** — PM splits win/draw/lose probability differently than bookmakers. Flag this to the user.
12
22
 
13
- 1. [Market Analysis Framework](#market-analysis-framework)
14
- 2. [Edge Calculation](#edge-calculation)
15
- 3. [Bookmaker Cross-Referencing](#bookmaker-cross-referencing)
16
- 4. [Risk Management Rules](#risk-management-rules)
17
- 5. [APIs & Data Sources](#apis--data-sources)
18
- 6. [Trading via CLOB](#trading-via-clob)
19
- 7. [Position Management & Redemption](#position-management--redemption)
20
- 8. [Understanding Polymarket Mechanics](#understanding-polymarket-mechanics)
21
- 9. [Common Pitfalls](#common-pitfalls)
23
+ ## Wallet & Keys
22
24
 
23
- ---
24
-
25
- ## Market Analysis Framework
25
+ - **Address:** `0xCa5e2a326DE9544EAe2810E3f0E4e1d4Cef1847b`
26
+ - **Chain:** Polygon (USDC.e)
27
+ - **Positions API:** `https://data-api.polymarket.com/positions?user=0xCa5e2a326DE9544EAe2810E3f0E4e1d4Cef1847b`
28
+ - **Keys:** macOS Keychain, account `stuart`:
29
+ - `polymarket-wallet-key`, `polymarket-api-key`, `polymarket-api-secret`, `polymarket-api-passphrase`
30
+ - `odds-api-key` (The Odds API)
26
31
 
27
- ### The Scan Pipeline
32
+ ## Supported Sports
28
33
 
29
- For every potential bet, follow this pipeline in order:
30
-
31
- ```
32
- 1. Polymarket prices identify markets with volume > $10K
33
- 2. Filter bookmaker favorites > 65% implied probability
34
- 3. Injury/news check any material changes not priced in?
35
- 4. Form & H2H analysis → recent performance, matchup history
36
- 5. Cross-reference 3+ bookmaker sources → calculate true probability
37
- 6. Calculate edge only bet if edge > 10% vs Polymarket price
38
- 7. Size the bet → based on edge magnitude and bankroll
39
- ```
34
+ | The Odds API Key | Sport |
35
+ |---|---|
36
+ | `basketball_nba` | NBA |
37
+ | `soccer_epl` | English Premier League |
38
+ | `soccer_spain_la_liga` | La Liga |
39
+ | `soccer_italy_serie_a` | Serie A |
40
+ | `soccer_germany_bundesliga` | Bundesliga |
41
+ | `soccer_france_ligue_one` | Ligue 1 |
42
+ | `soccer_efl_champ` | EFL Championship |
40
43
 
41
- ### Market Selection Criteria
44
+ ## Scripts
42
45
 
43
- **Good markets:**
44
- - High volume (> $10K) — ensures liquidity for entry and exit
45
- - Near-term resolution (days, not months) — capital efficiency
46
- - Binary outcomes with clear resolution criteria
47
- - Markets where bookmaker odds exist for cross-referencing
46
+ All scripts are in the skill directory: `~/.agents/skills/polymarket/`
48
47
 
49
- **Bad markets:**
50
- - Low liquidity (< $5K volume) — wide spreads eat your edge
51
- - Subjective resolution criteria dispute risk
52
- - Markets with insider information advantage (crypto governance, company decisions)
53
- - Long-dated futures that tie up capital for months
48
+ | Script | Purpose | Auth Required |
49
+ |---|---|---|
50
+ | `scripts/scan.mjs` | Scan odds filter → match PM → present picks | No (read-only) |
51
+ | `polymarket.mjs` | Query PM markets (search, price, book, spread) | No |
52
+ | `trade.mjs` | Place buy/sell orders on PM | Yes |
53
+ | `redeem.mjs` | Redeem resolved winning positions | Yes |
54
54
 
55
55
  ---
56
56
 
57
- ## Edge Calculation
57
+ ## Workflow
58
58
 
59
- ### The Math
59
+ ### Step 1: SCAN — Fetch Bookmaker Odds
60
60
 
61
- ```
62
- Bookmaker Implied Probability = 1 / Decimal Odds
63
- Edge = (True Probability - Polymarket Price) / Polymarket Price × 100
61
+ Run the scan script to find qualifying bets:
64
62
 
65
- Example:
66
- Bookmaker odds: -225 (decimal 1.44) → Implied probability: 69.4%
67
- Polymarket price: $0.645 (64.5%)
68
- Edge = (69.4 - 64.5) / 64.5 × 100 = 7.6%
69
- ```
63
+ ```bash
64
+ # Scan all sports
65
+ node ~/.agents/skills/polymarket/scripts/scan.mjs --all-sports
70
66
 
71
- ### American Odds to Probability
67
+ # Single sport
68
+ node ~/.agents/skills/polymarket/scripts/scan.mjs --sport=basketball_nba
72
69
 
70
+ # Custom probability threshold
71
+ node ~/.agents/skills/polymarket/scripts/scan.mjs --all-sports --min-prob=0.75
73
72
  ```
74
- Negative odds (favorites): Probability = |odds| / (|odds| + 100)
75
- -225 → 225 / 325 = 69.2%
76
73
 
77
- Positive odds (underdogs): Probability = 100 / (odds + 100)
78
- +150 100 / 250 = 40.0%
79
- ```
74
+ The script:
75
+ 1. Fetches odds from The Odds API (h2h market, EU region, decimal format)
76
+ 2. Calculates implied probability per team (average of `1/decimal_odds` across all bookmakers)
77
+ 3. Filters to favorites above the threshold (default 70%)
78
+ 4. Searches Polymarket for matching moneyline markets
79
+ 5. Resolves the correct token ID for each favorite outcome
80
+ 6. Fetches current PM price
81
+ 7. Outputs a table with: Game, Favorite, Book Prob, PM Price, Edge, Token ID, Kickoff
80
82
 
81
- ### Removing the Vig
83
+ **Validation gate:** If the script returns no picks, stop. Tell the user "No qualifying bets today." Do not lower the threshold.
82
84
 
83
- Bookmaker odds include a margin (vig). To get true probabilities:
84
-
85
- ```
86
- 1. Convert both sides to implied probabilities
87
- 2. Sum them (will be > 100%, e.g., 105%)
88
- 3. Divide each by the sum to normalize to 100%
89
-
90
- Example:
91
- Team A: -200 → 66.7% Team B: +170 → 37.0%
92
- Sum: 103.7%
93
- True A: 66.7 / 103.7 = 64.3%
94
- True B: 37.0 / 103.7 = 35.7%
95
- ```
96
-
97
- ### Edge Thresholds
98
-
99
- | Edge | Action |
100
- |------|--------|
101
- | < 5% | Skip — too thin, transaction costs eat it |
102
- | 5-10% | Marginal — only if very high conviction + multiple sources agree |
103
- | **> 10%** | **Target zone — place the bet** |
104
- | > 20% | Strong edge — size up, but verify it's not a trap (news you missed?) |
105
-
106
- ---
85
+ ### Step 2: REVIEW Validate Picks
107
86
 
108
- ## Bookmaker Cross-Referencing
87
+ Before presenting to the user, validate each pick:
109
88
 
110
- ### Why Cross-Reference?
89
+ 1. **Book probability ≥ 70%** — Reconfirm the threshold is met
90
+ 2. **PM market exists** — Token ID was resolved (not `N/A`)
91
+ 3. **Edge is reasonable** — `Edge = Book Probability - PM Price`. Positive edge means PM is cheaper than books. Near-zero or negative edge is still fine (PM has better payouts than bookmakers regardless).
92
+ 4. **Game hasn't started** — Check kickoff time vs current time
93
+ 5. **Football 3-way caveat** — If it's a football match, note that draw probability affects the moneyline odds
111
94
 
112
- Polymarket prices are set by traders, not oddsmakers. They systematically:
113
- - **Overvalue favorites** by 3-7% in major sports markets
114
- - **Undervalue underdogs/draws** in 3-way football markets
115
- - **Lag behind** sharp bookmaker lines by hours
95
+ **Validation gate:** Remove any picks where token ID is `N/A` or the game has already started.
116
96
 
117
- ### Sources to Cross-Reference
97
+ ### Step 3: PRESENT — Show Picks to User
118
98
 
119
- | Source | Use | Notes |
120
- |--------|-----|-------|
121
- | **Pinnacle** | Sharpest lines globally | Gold standard, lowest vig |
122
- | **Bet365** | Popular, liquid markets | Good for mainstream sports |
123
- | **DraftKings/FanDuel** | US sports | NFL, NBA, MLB, NHL |
124
- | **Betfair Exchange** | True market prices | No vig, just commission |
125
- | **OddsPortal/OddsChecker** | Aggregators | Compare across 20+ books |
126
- | **Action Network** | Analysis + odds | Good injury/form context |
99
+ Present qualifying picks in a clean format:
127
100
 
128
- ### The 3-Source Rule
129
-
130
- Never bet based on a single bookmaker. Always confirm with **3+ independent sources**:
131
-
132
- ```
133
- ✅ Good: Pinnacle -220, Bet365 -225, DraftKings -215 → consensus ~69%
134
- Polymarket at 60¢ → 15% edge → BET
135
-
136
- ❌ Bad: Only one bookmaker has odds, others don't list the market
137
- → Information asymmetry, you might be wrong
138
101
  ```
102
+ 🏀 NBA Picks — March 8, 2026
139
103
 
140
- ---
141
-
142
- ## Risk Management Rules
143
-
144
- ### Bankroll Management
104
+ | Game | Pick | Book Prob | PM Price | Edge | Kickoff |
105
+ |------|------|-----------|----------|------|---------|
106
+ | BOS vs WAS | Boston Celtics | 88.2% | 0.87 | +1.2% | 19:00 ET |
107
+ | LAL vs DET | LA Lakers | 74.5% | 0.73 | +1.5% | 21:30 ET |
145
108
 
146
- ```
147
- Max single bet: 10% of bankroll
148
- Typical bet size: 2-5% of bankroll
149
- Max daily exposure: 25% of bankroll
109
+ Token IDs:
110
+ - Boston Celtics: 123456789...
111
+ - LA Lakers: 987654321...
150
112
  ```
151
113
 
152
- ### Hard Rules
114
+ Include:
115
+ - The sport and date
116
+ - Clear table of picks
117
+ - Token IDs for execution
118
+ - Any caveats (football 3-way, low liquidity)
119
+ - **Ask for explicit approval before executing any trade**
153
120
 
154
- 1. **Sport only**No crypto, politics, or geopolitics bets. Crypto markets are manipulated by insiders.
155
- 2. **Only heavy favorites** — Bookmaker implied probability > 65%
156
- 3. **Edge > 10%** — No exceptions for "gut feelings"
157
- 4. **3+ sources minimum** — Cross-reference before every bet
158
- 5. **No long shots** — Underdogs and parlays are money pits
159
- 6. **The best trade is sometimes no trade** — Don't force action
121
+ ### Step 4: EXECUTE Place Trades
160
122
 
161
- ### When NOT to Bet
123
+ After user approval, execute trades using `trade.mjs`:
162
124
 
163
- - Market is illiquid (< $5K volume, wide spreads)
164
- - News is breaking and odds haven't settled
165
- - You can't find 3 bookmakers listing the event
166
- - The edge comes from a single outlier source
167
- - You're chasing losses from a previous bet
168
- - Resolution criteria are ambiguous
125
+ ```bash
126
+ # Buy $50 on Boston Celtics at 0.87
127
+ node ~/.agents/skills/polymarket/trade.mjs buy <token_id> 0.87 50
128
+ ```
169
129
 
170
- ### Track Record Requirements
130
+ **Parameters:**
131
+ - `token_id` — From the scan output
132
+ - `price` — The PM price (or slightly above for guaranteed fill)
133
+ - `size` — Dollar amount to bet
171
134
 
172
- - Target **80%+ win rate** on individual bets
173
- - If below 60% over 10+ bets, stop and re-evaluate strategy
174
- - Log every bet: market, entry price, bookmaker consensus, edge, result
135
+ **Validation gate:** Always confirm with the user before executing. Show the exact command that will run.
175
136
 
176
- ---
137
+ **Post-execution:** Show the order response. If the order is rejected, explain why (insufficient balance, price moved, etc.).
177
138
 
178
- ## APIs & Data Sources
139
+ ### Step 5: TRACK — Monitor Positions
179
140
 
180
- ### Gamma API (Public, No Auth)
141
+ Check current positions:
181
142
 
182
- Market discovery and search. Base: `https://gamma-api.polymarket.com`
143
+ ```bash
144
+ # Via Data API
145
+ curl "https://data-api.polymarket.com/positions?user=0xCa5e2a326DE9544EAe2810E3f0E4e1d4Cef1847b"
183
146
 
184
- ```
185
- GET /public-search?q=<query> — Search markets/events
186
- GET /events?active=true&closed=false — List active events
187
- GET /events?tag_slug=<slug> — Events by category (sports, politics, crypto)
188
- GET /markets?slug=<slug> — Market details by slug
189
- GET /tags — All available categories
147
+ # Or via trade.mjs
148
+ node ~/.agents/skills/polymarket/trade.mjs balances
190
149
  ```
191
150
 
192
- **Key response fields:**
193
- - `outcomePrices` — Current Yes/No prices (JSON string, parse it)
194
- - `clobTokenIds` — Token IDs needed for CLOB trading (JSON string)
195
- - `volume` — Total dollar volume traded
196
- - `negRisk` — If true, uses negRisk contract (multi-outcome markets)
197
- - `groupItemTitle` — The outcome name in grouped markets
151
+ Check open orders:
198
152
 
199
- ### CLOB API (Public reads, Auth for trading)
200
-
201
- Order book and trading. Base: `https://clob.polymarket.com`
202
-
203
- ```
204
- # Public (no auth)
205
- GET /midpoint?token_id=<id> — Midpoint price
206
- GET /book?token_id=<id> — Full order book
207
- GET /spread?token_id=<id> — Bid-ask spread
208
- GET /price?token_id=<id>&side=buy|sell — Best available price
209
- GET /tick-size?token_id=<id> — Min price increment
210
-
211
- # Authenticated (requires L2 API key)
212
- POST /order — Place order
213
- DELETE /order/<id> — Cancel order
214
- GET /orders — Open orders
215
- GET /balances — CLOB balances
153
+ ```bash
154
+ node ~/.agents/skills/polymarket/trade.mjs orders
216
155
  ```
217
156
 
218
- ### Data API (Public, No Auth)
157
+ ### Step 6: REDEEM Collect Winnings
219
158
 
220
- Positions and history. Base: `https://data-api.polymarket.com`
159
+ After markets resolve, redeem winning positions:
221
160
 
161
+ ```bash
162
+ node ~/.agents/skills/polymarket/redeem.mjs
222
163
  ```
223
- GET /positions?user=<wallet_address> — All positions for a wallet
224
- GET /trades?user=<wallet_address> — Trade history
225
- ```
226
-
227
- ### Authentication Model
228
164
 
229
- Two-layer auth system:
230
- - **L1 (Wallet Signature)**: EIP-712 signature from your Polygon wallet — used to derive API credentials
231
- - **L2 (API Key)**: HMAC-SHA256 headers for all trading operations
232
-
233
- **Headers for authenticated requests:**
234
- ```
235
- POLY_ADDRESS — Wallet address
236
- POLY_SIGNATURE — HMAC signature of request
237
- POLY_TIMESTAMP — Unix timestamp
238
- POLY_NONCE — Request nonce
239
- POLY_API_KEY — Your API key
240
- POLY_PASSPHRASE — Your passphrase
241
- POLY_SECRET — Your API secret (used for HMAC)
242
- ```
165
+ The script automatically:
166
+ - Finds all redeemable positions
167
+ - Handles both standard and negativeRisk markets
168
+ - Redeems via CTF contract on Polygon
169
+ - Reports the new USDC.e balance
243
170
 
244
171
  ---
245
172
 
246
- ## Trading via CLOB
247
-
248
- ### Using the TypeScript SDK
173
+ ## Error Handling
249
174
 
250
- ```javascript
251
- const { ClobClient, Side } = require('@polymarket/clob-client');
252
- const { Wallet } = require('ethers');
175
+ ### Common Issues
253
176
 
254
- // Initialize client
255
- const wallet = new Wallet(PRIVATE_KEY);
256
- const client = new ClobClient(
257
- 'https://clob.polymarket.com',
258
- 137, // Polygon chainId
259
- wallet,
260
- creds // { apiKey, secret, passphrase }
261
- );
177
+ | Error | Cause | Fix |
178
+ |---|---|---|
179
+ | `security: SecItemCopyMatching` | Keychain access denied | Run in terminal with Keychain access, or unlock Keychain |
180
+ | `HTTP 401` from Odds API | Invalid/expired API key | Check `security find-generic-password -s odds-api-key -a stuart -w` |
181
+ | `HTTP 429` from Odds API | Rate limited (500 req/month free tier) | Wait, or check remaining quota in response headers |
182
+ | Token ID `N/A` | PM doesn't have this market | Skip this pick — common for smaller football matches |
183
+ | `No results` from PM search | Team name mismatch | Try alternate team names or search manually via `polymarket.mjs search` |
184
+ | Order rejected | Price moved or insufficient USDC.e | Check balance, adjust price, retry |
185
+ | `NONCE_TOO_LOW` | Transaction nonce conflict | Wait 30s, retry |
186
+ | Redeem fails | Gas price spike on Polygon | Retry with higher gas — script uses 500 gwei max fee |
262
187
 
263
- // Derive API credentials (first time)
264
- const creds = await client.createOrDeriveApiKey();
188
+ ### The Odds API Quota
265
189
 
266
- // Place a limit buy order
267
- const order = await client.createAndPostOrder({
268
- tokenID: '<token_id>', // From Gamma API clobTokenIds
269
- price: 0.65, // Max price willing to pay
270
- size: 10, // Dollar amount
271
- side: Side.BUY,
272
- }, { tickSize: '0.01' }); // Check tick-size endpoint first
190
+ The free tier has 500 requests/month. Each scan of one sport = 1 request. A full `--all-sports` scan = 7 requests. Be mindful:
191
+ - Don't scan repeatedly in the same hour
192
+ - Check `x-requests-remaining` header in responses
193
+ - If close to limit, scan only the sport the user asks about
273
194
 
274
- // Cancel an order
275
- await client.cancelOrder(orderId);
195
+ ---
276
196
 
277
- // Get open orders
278
- const orders = await client.getOpenOrders();
279
- ```
197
+ ## Examples
280
198
 
281
- ### Order Flow
199
+ ### "Scan for bets today"
282
200
 
283
201
  ```
284
- 1. Search market on Gamma → get slug
285
- 2. Get market details extract clobTokenIds and outcomePrices
286
- 3. Identify the outcome you want (Yes token ID vs No token ID)
287
- 4. Check tick-size for that token
288
- 5. Check current best price: GET /price?token_id=X&side=buy
289
- 6. Place limit order at your target price
290
- 7. Monitor: GET /orders to check if filled
202
+ 1. Run: node ~/.agents/skills/polymarket/scripts/scan.mjs --all-sports
203
+ 2. Review output for qualifying picks
204
+ 3. Present picks table to user
205
+ 4. Wait for approval before trading
291
206
  ```
292
207
 
293
- ### Token ID Selection
294
-
295
- Grouped markets (like "Who wins UFC 326?") have multiple outcomes. Each outcome has a Yes and No token:
208
+ ### "Bet $100 on the Celtics"
296
209
 
297
210
  ```
298
- Market: "UFC 326 Main Event"
299
- Outcome: "Max Holloway"
300
- Yes Token ID: 7068099725... (buy this if you think Holloway wins)
301
- No Token ID: 1293847561... (buy this if you think Holloway loses)
302
- Outcome: "Charles Oliveira"
303
- Yes Token ID: 8843920183...
304
- → No Token ID: 5567382910...
211
+ 1. Search PM for the Celtics game: node ~/.agents/skills/polymarket/polymarket.mjs search "Celtics"
212
+ 2. Find the moneyline market and resolve the token ID for the Celtics outcome
213
+ 3. Get current price: node ~/.agents/skills/polymarket/polymarket.mjs price <token_id>
214
+ 4. Cross-check bookmaker odds to confirm >70% implied probability
215
+ 5. Present the trade: "Buy $100 on Celtics at 0.XX (PM), books have them at YY%"
216
+ 6. After approval: node ~/.agents/skills/polymarket/trade.mjs buy <token_id> <price> 100
305
217
  ```
306
218
 
307
- The Gamma API returns `clobTokenIds` as a JSON string with `[NoTokenId, YesTokenId]` — **index 1 is Yes**.
308
-
309
- ---
310
-
311
- ## Position Management & Redemption
312
-
313
- ### Checking Positions
219
+ ### "Check my positions"
314
220
 
315
221
  ```
316
- GET https://data-api.polymarket.com/positions?user=<wallet_address>
222
+ 1. Fetch: curl "https://data-api.polymarket.com/positions?user=0xCa5e2a326DE9544EAe2810E3f0E4e1d4Cef1847b"
223
+ 2. Show open positions with current value
224
+ 3. If any are redeemable: node ~/.agents/skills/polymarket/redeem.mjs
317
225
  ```
318
226
 
319
- Returns all current positions with:
320
- - `asset` — Token ID
321
- - `size` — Number of shares
322
- - `avgPrice` — Average entry price
323
- - `currentPrice` — Current market price
324
- - `pnl` — Unrealized P&L
325
-
326
- ### Redeeming Resolved Positions
227
+ ### "What are the odds on Real Madrid?"
327
228
 
328
- When a market resolves, winning shares are worth $1.00. You need to call the contract to redeem:
329
-
330
- **Standard markets** (2-outcome, `negRisk: false`):
331
- ```javascript
332
- // Call ConditionalTokens contract: redeemPositions()
333
- const CTF_ADDRESS = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
334
229
  ```
335
-
336
- **NegRisk markets** (multi-outcome, `negRisk: true`):
337
- ```javascript
338
- // Call NegRiskAdapter: redeemPositions()
339
- const NEG_RISK_ADAPTER = '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296';
340
- // Also call NegRiskCTFExchange for conversion
341
- const NEG_RISK_EXCHANGE = '0xC5d563A36AE78145C45a50134d48A1215220f80a';
230
+ 1. Scan La Liga: node ~/.agents/skills/polymarket/scripts/scan.mjs --sport=soccer_spain_la_liga
231
+ 2. Find Real Madrid in results
232
+ 3. If they qualify (>70%), present as a pick
233
+ 4. If they don't qualify, show the odds but warn it's below threshold
342
234
  ```
343
235
 
344
- ### Exit Strategies
345
-
346
- - **Winner**: Hold until resolution → redeem at $1.00
347
- - **Cut losses**: Sell on the CLOB if the market moves against you
348
- - **Take profit**: If price moved significantly in your favor before resolution, consider selling early to lock in gains and free capital
349
-
350
236
  ---
351
237
 
352
- ## Understanding Polymarket Mechanics
353
-
354
- ### How Prices Work
355
-
356
- - Prices = probabilities ($0.65 = market says 65% chance of Yes)
357
- - Markets resolve to $1.00 (correct outcome) or $0.00 (incorrect)
358
- - Your profit = $1.00 - entry price (per share, if you win)
359
- - Your loss = entry price (per share, if you lose)
360
-
361
- ### Where Polymarket Misprices
362
-
363
- | Pattern | Why | How to Exploit |
364
- |---------|-----|----------------|
365
- | Favorites overvalued by 3-7% | Retail bias toward "safe" bets | Compare vs sharp bookmaker lines |
366
- | Underdogs/draws undervalued | People avoid complexity | 3-way football markets (win/draw/lose) |
367
- | Slow to react to news | Traders aren't 24/7 | Fast reaction to injury reports, lineups |
368
- | Low-volume markets inefficient | Not enough informed traders | Small edges in niche markets |
369
-
370
- ### Polymarket vs Bookmakers
371
-
372
- | Feature | Polymarket | Traditional Bookmaker |
373
- |---------|-----------|----------------------|
374
- | Vig/margin | 0% (peer-to-peer) | 3-10% |
375
- | Liquidity | Variable | Guaranteed |
376
- | Resolution | Smart contract | Bookmaker decides |
377
- | Settlement | USDC on Polygon | Fiat |
378
- | Edge | Retail-driven inefficiencies | Sharp lines, hard to beat |
379
-
380
- ### Chain Details
381
-
382
- - **Chain**: Polygon (MATIC for gas, USDC.e for trading)
383
- - **USDC.e contract**: `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`
384
- - **Note**: Polymarket uses USDC.e (bridged), NOT native USDC
385
- - **Geo**: Restricted in some countries (US blocked, most of EU is fine)
386
-
387
- ---
238
+ ## Key Concepts
388
239
 
389
- ## Common Pitfalls
390
-
391
- ### Mistakes to Avoid
392
-
393
- 1. **Betting on crypto/politics markets** Insider manipulation is rampant (project teams, whale wallets, political operatives)
394
- 2. **Chasing long shots** — A 5¢ token that could pay $1 sounds amazing; it almost never hits
395
- 3. **Ignoring liquidity** — A "great price" means nothing if you can't exit
396
- 4. **Single-source analysis** — One bookmaker can be wrong; always cross-reference
397
- 5. **Overexposure** — Never have > 25% of bankroll in active bets
398
- 6. **Ignoring the vig** — Bookmaker odds include margin; remove it before comparing
399
- 7. **Trading illiquid markets** — Wide spreads (> 5¢) silently destroy your edge
400
- 8. **Holding long-dated positions** — Capital is locked; shorter resolution = better capital efficiency
401
- 9. **Not tracking results** — Without a log, you can't evaluate if your strategy works
402
- 10. **Emotional trading** — If you just lost, don't immediately place another bet
403
-
404
- ### ✅ Habits of Profitable Traders
405
-
406
- 1. Systematic scan pipeline for every bet (not ad-hoc)
407
- 2. Spreadsheet tracking all bets with entry, target, result, edge
408
- 3. Walk away when there's no edge — most days have no good bets
409
- 4. Focus on 1-2 sports you know deeply rather than spreading thin
410
- 5. Check injury reports, team news, and lineup confirmations before betting
411
- 6. Review win/loss ratio monthly and adjust thresholds if needed
240
+ - **Implied probability:** `1 / decimal_odds` — averaged across all bookmakers for consensus
241
+ - **Edge:** `Book Probability - PM Price` — positive means PM is cheaper than fair value
242
+ - **Why PM over bookmakers:** Better payouts (no vig baked in the same way), no KYC, instant settlement on Polygon
243
+ - **This isn't arbitrage:** PM tracks fair value closely. The strategy is picking safe winners and benefiting from PM's superior payout structure
244
+ - **Volume:** Bet size is unrestricted, but every bet must meet the conviction threshold
@@ -0,0 +1,495 @@
1
+ #!/usr/bin/env node
2
+ import os from 'os';
3
+ /**
4
+ * Polymarket Sports Betting Scanner
5
+ *
6
+ * Fetches bookmaker odds from The Odds API, filters to high-probability favorites,
7
+ * matches them to Polymarket markets, and outputs actionable picks.
8
+ *
9
+ * Usage:
10
+ * node scripts/scan.mjs --sport=basketball_nba
11
+ * node scripts/scan.mjs --all-sports
12
+ * node scripts/scan.mjs --all-sports --min-prob=0.75
13
+ */
14
+
15
+ import { execSync } from 'child_process';
16
+
17
+ // ── Config ──────────────────────────────────────────────────────────────────
18
+
19
+ const ODDS_API = 'https://api.the-odds-api.com/v4/sports';
20
+ const GAMMA = 'https://gamma-api.polymarket.com';
21
+ const CLOB = 'https://clob.polymarket.com';
22
+
23
+ const SPORTS = [
24
+ 'basketball_nba',
25
+ 'soccer_epl',
26
+ 'soccer_spain_la_liga',
27
+ 'soccer_italy_serie_a',
28
+ 'soccer_germany_bundesliga',
29
+ 'soccer_france_ligue_one',
30
+ 'soccer_efl_champ',
31
+ ];
32
+
33
+ // Map Odds API sport keys to PM search tags / keywords
34
+ const SPORT_META = {
35
+ basketball_nba: { pmTag: 'nba', label: 'NBA', type: 'basketball' },
36
+ soccer_epl: { pmTag: 'epl', label: 'EPL', type: 'soccer' },
37
+ soccer_spain_la_liga: { pmTag: 'la-liga', label: 'La Liga', type: 'soccer' },
38
+ soccer_italy_serie_a: { pmTag: 'serie-a', label: 'Serie A', type: 'soccer' },
39
+ soccer_germany_bundesliga:{ pmTag: 'bundesliga', label: 'Bundesliga', type: 'soccer' },
40
+ soccer_france_ligue_one: { pmTag: 'ligue-1', label: 'Ligue 1', type: 'soccer' },
41
+ soccer_efl_champ: { pmTag: 'efl-championship', label: 'EFL Champ', type: 'soccer' },
42
+ };
43
+
44
+ // ── Helpers ─────────────────────────────────────────────────────────────────
45
+
46
+ function getOddsApiKey() {
47
+ // Preferred: explicit env var (portable for Linux/CI/containers)
48
+ if (process.env.ODDS_API_KEY && process.env.ODDS_API_KEY.trim()) {
49
+ return process.env.ODDS_API_KEY.trim();
50
+ }
51
+
52
+ // Fallback: macOS Keychain for local developer setup
53
+ if (process.platform === 'darwin') {
54
+ try {
55
+ return execSync(`security find-generic-password -s odds-api-key -a ${os.userInfo().username} -w`, {
56
+ encoding: 'utf8',
57
+ }).trim();
58
+ } catch {}
59
+ }
60
+
61
+ throw new Error('Missing ODDS_API_KEY. Set environment variable ODDS_API_KEY (or use macOS keychain fallback).');
62
+ }
63
+
64
+ async function fetchJSON(url, { timeoutMs = 12000, retries = 2 } = {}) {
65
+ let lastErr;
66
+
67
+ for (let attempt = 0; attempt <= retries; attempt++) {
68
+ const controller = new AbortController();
69
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
70
+
71
+ try {
72
+ const res = await fetch(url, {
73
+ headers: { Accept: 'application/json' },
74
+ signal: controller.signal,
75
+ });
76
+ if (!res.ok) {
77
+ const body = await res.text().catch(() => '');
78
+ throw new Error(`HTTP ${res.status} from ${url}: ${body.slice(0, 200)}`);
79
+ }
80
+ return await res.json();
81
+ } catch (e) {
82
+ lastErr = e;
83
+ if (attempt < retries) {
84
+ await new Promise(r => setTimeout(r, 400 * (attempt + 1)));
85
+ }
86
+ } finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+
91
+ throw lastErr;
92
+ }
93
+
94
+ function parseArgs() {
95
+ const opts = { sports: [], minProb: 0.70, allSports: false };
96
+ for (const a of process.argv.slice(2)) {
97
+ if (a === '--all-sports') opts.allSports = true;
98
+ else if (a.startsWith('--sport=')) opts.sports.push(a.split('=')[1]);
99
+ else if (a.startsWith('--min-prob=')) opts.minProb = parseFloat(a.split('=')[1]);
100
+ else if (a === '--help' || a === '-h') {
101
+ console.log(`Usage: scan.mjs [--sport=basketball_nba] [--all-sports] [--min-prob=0.70]`);
102
+ process.exit(0);
103
+ }
104
+ }
105
+ if (opts.allSports) opts.sports = [...SPORTS];
106
+ if (opts.sports.length === 0) opts.sports = [...SPORTS]; // default to all
107
+ return opts;
108
+ }
109
+
110
+ // ── Odds API ────────────────────────────────────────────────────────────────
111
+
112
+ async function fetchOdds(sport, apiKey) {
113
+ // Filter to games starting from now until end of tomorrow (48h window)
114
+ const now = new Date();
115
+ const tomorrow = new Date(now);
116
+ tomorrow.setDate(tomorrow.getDate() + 2);
117
+ tomorrow.setHours(23, 59, 59, 999);
118
+
119
+ const params = new URLSearchParams({
120
+ apiKey,
121
+ regions: 'eu',
122
+ markets: 'h2h',
123
+ oddsFormat: 'decimal',
124
+ commenceTimeFrom: now.toISOString().replace(/\.\d{3}Z$/, 'Z'),
125
+ commenceTimeTo: tomorrow.toISOString().replace(/\.\d{3}Z$/, 'Z'),
126
+ });
127
+
128
+ const url = `${ODDS_API}/${sport}/odds?${params}`;
129
+ try {
130
+ return await fetchJSON(url);
131
+ } catch (e) {
132
+ if (e.message.includes('429')) {
133
+ console.error(`⚠️ Rate limited on Odds API. Check your quota.`);
134
+ return [];
135
+ }
136
+ if (e.message.includes('404')) {
137
+ // Sport may have no upcoming events
138
+ return [];
139
+ }
140
+ throw e;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Calculate implied probability for each outcome by averaging 1/odds across bookmakers.
146
+ * Returns array of { team, impliedProb } sorted by probability descending.
147
+ */
148
+ function calcImpliedProbabilities(game) {
149
+ const bookmakers = game.bookmakers || [];
150
+ if (bookmakers.length === 0) return [];
151
+
152
+ // Collect all h2h odds per outcome
153
+ const oddsMap = {}; // team -> [decimal_odds, ...]
154
+ for (const bm of bookmakers) {
155
+ const h2h = bm.markets?.find(m => m.key === 'h2h');
156
+ if (!h2h) continue;
157
+ for (const outcome of h2h.outcomes) {
158
+ if (!oddsMap[outcome.name]) oddsMap[outcome.name] = [];
159
+ oddsMap[outcome.name].push(outcome.price);
160
+ }
161
+ }
162
+
163
+ // Average implied probability per team
164
+ const results = [];
165
+ for (const [team, oddsList] of Object.entries(oddsMap)) {
166
+ const avgProb = oddsList.reduce((sum, o) => sum + 1 / o, 0) / oddsList.length;
167
+ results.push({ team, impliedProb: avgProb, numBooks: oddsList.length });
168
+ }
169
+
170
+ return results.sort((a, b) => b.impliedProb - a.impliedProb);
171
+ }
172
+
173
+ // ── Polymarket Matching ─────────────────────────────────────────────────────
174
+
175
+ /**
176
+ * Normalize a team name for fuzzy matching.
177
+ */
178
+ function normalize(name) {
179
+ return name
180
+ .toLowerCase()
181
+ .replace(/\bfc\b/g, '')
182
+ .replace(/\bsc\b/g, '')
183
+ .replace(/\bac\b/g, '')
184
+ .replace(/\brc\b/g, '')
185
+ .replace(/\bssc\b/g, '')
186
+ .replace(/\bas\b/g, '')
187
+ .replace(/\bcf\b/g, '')
188
+ .replace(/\bud\b/g, '')
189
+ .replace(/\brcd\b/g, '')
190
+ .replace(/\breal\b/g, 'real')
191
+ .replace(/\bunited\b/g, 'utd')
192
+ .replace(/\bcity\b/g, 'city')
193
+ .replace(/[^a-z0-9 ]/g, '')
194
+ .replace(/\s+/g, ' ')
195
+ .trim();
196
+ }
197
+
198
+ /**
199
+ * Check if two team names are a fuzzy match.
200
+ */
201
+ function fuzzyMatch(a, b) {
202
+ const na = normalize(a);
203
+ const nb = normalize(b);
204
+ // Exact
205
+ if (na === nb) return true;
206
+ // One contains the other
207
+ if (na.includes(nb) || nb.includes(na)) return true;
208
+ // Last word match (e.g. "Lens" in "Racing Club de Lens")
209
+ const wordsA = na.split(' ');
210
+ const wordsB = nb.split(' ');
211
+ const lastA = wordsA[wordsA.length - 1];
212
+ const lastB = wordsB[wordsB.length - 1];
213
+ if (lastA.length >= 3 && lastA === lastB) return true;
214
+ // Check if any significant word (4+ chars) matches
215
+ for (const w of wordsA) {
216
+ if (w.length >= 4 && wordsB.includes(w)) return true;
217
+ }
218
+ return false;
219
+ }
220
+
221
+ /**
222
+ * Search Polymarket for a game and try to find a moneyline market.
223
+ * Returns { found, tokenId, pmPrice, marketQuestion, slug } or { found: false }
224
+ */
225
+ async function findPMMarket(homeTeam, awayTeam, favoriteTeam, sportMeta) {
226
+ // Extract short team names (last word = mascot) for better PM search
227
+ const shortHome = homeTeam.split(' ').pop();
228
+ const shortAway = awayTeam.split(' ').pop();
229
+ const shortFav = favoriteTeam.split(' ').pop();
230
+
231
+ // Try multiple search strategies — short names first (avoid season-long market noise)
232
+ const searches = [
233
+ `${shortAway} ${shortHome}`,
234
+ `${shortHome} ${shortAway}`,
235
+ `${homeTeam} ${awayTeam}`,
236
+ shortFav,
237
+ ];
238
+
239
+ for (const query of searches) {
240
+ try {
241
+ const params = new URLSearchParams({ q: query, limit_per_type: '10' });
242
+ const data = await fetchJSON(`${GAMMA}/public-search?${params}`);
243
+ const events = data.events || [];
244
+
245
+ for (const ev of events) {
246
+ // Check if event title contains both teams or the favorite
247
+ const title = (ev.title || '').toLowerCase();
248
+ const hasHome = fuzzyMatch(ev.title || '', homeTeam);
249
+ const hasAway = fuzzyMatch(ev.title || '', awayTeam);
250
+ const hasFav = fuzzyMatch(ev.title || '', favoriteTeam);
251
+
252
+ if (!hasFav && !(hasHome && hasAway)) continue;
253
+
254
+ // Look through markets for a moneyline / winner market
255
+ const markets = (ev.markets || []).filter(m => !m.closed);
256
+ for (const m of markets) {
257
+ const q = (m.question || '').toLowerCase();
258
+ // Skip spread, total, player props, 1st half
259
+ if (/spread|total|over|under|points|1st half|first half|player|assists|rebounds|goals scored/i.test(q)) continue;
260
+
261
+ // Check if this is a "Will X win?" or "X vs Y" moneyline
262
+ if (fuzzyMatch(m.question || '', favoriteTeam) || q.includes('win') || q.includes('winner')) {
263
+ // Parse outcomes and find the favorite
264
+ const outcomes = typeof m.outcomes === 'string' ? JSON.parse(m.outcomes) : (m.outcomes || []);
265
+ const prices = m.outcomePrices ? (typeof m.outcomePrices === 'string' ? JSON.parse(m.outcomePrices) : m.outcomePrices) : [];
266
+ const clobIds = m.clobTokenIds ? (typeof m.clobTokenIds === 'string' ? JSON.parse(m.clobTokenIds) : m.clobTokenIds) : [];
267
+
268
+ // Find the outcome matching the favorite
269
+ for (let i = 0; i < outcomes.length; i++) {
270
+ const outcomeName = outcomes[i];
271
+ if (fuzzyMatch(outcomeName, favoriteTeam) || outcomeName.toLowerCase() === 'yes') {
272
+ const tokenId = clobIds[i] || null;
273
+ const price = prices[i] ? parseFloat(prices[i]) : null;
274
+
275
+ if (tokenId && price) {
276
+ return {
277
+ found: true,
278
+ tokenId,
279
+ pmPrice: price,
280
+ marketQuestion: m.question,
281
+ slug: m.slug || ev.slug,
282
+ outcomeName,
283
+ };
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ } catch (e) {
291
+ // Search failed, try next strategy
292
+ continue;
293
+ }
294
+
295
+ // Small delay between searches to be nice to the API
296
+ await new Promise(r => setTimeout(r, 300));
297
+ }
298
+
299
+ return { found: false };
300
+ }
301
+
302
+ /**
303
+ * Get the live midpoint price from CLOB for a token.
304
+ */
305
+ async function getLivePrice(tokenId) {
306
+ try {
307
+ const data = await fetchJSON(`${CLOB}/midpoint?token_id=${tokenId}`);
308
+ return parseFloat(data.mid);
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+
314
+ // ── Main ────────────────────────────────────────────────────────────────────
315
+
316
+ async function main() {
317
+ const opts = parseArgs();
318
+
319
+ // 1. Get API key
320
+ let apiKey;
321
+ try {
322
+ apiKey = getOddsApiKey();
323
+ } catch (e) {
324
+ console.error('❌ Failed to get Odds API key.');
325
+ console.error(' Set ODDS_API_KEY env var (or use macOS keychain fallback).');
326
+ process.exit(1);
327
+ }
328
+
329
+ console.log(`\n🔍 Scanning ${opts.sports.length} sport(s) | Min probability: ${(opts.minProb * 100).toFixed(0)}%\n`);
330
+
331
+ const picks = [];
332
+
333
+ for (const sport of opts.sports) {
334
+ const meta = SPORT_META[sport];
335
+ if (!meta) {
336
+ console.error(`⚠️ Unknown sport: ${sport}`);
337
+ continue;
338
+ }
339
+
340
+ process.stdout.write(` 📡 ${meta.label}... `);
341
+
342
+ // 2. Fetch odds
343
+ let games;
344
+ try {
345
+ games = await fetchOdds(sport, apiKey);
346
+ } catch (e) {
347
+ console.log(`❌ ${e.message}`);
348
+ continue;
349
+ }
350
+
351
+ if (!games || games.length === 0) {
352
+ console.log('no upcoming games');
353
+ continue;
354
+ }
355
+
356
+ console.log(`${games.length} game(s)`);
357
+
358
+ // 3. Calculate implied probabilities and filter
359
+ for (const game of games) {
360
+ const probs = calcImpliedProbabilities(game);
361
+ if (probs.length === 0) continue;
362
+
363
+ const favorite = probs[0];
364
+ if (favorite.impliedProb < opts.minProb) continue;
365
+
366
+ // This is a qualifying favorite
367
+ const homeTeam = game.home_team;
368
+ const awayTeam = game.away_team;
369
+ const kickoff = new Date(game.commence_time);
370
+
371
+ // Skip games that have already started
372
+ if (kickoff < new Date()) continue;
373
+
374
+ // 4. Search Polymarket
375
+ process.stdout.write(` 🔎 ${favorite.team} (${(favorite.impliedProb * 100).toFixed(1)}%)... `);
376
+
377
+ const pm = await findPMMarket(homeTeam, awayTeam, favorite.team, meta);
378
+
379
+ if (!pm.found) {
380
+ console.log('not on PM');
381
+ picks.push({
382
+ sport: meta.label,
383
+ game: `${homeTeam} vs ${awayTeam}`,
384
+ favorite: favorite.team,
385
+ bookProb: favorite.impliedProb,
386
+ numBooks: favorite.numBooks,
387
+ pmPrice: null,
388
+ edge: null,
389
+ tokenId: null,
390
+ kickoff,
391
+ isSoccer: meta.type === 'soccer',
392
+ });
393
+ continue;
394
+ }
395
+
396
+ // 5. Get live CLOB price (more accurate than Gamma cache)
397
+ const livePrice = await getLivePrice(pm.tokenId);
398
+ const pmPrice = livePrice || pm.pmPrice;
399
+
400
+ const edge = favorite.impliedProb - pmPrice;
401
+
402
+ console.log(`✅ PM: ${(pmPrice * 100).toFixed(1)}¢ | Edge: ${edge >= 0 ? '+' : ''}${(edge * 100).toFixed(1)}%`);
403
+
404
+ picks.push({
405
+ sport: meta.label,
406
+ game: `${homeTeam} vs ${awayTeam}`,
407
+ favorite: favorite.team,
408
+ bookProb: favorite.impliedProb,
409
+ numBooks: favorite.numBooks,
410
+ pmPrice,
411
+ edge,
412
+ tokenId: pm.tokenId,
413
+ kickoff,
414
+ marketQuestion: pm.marketQuestion,
415
+ isSoccer: meta.type === 'soccer',
416
+ });
417
+
418
+ // Rate limit protection
419
+ await new Promise(r => setTimeout(r, 200));
420
+ }
421
+ }
422
+
423
+ // ── Output ──────────────────────────────────────────────────────────────
424
+
425
+ console.log('\n' + '═'.repeat(120));
426
+
427
+ const matched = picks.filter(p => p.tokenId);
428
+ const unmatched = picks.filter(p => !p.tokenId);
429
+
430
+ if (matched.length === 0 && unmatched.length === 0) {
431
+ console.log('\n😐 No favorites above threshold found across all sports.');
432
+ console.log(' The best trade is sometimes no trade.\n');
433
+ process.exit(0);
434
+ }
435
+
436
+ if (matched.length > 0) {
437
+ console.log(`\n✅ ACTIONABLE PICKS (${matched.length})\n`);
438
+ console.log(
439
+ padR('Game', 40) +
440
+ padR('Pick', 24) +
441
+ padR('Books', 10) +
442
+ padR('PM', 8) +
443
+ padR('Edge', 8) +
444
+ padR('Kickoff', 18) +
445
+ 'Token ID'
446
+ );
447
+ console.log('─'.repeat(120));
448
+
449
+ for (const p of matched) {
450
+ const kickStr = p.kickoff.toLocaleString('en-GB', {
451
+ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
452
+ timeZone: 'Europe/Luxembourg',
453
+ });
454
+ const soccer3way = p.isSoccer ? ' ⚽' : '';
455
+ console.log(
456
+ padR(p.game, 40) +
457
+ padR(p.favorite + soccer3way, 24) +
458
+ padR(`${(p.bookProb * 100).toFixed(1)}%`, 10) +
459
+ padR(`${(p.pmPrice * 100).toFixed(1)}¢`, 8) +
460
+ padR(`${p.edge >= 0 ? '+' : ''}${(p.edge * 100).toFixed(1)}%`, 8) +
461
+ padR(kickStr, 18) +
462
+ p.tokenId
463
+ );
464
+ }
465
+
466
+ if (matched.some(p => p.isSoccer)) {
467
+ console.log('\n⚽ = Football 3-way market (win/draw/lose) — PM splits probability differently than bookmakers');
468
+ }
469
+
470
+ console.log('\n📋 To execute a trade:');
471
+ console.log(' node ~/.agents/skills/polymarket/trade.mjs buy <token_id> <price> <size>\n');
472
+ }
473
+
474
+ if (unmatched.length > 0) {
475
+ console.log(`\n⚠️ NOT ON POLYMARKET (${unmatched.length})\n`);
476
+ for (const p of unmatched) {
477
+ const kickStr = p.kickoff.toLocaleString('en-GB', {
478
+ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
479
+ timeZone: 'Europe/Luxembourg',
480
+ });
481
+ console.log(` ${padR(p.game, 40)} ${padR(p.favorite, 20)} ${(p.bookProb * 100).toFixed(1)}% ${kickStr}`);
482
+ }
483
+ console.log('');
484
+ }
485
+ }
486
+
487
+ function padR(str, len) {
488
+ if (str.length >= len) return str.slice(0, len - 1) + ' ';
489
+ return str + ' '.repeat(len - str.length);
490
+ }
491
+
492
+ main().catch(e => {
493
+ console.error(`\n❌ Fatal: ${e.message}`);
494
+ process.exit(1);
495
+ });
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: reddit-community-engagement
3
+ description: Safer Reddit community engagement for researching subreddits, scanning relevant threads, checking rules, drafting helpful replies, deciding when to skip, and documenting outcomes. Use when work involves Reddit outreach, community participation, response drafting, moderation-risk checks, or planning brand-safe participation in subreddit discussions. Trigger on requests about Reddit growth, Reddit engagement, subreddit outreach, thread scanning, reply drafting, or posting guidance.
4
+ ---
5
+
6
+ # Reddit Community Engagement
7
+
8
+ Use Reddit to be useful first. Prefer **read** and **draft** modes. Use **post** mode only when the user explicitly wants it and subreddit rules allow it.
9
+
10
+ ## Operating modes
11
+
12
+ - **Read mode**: research subreddits, find relevant threads, summarize themes, capture rules, recommend whether to engage.
13
+ - **Draft mode**: prepare reply options for human review. This is the default for anything external-facing.
14
+ - **Post mode**: only after explicit user approval when rules, disclosure needs, and tone are all clear.
15
+
16
+ If anything is ambiguous, stay in read/draft mode.
17
+
18
+ ## Non-negotiables
19
+
20
+ - Do not pretend to be an ordinary user if you are acting for a company, client, or product.
21
+ - Do not invent personal experience, results, customers, or usage.
22
+ - Do not hide affiliation when disclosure is appropriate or required.
23
+ - Do not mass-post, reuse near-identical comments, or force product mentions into weak-fit threads.
24
+ - Do not argue with moderators. If content is removed or warned on, pause and reassess.
25
+
26
+ ## Before engaging
27
+
28
+ Capture these basics:
29
+
30
+ - Product / client:
31
+ - What it does in one sentence:
32
+ - Who it helps:
33
+ - Allowed disclosure language:
34
+ - Target subreddits or themes:
35
+ - Keywords / pain points to scan:
36
+ - Current mode: read / draft / post
37
+
38
+ If representing a brand, state it plainly in drafts when relevant, e.g. “I work on X” or “I’m with X.”
39
+
40
+ ## Account readiness
41
+
42
+ Warm up accounts through normal participation, not manipulation:
43
+
44
+ - Build a history of on-topic, non-promotional participation before any outreach.
45
+ - Prefer genuine comments in communities you expect to revisit.
46
+ - Keep early activity light and human-paced.
47
+ - Avoid links or product mentions until the account has normal-looking participation and you understand each subreddit’s norms.
48
+ - If the account is new, low-karma, or has removals, favor read mode or draft mode.
49
+
50
+ ## Rule and risk check
51
+
52
+ Before drafting any reply for a subreddit, verify:
53
+
54
+ 1. Sidebar / about / pinned rules
55
+ 2. Whether self-promotion, links, surveys, or company participation are restricted
56
+ 3. Whether user flair, account age, or karma minimums are required
57
+ 4. Whether the thread is asking for recommendations, troubleshooting help, comparison advice, or something unrelated
58
+ 5. Whether a reply from a brand rep would feel additive or intrusive
59
+
60
+ ## Decide: reply, value-only, or skip
61
+
62
+ ### Strong candidates
63
+
64
+ - The post clearly matches the product’s use case or expertise
65
+ - The user is asking for help, recommendations, or tool comparisons
66
+ - The subreddit allows this kind of participation
67
+ - You can answer the actual question even without mentioning the product
68
+
69
+ ### Value-only candidates
70
+
71
+ - The thread is relevant but promo rules are strict
72
+ - A direct answer helps, but mentioning the product adds risk
73
+ - Disclosure is still needed if speaking as a representative
74
+
75
+ ### Skip immediately
76
+
77
+ - Rules ban self-promo, brand accounts, or links and the reply would clearly be promotional
78
+ - The thread is grief-heavy, legal/medical/high-risk, hostile, or moderation-sensitive
79
+ - The product is only loosely relevant
80
+ - Another reply would be repetitive, opportunistic, or defensive
81
+ - You cannot be honest about affiliation without hurting trust or breaking norms
82
+
83
+ When in doubt, skip.
84
+
85
+ ## Drafting guidance
86
+
87
+ Write like a helpful participant, not an ad.
88
+
89
+ - Answer the question first
90
+ - Keep it specific to the post
91
+ - Use plain language; avoid slogans, hype, or CTA-heavy phrasing
92
+ - Mention the product only if it is genuinely useful and allowed
93
+ - Prefer no link unless the thread, rules, and user intent clearly support it
94
+ - If affiliated, disclose briefly and naturally
95
+ - Offer next-step help without pressure
96
+
97
+ ## Simple reply pattern
98
+
99
+ 1. Acknowledge the exact problem
100
+ 2. Give 1–3 practical points that help on their own
101
+ 3. If appropriate, add a brief disclosed mention of the product
102
+ 4. End with a low-pressure offer or clarifying question
103
+
104
+ ## Draft output format
105
+
106
+ For each candidate thread, produce:
107
+
108
+ - **Thread**: title + URL
109
+ - **Subreddit**:
110
+ - **Intent**: what the user seems to need
111
+ - **Rules / risk**: short note
112
+ - **Recommendation**: reply / value-only / skip
113
+ - **Why**: one or two sentences
114
+ - **Draft reply**: only for reply or value-only
115
+ - **Disclosure note**: exact wording if needed
116
+
117
+ ## Posting checklist
118
+
119
+ Only in explicit post mode:
120
+
121
+ - User approved the draft
122
+ - Rules were checked in this session
123
+ - Disclosure wording is appropriate
124
+ - No copied text from another thread
125
+ - Pace is conservative; avoid bursts
126
+ - Log the outcome after posting or attempted posting
127
+
128
+ ## Outcome logging
129
+
130
+ After a session, record a short summary with:
131
+
132
+ - Date
133
+ - Mode used
134
+ - Subreddits reviewed
135
+ - Threads scanned
136
+ - Drafts prepared
137
+ - Posts actually made
138
+ - Skips and why
139
+ - Any removals, warnings, or rule changes noticed
140
+ - Recommended next step
141
+
142
+ ## Good defaults
143
+
144
+ - Default to draft mode
145
+ - Default to no link
146
+ - Default to skip over borderline cases
147
+ - Default to transparency over cleverness