BettingLab

Live Sports Odds Excel Pull Google Apps Script Tutorial

Marcus Hale
Marcus Hale

Most serious bettors eventually hit the same wall: manually refreshing odds across fifteen browser tabs, copying numbers into Excel, and realizing the line moved while you were updating your Kelly calculations. The solution isn't another betting app—it's pulling live sports odds into Excel using Google Apps Script and a proper API feed.

This tutorial shows you how to wire Google Sheets to pull fresh odds every few minutes, no developer background required. We'll build a custom function that fetches live NBA lines, updates your EV calculations automatically, and tracks line movement over time. Real formulas, real code, real results.

Why Pull Live Odds Into Spreadsheets

Manual odds entry is killing your edge. By the time you've copied +240 from DraftKings and +235 from FanDuel into your arb calculator, both books have moved to +225. The smart money moved while you were playing data entry clerk.

Professional shops solved this years ago with custom feeds and automated systems. But you don't need a $50k budget—just Google Sheets, ten minutes of setup, and access to clean API data.

The benefits are immediate:

The Technical Challenge

Excel and Google Sheets weren't built for live data feeds. You can't just =IMPORTDATA("https://api.odds.com/nba") and expect it to work reliably. Most betting APIs require authentication, headers, and structured requests that basic spreadsheet functions can't handle.

That's where Google Apps Script comes in. It's essentially JavaScript that runs in Google's cloud and can manipulate your sheets directly. Think of it as Excel VBA, but more powerful and hosted.

Setting Up Google Apps Script

First, create a new Google Sheet and name it something like "Live NBA Odds Tracker." Then we'll add the script that pulls fresh data.

Open Extensions → Apps Script from your sheet menu. Delete the default code and start fresh. We're building a custom function that fetches odds from the MoneyLine API and formats them for spreadsheet use.

Here's the complete script:

function getLiveOdds(sport = 'basketball_nba', market = 'h2h') {
  const API_KEY = 'your_api_key_here'; // Get free tier at https://mlapi.bet
  const BASE_URL = 'https://mlapi.bet/v1/odds';
  
  const url = `${BASE_URL}?sport=${sport}&markets=${market}&regions=us&oddsFormat=american`;
  
  const options = {
    'method': 'GET',
    'headers': {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json'
    }
  };
  
  try {
    const response = UrlFetchApp.fetch(url, options);
    const data = JSON.parse(response.getContentText());
    
    if (!data.data || data.data.length === 0) {
      return [['No games found', '', '', '', '']];
    }
    
    // Format data for spreadsheet
    const results = [['Game', 'Book', 'Team 1', 'Odds 1', 'Team 2', 'Odds 2', 'Updated']];
    
    data.data.forEach(game => {
      const teams = game.teams;
      const homeTeam = teams.find(t => t.side === 'home')?.name || 'Home';
      const awayTeam = teams.find(t => t.side === 'away')?.name || 'Away';
      
      if (game.bookmakers && game.bookmakers.length > 0) {
        game.bookmakers.forEach(book => {
          if (book.markets && book.markets.length > 0) {
            const market = book.markets[0];
            if (market.outcomes && market.outcomes.length >= 2) {
              const homeOdds = market.outcomes.find(o => o.name === homeTeam)?.price || '';
              const awayOdds = market.outcomes.find(o => o.name === awayTeam)?.price || '';
              
              results.push([
                `${awayTeam} @ ${homeTeam}`,
                book.title,
                homeTeam,
                homeOdds,
                awayTeam, 
                awayOdds,
                new Date()
              ]);
            }
          }
        });
      }
    });
    
    return results;
  } catch (error) {
    return [['Error fetching odds: ' + error.toString(), '', '', '', '']];
  }
}

// Auto-refresh function
function refreshOddsData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Live Odds');
  if (!sheet) return;
  
  const data = getLiveOdds();
  sheet.clear();
  sheet.getRange(1, 1, data.length, data[0].length).setValues(data);
}

Save the script (Ctrl+S) and name your project something like "Live Odds Puller."

API Authentication Setup

You'll need an API key to pull live odds data. The MoneyLine API offers 1,000 free requests per month, which is plenty for personal betting sheets. Sign up at the API dashboard and copy your key into the API_KEY variable above.

The free tier includes:

Building Your Live Odds Sheet

Create a new sheet tab called "Live Odds" where the API data will populate. Then create another tab called "Analysis" for your betting calculations.

In the Live Odds tab, you can manually trigger the data pull by entering =getLiveOdds() in cell A1. But the real power comes from automated refreshes every few minutes.

Setting Up Automated Updates

Go back to Apps Script and add a time-driven trigger:

  1. Click the clock icon (Triggers) in the left sidebar
  2. Click + Add Trigger
  3. Choose function: refreshOddsData
  4. Event source: Time-driven
  5. Type: Minutes timer
  6. Interval: Every 5 minutes

Now your sheet will pull fresh odds automatically without any manual intervention. The API updates every 2 minutes, so 5-minute pulls give you near real-time data without hitting rate limits.

Advanced Formulas for Betting Analysis

Raw odds data is just the starting point. The real value comes from automated calculations that update as lines move. Here are the essential formulas every serious bettor needs:

No-Vig Fair Value Calculator

In your Analysis tab, set up a no-vig calculator that strips juice from American odds:

// Cell B2: Home Odds (from Live Odds tab)
=INDEX('Live Odds'!D:D,2)

// Cell C2: Away Odds  
=INDEX('Live Odds'!F:F,2)

