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.
|
|
4
|
-
"description": "
|
|
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
|
|
3
|
-
description:
|
|
4
|
-
|
|
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
|
|
11
|
+
# Polymarket Sports Betting
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
## ⚠️ CRITICAL RULES (Non-Negotiable)
|
|
10
14
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
+
## Supported Sports
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
44
|
+
## Scripts
|
|
42
45
|
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
##
|
|
57
|
+
## Workflow
|
|
58
58
|
|
|
59
|
-
###
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
Before presenting to the user, validate each pick:
|
|
109
88
|
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
97
|
+
### Step 3: PRESENT — Show Picks to User
|
|
118
98
|
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
Max daily exposure: 25% of bankroll
|
|
109
|
+
Token IDs:
|
|
110
|
+
- Boston Celtics: 123456789...
|
|
111
|
+
- LA Lakers: 987654321...
|
|
150
112
|
```
|
|
151
113
|
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
After user approval, execute trades using `trade.mjs`:
|
|
162
124
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
+
### Step 5: TRACK — Monitor Positions
|
|
179
140
|
|
|
180
|
-
|
|
141
|
+
Check current positions:
|
|
181
142
|
|
|
182
|
-
|
|
143
|
+
```bash
|
|
144
|
+
# Via Data API
|
|
145
|
+
curl "https://data-api.polymarket.com/positions?user=0xCa5e2a326DE9544EAe2810E3f0E4e1d4Cef1847b"
|
|
183
146
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
157
|
+
### Step 6: REDEEM — Collect Winnings
|
|
219
158
|
|
|
220
|
-
|
|
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
|
-
|
|
230
|
-
-
|
|
231
|
-
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
##
|
|
247
|
-
|
|
248
|
-
### Using the TypeScript SDK
|
|
173
|
+
## Error Handling
|
|
249
174
|
|
|
250
|
-
|
|
251
|
-
const { ClobClient, Side } = require('@polymarket/clob-client');
|
|
252
|
-
const { Wallet } = require('ethers');
|
|
175
|
+
### Common Issues
|
|
253
176
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
264
|
-
const creds = await client.createOrDeriveApiKey();
|
|
188
|
+
### The Odds API Quota
|
|
265
189
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
275
|
-
await client.cancelOrder(orderId);
|
|
195
|
+
---
|
|
276
196
|
|
|
277
|
-
|
|
278
|
-
const orders = await client.getOpenOrders();
|
|
279
|
-
```
|
|
197
|
+
## Examples
|
|
280
198
|
|
|
281
|
-
###
|
|
199
|
+
### "Scan for bets today"
|
|
282
200
|
|
|
283
201
|
```
|
|
284
|
-
1.
|
|
285
|
-
2.
|
|
286
|
-
3.
|
|
287
|
-
4.
|
|
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
|
-
###
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
---
|
|
310
|
-
|
|
311
|
-
## Position Management & Redemption
|
|
312
|
-
|
|
313
|
-
### Checking Positions
|
|
219
|
+
### "Check my positions"
|
|
314
220
|
|
|
315
221
|
```
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|