{"id":2886,"date":"2026-05-15T10:00:00","date_gmt":"2026-05-15T10:00:00","guid":{"rendered":"https:\/\/oddspapi.io\/blog\/?p=2886"},"modified":"2026-06-05T18:12:14","modified_gmt":"2026-06-05T18:12:14","slug":"steam-move-detector-python","status":"publish","type":"post","link":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/","title":{"rendered":"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API)"},"content":{"rendered":"<h2>What Is a Steam Move, and Why Should You Care?<\/h2>\n<p>A <strong>steam move<\/strong> is a sudden, sharp line movement at a professional bookmaker \u2014 usually Pinnacle \u2014 triggered by informed money. When Pinnacle shortens odds on an outcome, it means sharp bettors have hammered that side. Soft bookmakers (Bet365, DraftKings, FanDuel) take 30 seconds to several minutes to react. That lag window is where the edge lives.<\/p>\n<p>Tools like OddsJam charge $199+\/month for their &#8220;Sharp Money&#8221; screen. Sports Insights keeps their detection logic proprietary. Both are black boxes \u2014 you pay, you trust, you hope the signals are real.<\/p>\n<p>This tutorial builds an <strong>open-source steam move detector in Python<\/strong> that uses OddsPapi&#8217;s API to track Pinnacle line movements across 350+ bookmakers and alert you before soft books correct. Every line of code is tested against real data. You own the logic, you tune the parameters, and you can run it on the free tier.<\/p>\n<h2>How Steam Moves Work (The 90-Second Explainer)<\/h2>\n<p>Here&#8217;s a concrete example. Chelsea vs Manchester City, Full Time Result market:<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Time<\/th>\n<th>Event<\/th>\n<th>Pinnacle (Chelsea Win)<\/th>\n<th>Bet365 (Chelsea Win)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>T = 0<\/td>\n<td>Market open<\/td>\n<td>3.84<\/td>\n<td>3.60<\/td>\n<\/tr>\n<tr>\n<td>T = 30s<\/td>\n<td>Sharp money hits Pinnacle<\/td>\n<td><strong>3.55<\/strong><\/td>\n<td>3.60 (stale)<\/td>\n<\/tr>\n<tr>\n<td>T = 2min<\/td>\n<td>Bet365 corrects<\/td>\n<td>3.55<\/td>\n<td>3.50<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>At T = 30 seconds, Pinnacle dropped from 3.84 to 3.55 \u2014 a 0.29 move. Bet365 is still sitting at 3.60. That&#8217;s a <strong>steam move signal<\/strong>: the sharp book moved, the soft book hasn&#8217;t followed. You have a ~90-second window where Bet365&#8217;s price is stale relative to the true market price.<\/p>\n<p>Two signals matter: (1) the <strong>magnitude<\/strong> of the Pinnacle move, and (2) the <strong>lag<\/strong> before soft books follow. Bigger moves with longer lags = stronger signals.<\/p>\n<h2>Old Way vs OddsPapi<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Feature<\/th>\n<th>Scraping \/ Manual<\/th>\n<th>OddsJam ($199+\/mo)<\/th>\n<th>OddsPapi<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Bookmakers<\/strong><\/td>\n<td>2-3 you scrape<\/td>\n<td>~100 (US-focused)<\/td>\n<td>350+<\/td>\n<\/tr>\n<tr>\n<td><strong>Pinnacle included<\/strong><\/td>\n<td>No (blocked)<\/td>\n<td>No (US books only)<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td><strong>Detection logic<\/strong><\/td>\n<td>Your guess<\/td>\n<td>Proprietary black box<\/td>\n<td>Open source (this tutorial)<\/td>\n<\/tr>\n<tr>\n<td><strong>Alert delivery<\/strong><\/td>\n<td>None<\/td>\n<td>In-app only<\/td>\n<td>You own it (Discord\/Telegram)<\/td>\n<\/tr>\n<tr>\n<td><strong>changedAt timestamp<\/strong><\/td>\n<td>No<\/td>\n<td>No<\/td>\n<td>Every outcome, every book<\/td>\n<\/tr>\n<tr>\n<td><strong>Historical backtest<\/strong><\/td>\n<td>No<\/td>\n<td>Separate $79\/mo<\/td>\n<td>Free tier included<\/td>\n<\/tr>\n<tr>\n<td><strong>Cost<\/strong><\/td>\n<td>Your time + IP bans<\/td>\n<td>$199-999\/mo<\/td>\n<td>Free tier: 250 req\/mo<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>The key technical differentiator: OddsPapi returns a <code>changedAt<\/code> timestamp on every outcome for every bookmaker. This tells you exactly <em>when<\/em> a bookmaker last updated a price \u2014 without it, you&#8217;d need to poll continuously and diff prices yourself. With it, you can detect staleness in a single API call.<\/p>\n<h2>Build the Steam Move Detector: Step by Step<\/h2>\n<h3>Step 1: Setup and Configuration<\/h3>\n<pre class=\"wp-block-code\"><code>import requests\nimport time\nfrom datetime import datetime, timedelta, timezone\n\nAPI_KEY = \"YOUR_API_KEY\"\nBASE_URL = \"https:\/\/api.oddspapi.io\/v4\"\n\n# --- Detection parameters ---\nSHARP_BOOK = \"pinnacle\"\nSOFT_BOOKS = [\"bet365\", \"1xbet\", \"draftkings\", \"fanduel\", \"betmgm\", \"betway\"]\nSPORT_ID = 10              # Soccer (change to 11 for basketball, 14 for NFL, etc.)\nMARKET_ID = 101            # Full Time Result (1X2)\nSTEAM_THRESHOLD = 0.05     # Minimum price drop to flag as steam\nPOLL_INTERVAL = 90         # Seconds between polling cycles\nOUTCOME_NAMES = {\"101\": \"Home\", \"102\": \"Draw\", \"103\": \"Away\"}\n\ndef api_get(endpoint, params=None):\n    \"\"\"Authenticated API call with rate-limit safety.\"\"\"\n    if params is None:\n        params = {}\n    params[\"apiKey\"] = API_KEY\n    resp = requests.get(f\"{BASE_URL}\/{endpoint}\", params=params)\n    resp.raise_for_status()\n    time.sleep(0.2)\n    return resp.json()<\/code><\/pre>\n<p><code>STEAM_THRESHOLD<\/code> is the minimum price drop at Pinnacle to count as a steam move. A drop from 3.84 to 3.55 = 0.29, well above the 0.05 default. Tune this based on your risk tolerance \u2014 tighter thresholds mean fewer but stronger signals.<\/p>\n<h3>Step 2: Fetch Upcoming Fixtures<\/h3>\n<pre class=\"wp-block-code\"><code>def get_fixtures(sport_id, days_ahead=3):\n    \"\"\"Fetch upcoming fixtures with odds available.\"\"\"\n    today = datetime.now(timezone.utc).strftime(\"%Y-%m-%d\")\n    to_date = (datetime.now(timezone.utc) + timedelta(days=days_ahead)).strftime(\"%Y-%m-%d\")\n\n    fixtures = api_get(\"fixtures\", {\n        \"sportId\": sport_id,\n        \"from\": today,\n        \"to\": to_date\n    })\n\n    with_odds = [f for f in fixtures if f.get(\"hasOdds\")]\n    print(f\"Found {len(with_odds)} fixtures with odds (out of {len(fixtures)} total)\")\n    return with_odds<\/code><\/pre>\n<p>Only fixtures with <code>hasOdds: true<\/code> will return a <code>bookmakerOdds<\/code> payload from the <code>\/odds<\/code> endpoint. The <code>\/fixtures<\/code> endpoint supports a max 10-day range, but 3 days is enough for most use cases.<\/p>\n<h3>Step 3: Snapshot Builder \u2014 Capture Current Odds State<\/h3>\n<p>This is the core data-capture function. For each fixture, it grabs the current price <em>and<\/em> <code>changedAt<\/code> timestamp from Pinnacle plus your list of soft books.<\/p>\n<pre class=\"wp-block-code\"><code>def snapshot_odds(fixture_id, bookmakers):\n    \"\"\"\n    Capture price + changedAt for every outcome across bookmakers.\n    Returns: {slug: {outcome_id: {\"price\": float, \"changedAt\": datetime}}}\n    \"\"\"\n    snapshot = {}\n    all_books = list(bookmakers)\n\n    # Batch requests (API accepts comma-separated bookmaker slugs)\n    data = api_get(\"odds\", {\n        \"fixtureId\": fixture_id,\n        \"bookmakers\": \",\".join(all_books)\n    })\n\n    for slug, book_data in data.get(\"bookmakerOdds\", {}).items():\n        market = book_data.get(\"markets\", {}).get(str(MARKET_ID), {})\n        snapshot[slug] = {}\n\n        for oid, outcome in market.get(\"outcomes\", {}).items():\n            player = outcome.get(\"players\", {}).get(\"0\", {})\n            if not isinstance(player, dict) or \"price\" not in player:\n                continue\n\n            changed_raw = player.get(\"changedAt\", \"\")\n            changed_dt = None\n            if changed_raw:\n                changed_dt = datetime.fromisoformat(changed_raw)\n\n            snapshot[slug][oid] = {\n                \"price\": player[\"price\"],\n                \"changedAt\": changed_dt\n            }\n\n    return snapshot<\/code><\/pre>\n<p>The <code>changedAt<\/code> field is an ISO 8601 UTC timestamp on every outcome \u2014 it tells you the last time that specific bookmaker updated that specific price. This is what makes the entire detector work: you compare <code>changedAt<\/code> across sharp and soft books to detect staleness without maintaining your own price history.<\/p>\n<h3>Step 4: Steam Detection Engine<\/h3>\n<p>This is the algorithm. Compare two consecutive snapshots for the sharp book. If Pinnacle&#8217;s price dropped by more than the threshold, check each soft book: if the soft book&#8217;s <code>changedAt<\/code> is <em>older<\/em> than Pinnacle&#8217;s <code>changedAt<\/code>, it hasn&#8217;t reacted yet. That&#8217;s the steam signal.<\/p>\n<pre class=\"wp-block-code\"><code>def detect_steam(prev_snapshot, curr_snapshot, fixture_meta):\n    \"\"\"\n    Compare two snapshots. Detect where Pinnacle moved but soft books haven't.\n    Returns list of signal dicts.\n    \"\"\"\n    signals = []\n    sharp = SHARP_BOOK\n\n    if sharp not in prev_snapshot or sharp not in curr_snapshot:\n        return signals\n\n    for oid in curr_snapshot.get(sharp, {}):\n        prev_sharp = prev_snapshot[sharp].get(oid)\n        curr_sharp = curr_snapshot[sharp].get(oid)\n        if not prev_sharp or not curr_sharp:\n            continue\n\n        # Price drop = shortening odds = sharp money on this side\n        price_move = prev_sharp[\"price\"] - curr_sharp[\"price\"]\n        if price_move < STEAM_THRESHOLD:\n            continue\n\n        # Pinnacle moved. Check each soft book for staleness.\n        for soft_slug in SOFT_BOOKS:\n            if soft_slug not in curr_snapshot:\n                continue\n            soft_data = curr_snapshot[soft_slug].get(oid)\n            if not soft_data:\n                continue\n\n            sharp_moved_at = curr_sharp[\"changedAt\"]\n            soft_moved_at = soft_data[\"changedAt\"]\n\n            if not sharp_moved_at or not soft_moved_at:\n                continue\n\n            # Soft book's last update is OLDER than Pinnacle's = stale\n            if soft_moved_at < sharp_moved_at:\n                lag_seconds = (sharp_moved_at - soft_moved_at).total_seconds()\n                edge_pct = ((soft_data[\"price\"] \/ curr_sharp[\"price\"]) - 1) * 100\n\n                signals.append({\n                    \"fixture\": fixture_meta,\n                    \"outcome_id\": oid,\n                    \"sharp_prev\": prev_sharp[\"price\"],\n                    \"sharp_now\": curr_sharp[\"price\"],\n                    \"sharp_move\": round(price_move, 3),\n                    \"soft_book\": soft_slug,\n                    \"soft_price\": soft_data[\"price\"],\n                    \"soft_stale_sec\": int(lag_seconds),\n                    \"edge_vs_sharp\": round(edge_pct, 2),\n                    \"detected_at\": datetime.now(timezone.utc).isoformat()\n                })\n\n    return signals<\/code><\/pre>\n<p>The <code>edge_vs_sharp<\/code> field tells you how much value remains. If Pinnacle moved Chelsea Win from 3.84 to 3.55 and Bet365 is still at 3.60, the edge is <code>(3.60 \/ 3.55) - 1 = +1.41%<\/code>. That's the EV of betting Bet365's stale price against Pinnacle's new \"true\" price.<\/p>\n<h3>Step 5: The Polling Loop<\/h3>\n<pre class=\"wp-block-code\"><code>def run_detector(fixtures):\n    \"\"\"Main loop: snapshot -> compare -> alert -> repeat.\"\"\"\n    all_books = [SHARP_BOOK] + SOFT_BOOKS\n    prev_snapshots = {}\n\n    print(f\"Steam Detector running | {len(fixtures)} fixtures | Poll every {POLL_INTERVAL}s\")\n    print(f\"Sharp: {SHARP_BOOK} | Soft: {', '.join(SOFT_BOOKS)}\")\n    print(f\"Threshold: {STEAM_THRESHOLD} price drop\")\n    print(\"=\" * 60)\n\n    while True:\n        for f in fixtures:\n            fid = f[\"fixtureId\"]\n            match_name = f\"{f['participant1Name']} vs {f['participant2Name']}\"\n            meta = {\"fixtureId\": fid, \"match\": match_name, \"kickoff\": f[\"startTime\"]}\n\n            curr = snapshot_odds(fid, all_books)\n\n            if fid in prev_snapshots:\n                signals = detect_steam(prev_snapshots[fid], curr, meta)\n                for s in signals:\n                    label = OUTCOME_NAMES.get(s[\"outcome_id\"], s[\"outcome_id\"])\n                    print(f\"\\n{'=' * 60}\")\n                    print(f\"  STEAM MOVE DETECTED\")\n                    print(f\"  {s['fixture']['match']}\")\n                    print(f\"  Outcome: {label}\")\n                    print(f\"  Pinnacle: {s['sharp_prev']} -> {s['sharp_now']} (moved {s['sharp_move']})\")\n                    print(f\"  {s['soft_book']}: still at {s['soft_price']} (stale {s['soft_stale_sec']}s)\")\n                    print(f\"  Edge vs sharp: +{s['edge_vs_sharp']}%\")\n                    print(f\"  Time: {s['detected_at'][:19]} UTC\")\n                    print(f\"{'=' * 60}\")\n\n            prev_snapshots[fid] = curr\n            time.sleep(1)  # Rate limit between fixtures\n\n        ts = datetime.now(timezone.utc).strftime(\"%H:%M:%S\")\n        print(f\"[{ts}] Poll complete. Next in {POLL_INTERVAL}s...\")\n        time.sleep(POLL_INTERVAL)<\/code><\/pre>\n<p>Run it:<\/p>\n<pre class=\"wp-block-code\"><code># Fetch fixtures and start detecting\nfixtures = get_fixtures(SPORT_ID, days_ahead=1)\nrun_detector(fixtures[:5])  # Monitor 5 fixtures to stay within rate limits<\/code><\/pre>\n<p>When the detector catches a steam move, the output looks like this:<\/p>\n<pre class=\"wp-block-code\"><code>============================================================\n  STEAM MOVE DETECTED\n  Chelsea FC vs Manchester City\n  Outcome: Home\n  Pinnacle: 3.84 -> 3.55 (moved 0.29)\n  bet365: still at 3.60 (stale 87s)\n  Edge vs sharp: +1.41%\n  Time: 2026-04-12T16:32:05 UTC\n============================================================<\/code><\/pre>\n<h3>Step 6: Add Discord or Telegram Alerts<\/h3>\n<p>Console output is fine for testing. For live monitoring, push signals to a Discord channel or Telegram chat.<\/p>\n<pre class=\"wp-block-code\"><code>def send_discord_alert(signal, webhook_url):\n    \"\"\"Push a steam move signal to Discord via webhook.\"\"\"\n    label = OUTCOME_NAMES.get(signal[\"outcome_id\"], signal[\"outcome_id\"])\n    msg = {\n        \"content\": (\n            f\"**STEAM MOVE** | {signal['fixture']['match']}\\n\"\n            f\"Outcome: {label}\\n\"\n            f\"Pinnacle: {signal['sharp_prev']} -> {signal['sharp_now']} \"\n            f\"(moved {signal['sharp_move']})\\n\"\n            f\"{signal['soft_book']}: {signal['soft_price']} \"\n            f\"(stale {signal['soft_stale_sec']}s)\\n\"\n            f\"Edge: +{signal['edge_vs_sharp']}%\"\n        )\n    }\n    requests.post(webhook_url, json=msg)<\/code><\/pre>\n<p>To use it, create a Discord webhook in your channel settings and drop the URL into the polling loop:<\/p>\n<pre class=\"wp-block-code\"><code>DISCORD_WEBHOOK = \"https:\/\/discord.com\/api\/webhooks\/YOUR_WEBHOOK_URL\"\n\n# Inside the polling loop, after detecting signals:\nfor s in signals:\n    send_discord_alert(s, DISCORD_WEBHOOK)<\/code><\/pre>\n<p>For Telegram, swap in a call to the Bot API (<code>sendMessage<\/code> to your chat ID). The detection logic stays the same \u2014 only the delivery changes.<\/p>\n<h2>How to Tune the Detector<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Parameter<\/th>\n<th>Default<\/th>\n<th>Tighter<\/th>\n<th>Looser<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>STEAM_THRESHOLD<\/code><\/td>\n<td>0.05<\/td>\n<td>Fewer signals, higher confidence<\/td>\n<td>More signals, more noise<\/td>\n<\/tr>\n<tr>\n<td><code>POLL_INTERVAL<\/code><\/td>\n<td>90s<\/td>\n<td>Faster detection, more API calls<\/td>\n<td>Fewer calls, may miss fast windows<\/td>\n<\/tr>\n<tr>\n<td><code>SOFT_BOOKS<\/code><\/td>\n<td>6 books<\/td>\n<td>Focus on slowest movers<\/td>\n<td>More edge opportunities<\/td>\n<\/tr>\n<tr>\n<td><code>MARKET_ID<\/code><\/td>\n<td>101 (1X2)<\/td>\n<td>Single market focus<\/td>\n<td>Scan multiple markets (loop IDs)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h3>Free Tier Budget Math<\/h3>\n<p>OddsPapi's free tier gives you 250 requests per month. Each call to <code>\/odds<\/code> is 1 request. If you monitor 5 fixtures every 90 seconds, that's ~5 requests per poll cycle. At 250 req\/month, you get roughly <strong>50 poll cycles<\/strong> \u2014 enough to cover a Saturday afternoon of matches or a midweek Champions League slate.<\/p>\n<p>To stretch the budget: reduce the fixture count, increase the poll interval, or poll only during kickoff windows (when lines move most). For all-day monitoring across multiple sports, upgrade to a paid plan.<\/p>\n<h2>Backtest It: Did the Steam Moves Actually Win?<\/h2>\n<p>Detecting steam moves is one thing. Proving they're profitable is another. OddsPapi's <code>\/historical-odds<\/code> endpoint \u2014 free on every tier \u2014 lets you pull the full price history for any past fixture and check whether Pinnacle's move predicted the result.<\/p>\n<pre class=\"wp-block-code\"><code>def check_closing_line(fixture_id, bookmaker=\"pinnacle\"):\n    \"\"\"Pull historical prices and compare opening vs closing line.\"\"\"\n    data = api_get(\"historical-odds\", {\n        \"fixtureId\": fixture_id,\n        \"bookmakers\": bookmaker\n    })\n\n    book = data.get(\"bookmakers\", {}).get(bookmaker, {})\n    market = book.get(\"markets\", {}).get(str(MARKET_ID), {})\n\n    for oid, outcome in market.get(\"outcomes\", {}).items():\n        snaps = outcome.get(\"players\", {}).get(\"0\", [])\n        if not snaps:\n            continue\n        opening = snaps[0][\"price\"]\n        closing = snaps[-1][\"price\"]\n        move = opening - closing\n        label = OUTCOME_NAMES.get(oid, oid)\n        print(f\"  {label}: opened {opening} -> closed {closing} (moved {move:+.3f})\")<\/code><\/pre>\n<p>If the closing line consistently moved in the same direction as the steam signal, your detector is capturing real information. For a full backtesting framework, see our <a href=\"\/blog\/backtest-betting-model-free-historical-odds\/\">guide to backtesting betting models with free historical data<\/a>.<\/p>\n<h2>Why OddsPapi for Steam Detection<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Requirement<\/th>\n<th>Why It Matters<\/th>\n<th>OddsPapi<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>350+ bookmakers<\/strong><\/td>\n<td>More soft books = more stale prices to catch<\/td>\n<td>350+ books including regional and crypto<\/td>\n<\/tr>\n<tr>\n<td><strong>Sharp benchmarks<\/strong><\/td>\n<td>Need Pinnacle (or Singbet\/SBOBet) as the signal source<\/td>\n<td>All three available on free tier<\/td>\n<\/tr>\n<tr>\n<td><strong>changedAt timestamp<\/strong><\/td>\n<td>Detect staleness without continuous polling<\/td>\n<td>On every outcome, every bookmaker<\/td>\n<\/tr>\n<tr>\n<td><strong>Free historical data<\/strong><\/td>\n<td>Backtest whether steam signals actually predict outcomes<\/td>\n<td>Full price history, free tier<\/td>\n<\/tr>\n<tr>\n<td><strong>Simple auth<\/strong><\/td>\n<td>No OAuth, no session tokens<\/td>\n<td>Single query parameter: <code>?apiKey=KEY<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Most competing odds APIs either don't carry Pinnacle at all (The Odds API, OddsJam's standard tier) or don't expose the <code>changedAt<\/code> timestamp that makes staleness detection possible. Without both, you're stuck polling every second and maintaining your own price history \u2014 or paying $200\/month for someone else to do it.<\/p>\n<h2>Frequently Asked Questions<\/h2>\n<h3>What is a steam move in sports betting?<\/h3>\n<p>A steam move is a sudden, sharp line movement caused by professional bettors placing large wagers at a sharp bookmaker like Pinnacle. When Pinnacle's odds shorten rapidly on one side, it signals that informed money has identified value. Soft bookmakers typically follow 30 seconds to several minutes later \u2014 that lag is the window to act.<\/p>\n<h3>How accurate are steam moves at predicting outcomes?<\/h3>\n<p>Pinnacle's line movements are the single most predictive signal in sports betting markets. Academic research shows that Pinnacle's closing line closely approximates true outcome probabilities. Steam moves that shorten odds by 5+ cents have historically high follow-through rates, but no signal is 100% \u2014 proper bankroll management is still essential.<\/p>\n<h3>Can I detect steam moves with a free API?<\/h3>\n<p>Yes. OddsPapi's free tier includes 250 requests per month with access to Pinnacle and 350+ other bookmakers. The detector in this tutorial batches bookmakers into single requests and uses 90-second polling intervals to maximize coverage within the free tier. For continuous monitoring across multiple sports, a paid plan removes the cap.<\/p>\n<h3>What is the difference between a steam move and reverse line movement?<\/h3>\n<p>A steam move is driven by sharp bookmaker price changes \u2014 Pinnacle moves, soft books follow. Reverse line movement (RLM) is when the line moves opposite to public betting percentages, suggesting sharp money is on the unpopular side. Both indicate sharp action, but steam moves are directly observable through odds data, while RLM requires access to public betting percentages that most APIs don't provide.<\/p>\n<h3>Why do soft bookmakers lag behind Pinnacle?<\/h3>\n<p>Soft bookmakers like Bet365, DraftKings, and FanDuel set odds through a mix of internal models and shadow-copying sharp books. Their automated feeds and manual traders take 30 seconds to several minutes to process Pinnacle's movements. Regional books and crypto sportsbooks are often even slower, creating wider windows for steam detection.<\/p>\n<p><script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    {\n      \"@type\": \"Question\",\n      \"name\": \"What is a steam move in sports betting?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"A steam move is a sudden, sharp line movement caused by professional bettors placing large wagers at a sharp bookmaker like Pinnacle. When Pinnacle's odds shorten rapidly on one side, it signals that informed money has identified value. Soft bookmakers typically follow 30 seconds to several minutes later \u2014 that lag is the window to act.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How accurate are steam moves at predicting outcomes?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Pinnacle's line movements are the single most predictive signal in sports betting markets. Academic research shows that Pinnacle's closing line closely approximates true outcome probabilities. Steam moves that shorten odds by 5+ cents have historically high follow-through rates, but no signal is 100% \u2014 proper bankroll management is still essential.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Can I detect steam moves with a free API?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Yes. OddsPapi's free tier includes 250 requests per month with access to Pinnacle and 350+ other bookmakers. The detector in this tutorial batches bookmakers into single requests and uses 90-second polling intervals to maximize coverage within the free tier. For continuous monitoring across multiple sports, a paid plan removes the cap.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"What is the difference between a steam move and reverse line movement?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"A steam move is driven by sharp bookmaker price changes \u2014 Pinnacle moves, soft books follow. Reverse line movement (RLM) is when the line moves opposite to public betting percentages, suggesting sharp money is on the unpopular side. Both indicate sharp action, but steam moves are directly observable through odds data, while RLM requires access to public betting percentages that most APIs don't provide.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Why do soft bookmakers lag behind Pinnacle?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Soft bookmakers like Bet365, DraftKings, and FanDuel set odds through a mix of internal models and shadow-copying sharp books. Their automated feeds and manual traders take 30 seconds to several minutes to process Pinnacle's movements. Regional books and crypto sportsbooks are often even slower, creating wider windows for steam detection.\"\n      }\n    }\n  ]\n}\n<\/script><\/p>\n<h2>Stop Paying $200\/mo for a Black Box<\/h2>\n<p>You just built a steam move detector that does what OddsJam and Sports Insights charge hundreds for \u2014 in ~100 lines of Python, with fully transparent logic you can inspect, tune, and extend.<\/p>\n<p>OddsPapi gives you the two things no other free API offers: <strong>Pinnacle odds<\/strong> and <strong>changedAt timestamps<\/strong> on every outcome from 350+ bookmakers. That's everything you need to detect steam moves, quantify edges, and backtest whether they convert.<\/p>\n<p><strong><a href=\"https:\/\/oddspapi.io\/en\/pricing\">Get your free API key<\/a><\/strong> and start detecting steam moves today.<\/p>\n<h3>Related Tutorials<\/h3>\n<ul>\n<li><a href=\"\/blog\/value-betting-scanner-python\/\">Build a Value Betting Scanner with Python<\/a><\/li>\n<li><a href=\"\/blog\/arbitrage-betting-bot-python\/\">Build an Arbitrage Betting Bot with Python<\/a><\/li>\n<li><a href=\"\/blog\/backtest-betting-model-free-historical-odds\/\">Backtest a Betting Model with Free Historical Odds<\/a><\/li>\n<li><a href=\"\/blog\/odds-comparison-dashboard-python-streamlit\/\">Build a Live Odds Dashboard with Streamlit<\/a><\/li>\n<li><a href=\"\/blog\/oddsjam-api-alternative\/\">OddsJam API: Pricing, Coverage & a Free Alternative<\/a><\/li>\n<\/ul>\n<p><!--\nFocus Keyphrase: steam move detector\nSEO Title: Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API)\nMeta Description: Build a Python steam move detector that tracks Pinnacle line movement across 350+ bookmakers and alerts you before soft books adjust. Free API tier.\nSlug: steam-move-detector-python\n--><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Build a Python steam move detector that tracks Pinnacle line movement across 350+ bookmakers and alerts you before soft books adjust. Free API tier.<\/p>\n","protected":false},"author":2,"featured_media":2887,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[54,9,55,11,10,53],"class_list":["post-2886","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to-guides","tag-line-movement","tag-odds-api","tag-pinnacle","tag-python","tag-sports-betting-api","tag-steam-move"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API) | OddsPapi Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API) | OddsPapi Blog\" \/>\n<meta property=\"og:description\" content=\"Build a Python steam move detector that tracks Pinnacle line movement across 350+ bookmakers and alerts you before soft books adjust. Free API tier.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/\" \/>\n<meta property=\"og:site_name\" content=\"OddsPapi Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-05-15T10:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-05T18:12:14+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp\" \/>\n\t<meta property=\"og:image:width\" content=\"2560\" \/>\n\t<meta property=\"og:image:height\" content=\"1429\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/webp\" \/>\n<meta name=\"author\" content=\"Odds API Writer\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/oddspapi.io\/logo-v2.webp\" \/>\n<meta name=\"twitter:creator\" content=\"@oddspapiapi\" \/>\n<meta name=\"twitter:site\" content=\"@oddspapiapi\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Odds API Writer\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"12 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/\"},\"author\":{\"name\":\"Odds API Writer\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\"},\"headline\":\"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API)\",\"datePublished\":\"2026-05-15T10:00:00+00:00\",\"dateModified\":\"2026-06-05T18:12:14+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/\"},\"wordCount\":1506,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp\",\"keywords\":[\"Line Movement\",\"Odds API\",\"Pinnacle\",\"Python\",\"Sports Betting API\",\"Steam Move\"],\"articleSection\":[\"How To Guides\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/\",\"name\":\"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API) | OddsPapi Blog\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp\",\"datePublished\":\"2026-05-15T10:00:00+00:00\",\"dateModified\":\"2026-06-05T18:12:14+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#primaryimage\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp\",\"width\":2560,\"height\":1429,\"caption\":\"Steam Move Detector - OddsPapi API Blog\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/oddspapi.io\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\",\"url\":\"https:\/\/oddspapi.io\/blog\/\",\"name\":\"OddsPapi\",\"description\":\"Sports Odds API Tutorials &amp; Guides\",\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"alternateName\":\"Odds Papi\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/oddspapi.io\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\",\"name\":\"OddsPapi\",\"url\":\"https:\/\/oddspapi.io\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png\",\"width\":135,\"height\":135,\"caption\":\"OddsPapi\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/x.com\/oddspapiapi\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\",\"name\":\"Odds API Writer\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g\",\"caption\":\"Odds API Writer\"},\"url\":\"https:\/\/oddspapi.io\/blog\/author\/andy-lavelle\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API) | OddsPapi Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/","og_locale":"en_US","og_type":"article","og_title":"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API) | OddsPapi Blog","og_description":"Build a Python steam move detector that tracks Pinnacle line movement across 350+ bookmakers and alerts you before soft books adjust. Free API tier.","og_url":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/","og_site_name":"OddsPapi Blog","article_published_time":"2026-05-15T10:00:00+00:00","article_modified_time":"2026-06-05T18:12:14+00:00","og_image":[{"width":2560,"height":1429,"url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp","type":"image\/webp"}],"author":"Odds API Writer","twitter_card":"summary_large_image","twitter_image":"https:\/\/oddspapi.io\/logo-v2.webp","twitter_creator":"@oddspapiapi","twitter_site":"@oddspapiapi","twitter_misc":{"Written by":"Odds API Writer","Est. reading time":"12 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#article","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/"},"author":{"name":"Odds API Writer","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13"},"headline":"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API)","datePublished":"2026-05-15T10:00:00+00:00","dateModified":"2026-06-05T18:12:14+00:00","mainEntityOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/"},"wordCount":1506,"commentCount":0,"publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp","keywords":["Line Movement","Odds API","Pinnacle","Python","Sports Betting API","Steam Move"],"articleSection":["How To Guides"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/","url":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/","name":"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API) | OddsPapi Blog","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#primaryimage"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp","datePublished":"2026-05-15T10:00:00+00:00","dateModified":"2026-06-05T18:12:14+00:00","breadcrumb":{"@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#primaryimage","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/04\/steam-move-detector-python-scaled.webp","width":2560,"height":1429,"caption":"Steam Move Detector - OddsPapi API Blog"},{"@type":"BreadcrumbList","@id":"https:\/\/oddspapi.io\/blog\/steam-move-detector-python\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/oddspapi.io\/blog\/"},{"@type":"ListItem","position":2,"name":"Steam Move Detector: Build a Line Movement Alert Bot with Python (Free API)"}]},{"@type":"WebSite","@id":"https:\/\/oddspapi.io\/blog\/#website","url":"https:\/\/oddspapi.io\/blog\/","name":"OddsPapi","description":"Sports Odds API Tutorials &amp; Guides","publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"alternateName":"Odds Papi","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/oddspapi.io\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/oddspapi.io\/blog\/#organization","name":"OddsPapi","url":"https:\/\/oddspapi.io\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png","width":135,"height":135,"caption":"OddsPapi"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/x.com\/oddspapiapi"]},{"@type":"Person","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13","name":"Odds API Writer","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g","caption":"Odds API Writer"},"url":"https:\/\/oddspapi.io\/blog\/author\/andy-lavelle\/"}]}},"_links":{"self":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2886","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/comments?post=2886"}],"version-history":[{"count":2,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2886\/revisions"}],"predecessor-version":[{"id":3014,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/2886\/revisions\/3014"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media\/2887"}],"wp:attachment":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media?parent=2886"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/categories?post=2886"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/tags?post=2886"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}