// Cell D2: Home Implied Probability
=IF(B2>0,100/(B2+100),ABS(B2)/(ABS(B2)+100))

// Cell E2: Away Implied Probability
=IF(C2>0,100/(C2+100),ABS(C2)/(ABS(C2)+100))

// Cell F2: Total Market Probability (includes vig)
=D2+E2

// Cell G2: No-Vig Home Probability
=D2/F2

// Cell H2: No-Vig Away Probability  
=E2/F2

// Cell I2: Fair Odds Home (American)
=IF(G2>=0.5,-G2/(1-G2)*100,(1-G2)/G2*100)

// Cell J2: Fair Odds Away (American)
=IF(H2>=0.5,-H2/(1-H2)*100,(1-H2)/H2*100)

Expected Value Tracker

Add EV calculations that automatically update with fresh odds:

// Cell K2: Your Home Team Edge % (manual input)
=0.52

// Cell L2: Expected Value Home
=(K2*I2-(1-K2)*100)/100

// Cell M2: Kelly Bet Size % (25% of calculated Kelly for safety)
=MAX(0,(K2-(1-K2)/(I2/100))*0.25)

// Cell N2: EV Color Coding
=IF(L2>0.05,"STRONG BET",IF(L2>0.02,"DECENT","PASS"))

Line Movement Tracking

Track how odds change over time by logging historical data:

// In a separate "Line History" tab
// Column A: Timestamp
// Column B: Game  
// Column C: Book
// Column D: Home Odds
// Column E: Away Odds

// Use this Apps Script addition to log line changes:
function logLineMovement() {
  const liveSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Live Odds');
  const historySheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Line History');
  
  if (!liveSheet || !historySheet) return;
  
  const liveData = liveSheet.getDataRange().getValues();
  const timestamp = new Date();
  
  // Skip header row
  for (let i = 1; i < liveData.length; i++) {
    const row = liveData[i];
    historySheet.appendRow([
      timestamp,
      row[0], // Game
      row[1], // Book  
      row[3], // Home odds
      row[5]  // Away odds
    ]);
  }
}

Troubleshooting Common Issues

API Rate Limits

The MoneyLine API free tier allows 1,000 requests per month. With 5-minute updates running 24/7, you'll use about 8,640 requests per month. Either:

  1. Reduce update frequency to 10-15 minutes during off-peak hours
  2. Use conditional logic to only pull data during active betting windows
  3. Upgrade to a paid tier for higher limits

Stale Data Detection

Add a formula to highlight when odds data is more than 10 minutes old:

// Cell O2: Data Freshness Warning
=IF((NOW()-G2)*24*60>10,"STALE DATA","FRESH")

Error Handling

The script includes basic error handling, but you can enhance it:

// Add this to the getLiveOdds function after the try block
catch (error) {
  console.log('API Error: ' + error.toString());
  
  // Return cached data if available
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Live Odds');
  if (sheet && sheet.getLastRow() > 1) {
    return sheet.getDataRange().getValues();
  }
  
  return [['Error: ' + error.toString(), '', '', '', '']];
}

Scaling Beyond Basic Odds

Once you have live odds flowing smoothly, consider expanding the system:

Multi-Sport Coverage

Modify the sport parameter to pull odds from multiple leagues:

// Add this function for multi-sport pulls
function getAllSportsOdds() {
  const sports = ['basketball_nba', 'americanfootball_nfl', 'baseball_mlb'];
  const allData = [['Sport', 'Game', 'Book', 'Team 1', 'Odds 1', 'Team 2', 'Odds 2']];
  
  sports.forEach(sport => {
    const sportData = getLiveOdds(sport);
    sportData.slice(1).forEach(row => {
      allData.push([sport, ...row]);
    });
  });
  
  return allData;
}

Alternative Markets

Pull spreads, totals, and player props by changing the markets parameter:

// For point spreads
const spreadData = getLiveOdds('basketball_nba', 'spreads');

// For totals  
const totalsData = getLiveOdds('basketball_nba', 'totals');

// For player props (if supported)
const playerData = getLiveOdds('basketball_nba', 'player_points');

For more advanced integrations and automated arbitrage detection, the MoneyLine API offers WebSocket feeds and bulk endpoints that can handle institutional-level betting operations.

Frequently Asked Questions

How often does the MoneyLine API update odds data?

The API updates live odds every 2 minutes during active betting windows. For pre-game markets, updates occur whenever sportsbooks move their lines, typically every 5-15 minutes depending on betting volume and news flow.

Can I pull historical odds data for backtesting strategies?

The MoneyLine API includes historical endpoints for closing line value analysis and strategy backtesting. The free tier includes 30 days of historical data, while paid tiers offer multiple years of line history across all major markets.

What happens if I exceed the API rate limits?

The free tier allows 1,000 requests per month. If you exceed this limit, the API returns a 429 rate limit error. Your Google Apps Script will fall back to displaying cached data until your monthly limit resets. Consider upgrading to a paid tier if you need higher request volumes.

How do I handle games that are postponed or cancelled?

The API automatically filters out postponed and cancelled games from the odds feed. If a game status changes after you've pulled the data, the next API call will exclude it from results. Your spreadsheet formulas should include error handling for missing game data.

Can I share this spreadsheet with other bettors in my group?

Yes, but each person needs their own API key for the requests to work properly. You can share the spreadsheet template and Apps Script code, but they'll need to input their own MoneyLine API credentials in the script configuration.

Build with the same data we use.

MoneyLine API powers BettingLab's edge calculations. Free tier, 1k credits/month.