Build an OpenClaw skill that tracks line movements at sharp sportsbooks like Pinnacle, detects steam moves and reverse line movement, and tells your agent when smart money is shifting the market. One SKILL.md, a free API key, and a cron job — your agent follows the sharps automatically.
Why Your Agent Needs a Sharp Line Detector
Sportsbook odds don’t move randomly. When Pinnacle shifts a spread from -3 to -4.5 in 20 minutes, that’s a signal — a large, informed bettor (or syndicate) just bet enough to move the most efficient market in sports betting.
These are called steam moves, and they’re the closest thing to insider information that exists in legal sports betting. Professional bettors have tracked them manually for decades. Your agent can do it in real time.
The problem: The Odds API gives you a single snapshot of current odds. It doesn’t tell you what changed, how fast it changed, or whether the movement contradicts public sentiment. You need a skill that takes periodic snapshots, computes deltas, and flags significant moves — so your agent knows when to pay attention.
This guide builds sharp-line-detector — a Layer 4 (Intelligence) skill that sits on top of your odds-scanner data pipeline and adds the analytical layer. It monitors the books that sharp bettors actually respect, computes implied probability shifts, and surfaces actionable signals your agent can feed into downstream skills like kelly-sizer and ev-calculator.
What You’re Building
The sharp-line-detector skill teaches your OpenClaw agent four operations:
- Snapshot current lines — Store a timestamped record of odds from sharp books for a given sport
- Detect line movements — Compare current odds to the last snapshot and flag moves exceeding thresholds
- Compute implied probability shift — Quantify how much the market’s view of a game has changed
- Identify reverse line movement — Flag games where the line moved opposite to the expected public direction
The skill uses curl, jq, and inline python3 for math — no pip packages, no databases, just flat JSON files and shell commands.
Prerequisites
- OpenClaw installed and running — Any channel (WhatsApp, Telegram, Discord, WebChat)
- The Odds API key — Free at the-odds-api.com (500 requests/month)
- curl, jq, and python3 — Pre-installed on macOS and most Linux distributions
- odds-scanner skill — Recommended but not required. The sharp-line-detector fetches its own data
The Math Behind Sharp Line Detection
Implied Probability from American Odds
To compare moneyline moves meaningfully, convert American odds to implied probability:
For favorites (negative odds like -150):
implied_prob = abs(odds) / (abs(odds) + 100)
Example: -150 → 150 / 250 = 0.600 (60.0%)
For underdogs (positive odds like +130):
implied_prob = 100 / (odds + 100)
Example: +130 → 100 / 230 = 0.435 (43.5%)
Steam Move Detection
A steam move is flagged when:
|current_spread - snapshot_spread| >= SPREAD_THRESHOLD (default: 1.5 points)
OR
|current_ml_implied - snapshot_ml_implied| >= ML_THRESHOLD (default: 0.05 i.e. 5%)
The implied probability shift is more reliable than raw moneyline movement because the same dollar change on a -300 favorite is far less significant than on a +200 underdog. Converting to probability normalizes the signal.
Reverse Line Movement
Reverse line movement (RLM) is detected when:
line_direction = sign(current_spread - opening_spread)
expected_direction = toward the publicly popular side
RLM signal = (line_direction != expected_direction)
Since The Odds API doesn’t provide betting percentages directly, the skill uses a heuristic: the team with the shorter moneyline (bigger favorite) is assumed to be the public side. When the spread moves away from that favorite, it’s flagged as potential RLM.
The Complete SKILL.md
Create the skill directory:
mkdir -p ~/.openclaw/skills/sharp-line-detector
Create ~/.openclaw/skills/sharp-line-detector/SKILL.md with this content:
---
name: sharp-line-detector
description: "Monitor line movements at sharp sportsbooks (Pinnacle, Circa, Bookmaker). Detect steam moves, reverse line movement, and significant implied probability shifts. Use when asked about line movement, sharp money, steam moves, or smart money signals."
metadata:
openclaw:
emoji: "🔍"
requires:
bins: ["curl", "jq", "python3"]
credentials:
- id: "odds-api-key"
name: "The Odds API Key"
description: "Free API key from https://the-odds-api.com/"
env: "ODDS_API_KEY"
---
# Sharp Line Detector
Monitor line movements at sharp sportsbooks. Detect steam moves, reverse line movement, and smart money signals.
# When to Use
Use this skill when the user asks about:
- Line movement on a game or sport
- Steam moves or sharp action
- Reverse line movement (RLM) signals
- Whether smart money has moved a line
- Pinnacle, Circa, or Bookmaker line changes
- Significant odds shifts since open
# Sharp Book Keys
The skill prioritizes lines from these market-making books (in order of sharpness):
| Book | The Odds API Key | Why It Matters |
|------|-----------------|----------------|
| Pinnacle | pinnacle | Lowest vig, sharpest global market |
| Circa | circasports | Sharpest US book, accepts large limits |
| Bookmaker | bookmaker | Long-standing sharp-friendly book |
| BetOnline | betonlineag | Accepts sharp action, moves early |
| DraftKings | draftkings | High volume, useful as public benchmark |
| FanDuel | fanduel | High volume, useful as public benchmark |
If Pinnacle odds are not available in the API response, fall back to Circa, then Bookmaker.
# Configuration
Environment variables for tuning thresholds (all optional):
- SHARP_SPREAD_THRESHOLD — Minimum spread movement in points to flag (default: 1.5)
- SHARP_ML_THRESHOLD — Minimum moneyline implied probability shift to flag (default: 0.05 = 5%)
- SHARP_SNAPSHOT_DIR — Directory for snapshot files (default: /tmp/openclaw-sharp-snapshots)
# Operations
### 1. Snapshot Current Lines
Capture a timestamped snapshot of current odds from all books for a sport. Stores as JSON for later comparison.
```bash
SPORT_KEY="${SPORT_KEY:-basketball_nba}"
SNAP_DIR="${SHARP_SNAPSHOT_DIR:-/tmp/openclaw-sharp-snapshots}"
mkdir -p "$SNAP_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
curl -s "https://api.the-odds-api.com/v4/sports/$SPORT_KEY/odds?apiKey=$ODDS_API_KEY®ions=us,eu&markets=h2h,spreads&oddsFormat=american&bookmakers=pinnacle,circasports,bookmaker,betonlineag,draftkings,fanduel" \
| jq --arg ts "$TIMESTAMP" '{
timestamp: $ts,
sport: "'$SPORT_KEY'",
games: [.[] | {
id: .id,
game: "\(.away_team) @ \(.home_team)",
away: .away_team,
home: .home_team,
start: .commence_time,
books: [.bookmakers[] | {
key: .key,
name: .title,
h2h: [(.markets[] | select(.key=="h2h")).outcomes[] | {team: .name, odds: .price}],
spread: [(.markets[] | select(.key=="spreads")).outcomes[] | {team: .name, point: .point, odds: .price}]
}]
}]
}' > "$SNAP_DIR/${SPORT_KEY}_${TIMESTAMP}.json"
echo "Snapshot saved: ${SPORT_KEY}_${TIMESTAMP}.json ($(jq '.games | length' "$SNAP_DIR/${SPORT_KEY}_${TIMESTAMP}.json") games)"
```
### 2. Detect Line Movements
Compare current odds to the most recent snapshot. Flag games where spread or moneyline moved past thresholds.
```bash
SPORT_KEY="${SPORT_KEY:-basketball_nba}"
SNAP_DIR="${SHARP_SNAPSHOT_DIR:-/tmp/openclaw-sharp-snapshots}"
SPREAD_THRESH="${SHARP_SPREAD_THRESHOLD:-1.5}"
ML_THRESH="${SHARP_ML_THRESHOLD:-0.05}"
## Find latest snapshot
LATEST=$(ls -t "$SNAP_DIR"/${SPORT_KEY}_*.json 2>/dev/null | head -1)
if [ -z "$LATEST" ]; then
echo "No previous snapshot found for $SPORT_KEY. Run a snapshot first."
exit 1
fi
echo "Comparing against snapshot: $(basename $LATEST)"
## Fetch current odds
CURRENT=$(curl -s "https://api.the-odds-api.com/v4/sports/$SPORT_KEY/odds?apiKey=$ODDS_API_KEY®ions=us,eu&markets=h2h,spreads&oddsFormat=american&bookmakers=pinnacle,circasports,bookmaker,betonlineag,draftkings,fanduel")
## Compare using Python for math precision
python3 -c "
import json, sys
def american_to_implied(odds):
if odds < 0:
return abs(odds) / (abs(odds) + 100)
else:
return 100 / (odds + 100)
with open('$LATEST') as f:
snap = json.load(f)
current = json.loads('''$(echo "$CURRENT" | sed "s/'/'\\\\''/g")''')
spread_thresh = float('$SPREAD_THRESH')
ml_thresh = float('$ML_THRESH')
snap_lookup = {}
for g in snap['games']:
for b in g['books']:
if b['key'] == 'pinnacle' or (g['id'] not in snap_lookup):
snap_lookup[g['id']] = {
'game': g['game'],
'h2h': {o['team']: o['odds'] for o in b.get('h2h', [])},
'spread': {o['team']: {'point': o['point'], 'odds': o['odds']} for o in b.get('spread', [])}
}
alerts = []
for game in current:
gid = game['id']
if gid not in snap_lookup:
continue
old = snap_lookup[gid]
game_label = f\"{game['away_team']} @ {game['home_team']}\"
for bk in game.get('bookmakers', []):
if bk['key'] not in ('pinnacle', 'circasports', 'bookmaker'):
continue
# Check spread movement
for mkt in bk.get('markets', []):
if mkt['key'] == 'spreads':
for outcome in mkt['outcomes']:
team = outcome['name']
new_pt = outcome['point']
if team in old.get('spread', {}):
old_pt = old['spread'][team]['point']
delta = abs(new_pt - old_pt)
if delta >= spread_thresh:
alerts.append({
'type': 'STEAM_SPREAD',
'game': game_label,
'book': bk['title'],
'team': team,
'old': old_pt,
'new': new_pt,
'delta': delta
})
if mkt['key'] == 'h2h':
for outcome in mkt['outcomes']:
team = outcome['name']
new_odds = outcome['price']
if team in old.get('h2h', {}):
old_odds = old['h2h'][team]
old_imp = american_to_implied(old_odds)
new_imp = american_to_implied(new_odds)
shift = abs(new_imp - old_imp)
if shift >= ml_thresh:
direction = 'shortened' if new_imp > old_imp else 'lengthened'
alerts.append({
'type': 'STEAM_ML',
'game': game_label,
'book': bk['title'],
'team': team,
'old_odds': old_odds,
'new_odds': new_odds,
'prob_shift': f'{shift:.1%}',
'direction': direction
})
if alerts:
print(f'🚨 {len(alerts)} SHARP MOVE(S) DETECTED:')
print()
for a in alerts:
if a['type'] == 'STEAM_SPREAD':
print(f\" [{a['book']}] {a['game']}\")
print(f\" {a['team']} spread: {a['old']} → {a['new']} (moved {a['delta']} pts)\")
elif a['type'] == 'STEAM_ML':
print(f\" [{a['book']}] {a['game']}\")
print(f\" {a['team']} ML: {a['old_odds']} → {a['new_odds']} ({a['direction']} {a['prob_shift']})\")
print()
else:
print('No significant line movements detected since last snapshot.')
print(f'Thresholds: spread >= {spread_thresh} pts, ML >= {ml_thresh:.0%} implied prob shift')
"
```
### 3. Detect Reverse Line Movement
Identify games where the line moved opposite to the expected public side. Uses the heuristic that the bigger favorite attracts more public money.
```bash
SPORT_KEY="${SPORT_KEY:-basketball_nba}"
SNAP_DIR="${SHARP_SNAPSHOT_DIR:-/tmp/openclaw-sharp-snapshots}"
LATEST=$(ls -t "$SNAP_DIR"/${SPORT_KEY}_*.json 2>/dev/null | head -1)
if [ -z "$LATEST" ]; then
echo "No previous snapshot found. Run a snapshot first."
exit 1
fi
CURRENT=$(curl -s "https://api.the-odds-api.com/v4/sports/$SPORT_KEY/odds?apiKey=$ODDS_API_KEY®ions=us,eu&markets=h2h,spreads&oddsFormat=american&bookmakers=pinnacle,circasports,bookmaker,draftkings,fanduel")
python3 -c "
import json
def american_to_implied(odds):
if odds < 0:
return abs(odds) / (abs(odds) + 100)
else:
return 100 / (odds + 100)
with open('$LATEST') as f:
snap = json.load(f)
current = json.loads('''$(echo "$CURRENT" | sed "s/'/'\\\\''/g")''')
snap_lookup = {}
for g in snap['games']:
for b in g['books']:
if b['key'] == 'pinnacle' or (g['id'] not in snap_lookup):
snap_lookup[g['id']] = {
'game': g['game'],
'away': g['away'],
'home': g['home'],
'h2h': {o['team']: o['odds'] for o in b.get('h2h', [])},
'spread': {o['team']: o['point'] for o in b.get('spread', [])}
}
rlm_signals = []
for game in current:
gid = game['id']
if gid not in snap_lookup:
continue
old = snap_lookup[gid]
away = game['away_team']
home = game['home_team']
# Determine public side from moneyline (bigger favorite = more public)
current_h2h = {}
current_spread = {}
for bk in game.get('bookmakers', []):
if bk['key'] not in ('pinnacle', 'circasports'):
continue
for mkt in bk.get('markets', []):
if mkt['key'] == 'h2h':
for o in mkt['outcomes']:
current_h2h[o['name']] = o['price']
if mkt['key'] == 'spreads':
for o in mkt['outcomes']:
current_spread[o['name']] = o['point']
if not current_h2h or not current_spread:
continue
if home not in old.get('spread', {}) or home not in current_spread:
continue
# Public side = bigger favorite (lower implied odds / more negative spread)
home_imp = american_to_implied(current_h2h.get(home, 100))
away_imp = american_to_implied(current_h2h.get(away, 100))
public_side = home if home_imp > away_imp else away
# Check if spread moved TOWARD the public side (normal) or AWAY (RLM)
old_home_spread = old['spread'].get(home, 0)
new_home_spread = current_spread.get(home, 0)
spread_move = new_home_spread - old_home_spread
# If public is on home, spread should get more negative (more points given)
# If it got less negative (moved toward home), that's reverse
if public_side == home and spread_move > 0:
rlm_signals.append({
'game': f'{away} @ {home}',
'public_side': public_side,
'old_spread': old_home_spread,
'new_spread': new_home_spread,
'direction': 'against public (home fav)'
})
elif public_side == away and spread_move < 0:
rlm_signals.append({
'game': f'{away} @ {home}',
'public_side': public_side,
'old_spread': old_home_spread,
'new_spread': new_home_spread,
'direction': 'against public (away fav)'
})
if rlm_signals:
print(f'🔄 {len(rlm_signals)} REVERSE LINE MOVEMENT SIGNAL(S):')
print()
for s in rlm_signals:
print(f\" {s['game']}\")
print(f\" Public side: {s['public_side']}\")
print(f\" Home spread: {s['old_spread']} → {s['new_spread']}\")
print(f\" Signal: Line moved {s['direction']}\")
print()
else:
print('No reverse line movement detected.')
"
```
### 4. Check Snapshot History
List all stored snapshots for a sport with game counts and timestamps:
```bash
SPORT_KEY="${SPORT_KEY:-basketball_nba}"
SNAP_DIR="${SHARP_SNAPSHOT_DIR:-/tmp/openclaw-sharp-snapshots}"
echo "Snapshots for $SPORT_KEY:"
echo "---"
for f in $(ls -t "$SNAP_DIR"/${SPORT_KEY}_*.json 2>/dev/null | head -10); do
TS=$(jq -r '.timestamp' "$f")
GAMES=$(jq '.games | length' "$f")
echo " $TS — $GAMES games — $(basename $f)"
done
TOTAL=$(ls "$SNAP_DIR"/${SPORT_KEY}_*.json 2>/dev/null | wc -l)
echo "---"
echo "Total snapshots: $TOTAL"
```
# Output Rules
1. Always identify which sharp book's line is being reported (Pinnacle preferred)
2. Show movement as old → new with delta in both points and implied probability
3. Label steam moves with 🚨 and reverse line movement with 🔄
4. Include the snapshot timestamp so the user knows the comparison window
5. When no movements exceed thresholds, report the current thresholds and suggest adjusting if needed
6. Always report remaining API quota after fetching odds
# Error Handling
- If ODDS_API_KEY is not set, tell the user to get a free key at https://the-odds-api.com/
- If no snapshot exists for the requested sport, prompt the user to run a snapshot first
- If Pinnacle odds are not in the API response, fall back to Circa, then Bookmaker, and note which book is being used
- If rate limited (429), report remaining quota and suggest reducing snapshot frequency
- If the sport is out of season (empty response), inform the user and suggest an active sport
Set Your API Key
export ODDS_API_KEY=your_key_here
Optionally configure thresholds:
export SHARP_SPREAD_THRESHOLD=1.5 # points of spread movement to flag
export SHARP_ML_THRESHOLD=0.05 # 5% implied probability shift to flag
Add to your shell profile (~/.zshrc, ~/.bashrc) for persistence. OpenClaw reads environment variables from the host process at runtime.
Test It
Restart OpenClaw (or wait for hot-reload), then try these prompts:
| Prompt | Expected Behavior |
|---|---|
| “Snapshot NBA lines” | Fetches current odds from sharp books, saves timestamped JSON file |
| “Any sharp line movement on NBA games?” | Compares current odds to last snapshot, flags moves past thresholds |
| “Show me steam moves today” | Runs detect operation, reports spread and moneyline moves with magnitude |
| “Reverse line movement on NFL this week” | Identifies games where line moved against public-side favorite |
| “How many snapshots do I have for NBA?” | Lists stored snapshots with timestamps and game counts |
Your agent reads the SKILL.md, picks the right operation based on the prompt, substitutes the sport key, and runs the curl + jq + python pipeline.
How It Works Under the Hood
OpenClaw skills are not plugins that execute code directly. The SKILL.md is a set of instructions the LLM reads and follows at runtime.
When you ask “any sharp moves on NBA today?”, this is what happens:
User message → OpenClaw Gateway
→ LLM reads system prompt + active skills
→ LLM matches "sharp moves" to sharp-line-detector skill
→ LLM runs Operation 2 (Detect Line Movements) with sport key basketball_nba
→ OpenClaw executes curl to fetch current odds
→ Python script compares against last snapshot
→ Alerts printed with movement deltas
→ LLM formats output: which games moved, direction, magnitude, book source
The skill’s Python inline scripts handle the math that jq can’t — implied probability conversion and threshold comparison. By keeping the logic in python3 -c commands, the skill avoids any pip dependencies while still doing precise probability math.
Extending the Skill
Add Cron-Based Steam Move Alerts
The real power of sharp line detection is automated monitoring. Configure OpenClaw to snapshot and scan every 15 minutes during game days:
{
"cron": "*/15 * * * 0,1,4,5,6",
"prompt": "Snapshot NBA lines, then check for sharp line movements. If any steam moves detected, alert me with the game, direction, and magnitude."
}
On NFL Sundays, run tighter intervals during the critical pregame window (10 AM - 1 PM ET):
{
"cron": "*/10 10-13 * * 0",
"prompt": "Snapshot NFL lines and scan for steam moves and reverse line movement. Flag anything that moved more than 1 point on the spread at Pinnacle."
}
Chain with Other Skills
The sharp-line-detector generates signals that downstream skills can act on:
- odds-scanner — Provides the raw odds data that sharp-line-detector snapshots and compares
- ev-calculator — When a steam move is detected, estimate whether the new line still has positive expected value
- kelly-sizer — Size a position on the sharp side of a detected move based on your edge estimate and bankroll
- clv-tracker — Log your bet at the current line, then track whether the closing line validates the sharp signal
- arb-finder — After a sharp book moves, check if slower books haven’t caught up yet — creating temporary arbitrage windows
This is the intelligence layer of the Agent Betting Stack — your agent doesn’t just see odds, it understands which movements carry information.
Full Skill Series
This guide is part of the AgentBets OpenClaw Skills series. We’re building a complete library of betting-specific OpenClaw skills that map to the four layers of the Agent Betting Stack:
- Layer 1 — Identity: Agent reputation tracking via Moltbook
- Layer 2 — Wallet: Bankroll management, balance checking across platforms
- Layer 3 — Trading: Odds scanning, Polymarket monitoring, Kalshi tracking, arbitrage detection
- Layer 4 — Intelligence: EV calculation, Kelly sizing, CLV tracking, sharp line detection (this guide), news sentiment
Each skill is a standalone SKILL.md you can install independently or compose into a full autonomous betting pipeline.
What’s Next
- Sharp Betting Guide — Deep dive into sharp vs. square betting and how pros use line movement
- AgentBets Vig Index — Daily sportsbook efficiency rankings — see which books price sharpest
- Agent Betting Stack Overview — The four-layer framework these skills map to
- EV Calculator Skill — Calculate expected value for bets your sharp detector flags
- CLV Tracker Skill — Validate your sharp signals by measuring closing line value
