If you've spent any time seriously handicapping sports, you've run into this problem: a book posts -115 / -115 on a two-way market. What's the actual fair probability? What's the line without the juice? That's what a no-vig calculator in Google Sheets solves — and once you wire it to live odds, it becomes one of the most useful tools you can build without touching a line of Python.
This walkthrough shows you exactly how to build it. We'll cover the math, the formulas, and then wire the sheet to the MoneyLine API via Google Apps Script so your implied probabilities update in real time against the current market.
No developer experience required. If you can write =IF(), you can build this.
What "No-Vig" Actually Means (and Why It Matters)
Sportsbooks don't take a percentage cut the way a casino does on roulette. Instead, they shade the odds. A coin-flip game that should be +100 / +100 gets posted as -110 / -110. Both sides now imply more than 50% probability, which is how the book guarantees margin regardless of outcome.
The standard -110 / -110 market implies:
- Side A: 110 / (110 + 100) = 52.38%
- Side B: 110 / (110 + 100) = 52.38%
- Total: 104.76% — the 4.76% overage is the vig
Removing the vig normalizes those implied probabilities so they sum to exactly 100%. That gives you the fair probability the market is pricing. From there you can:
- Compare your own model probability to the market's fair probability
- Calculate EV correctly (using the no-vig line, not the posted line)
- Set Kelly bets against a real edge, not a distorted one
The EV betting methodology on BettingLab is built on exactly this normalization. If you're still calculating EV off the raw posted odds, you're overestimating your edge by the width of the vig.
Building the Core No-Vig Formulas in Sheets
Open a new Google Sheet. Set up columns like this in rows 1–2:
| A | B | C | D | E | F | G | |---|---|---|---|---|---|---| | Market | Side A Odds | Side B Odds | Implied A | Implied B | Total Implied | No-Vig A | No-Vig B |
Now the formulas. All odds input as American moneyline integers (e.g., -110, +135).
Converting American Odds to Implied Probability
In D2 (implied probability for Side A):
=IF(B2<0, ABS(B2)/(ABS(B2)+100), 100/(B2+100))
In E2 (implied probability for Side B):
=IF(C2<0, ABS(C2)/(ABS(C2)+100), 100/(C2+100))
Total Implied (the vig indicator)
In F2:
=D2+E2
If this is 1.0 exactly, there's no vig (you won't see this in the wild). Anything above 1.0 is the book's margin. A typical -110 / -110 market gives you 1.0476.
Normalizing to No-Vig Probabilities
In G2 (no-vig probability for Side A):
=D2/F2
In H2 (no-vig probability for Side B):
=E2/F2
Check: G2 + H2 should always equal 1.0 exactly.
Converting No-Vig Probability Back to American Odds
Sometimes you want to see the fair line, not just the probability. Add columns I and J.
I2 — fair American odds for Side A:
=IF(G2>=0.5, -1*ROUND((G2/(1-G2))*100,0), ROUND(((1-G2)/G2)*100,0))
J2 — fair American odds for Side B:
=IF(H2>=0.5, -1*ROUND((H2/(1-H2))*100,0), ROUND(((1-H2)/H2)*100,0))
At -110 / -110 input, G2 = H2 = 0.5 exactly, and I2 = J2 = -100 (fair line is even money, as expected for a coin flip).
Adding a Kelly Criterion Column
While we're here, add EV and Kelly. Assume column K is "Your Model Probability for Side A" — you fill this in manually.
L2 — EV of betting Side A at the posted odds:
=IF(B2>0, K2*(B2/100) - (1-K2), K2 - (1-K2)*(ABS(B2)/100))
M2 — Kelly fraction (bet this % of bankroll):
=IF(L2>0, L2/IF(B2>0, B2/100, ABS(B2)/100), 0)
If L2 is negative, Kelly correctly returns 0 — don't bet. If you want half-Kelly (standard for real money), wrap M2 in =M2*0.5.
Pulling Live Odds with Google Apps Script
The formulas above are only as useful as the odds you feed them. Manually copying lines from a book wastes time and introduces errors. Here's how to connect the sheet to the MoneyLine API so odds populate automatically.
In Google Sheets: Extensions → Apps Script → New Script
Paste this:
const API_BASE = "https://mlapi.bet";
const API_KEY = "YOUR_MONEYLINE_API_KEY"; // paste your key here
/**
* Custom sheet function: =MLODDS("americanfootball_nfl","LAR","SF","h2h","home")
* Returns the American-odds moneyline for one side of a head-to-head market.
*
* @param {string} sport e.g. "americanfootball_nfl", "baseball_mlb"
* @param {string} home Home team name (partial match OK)
* @param {string} away Away team name
* @param {string} market Market key: "h2h", "spreads", "totals"
* @param {string} side "home" or "away"
* @return {number} American odds integer
* @customfunction
*/
function MLODDS(sport, home, away, market, side) {
const url = `${API_BASE}/v1/odds?sport=${sport}&markets=${market}&oddsFormat=american`;
const options = {
method: "get",
headers: { "Authorization": `Bearer ${API_KEY}` },
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
if (response.getResponseCode() !== 200) {
return "API_ERR";
}
const data = JSON.parse(response.getContentText());
const events = data.data || [];
// Find the matching event
const event = events.find(e =>
e.home_team.toLowerCase().includes(home.toLowerCase()) &&
e.away_team.toLowerCase().includes(away.toLowerCase())
);
if (!event) return "NOT_FOUND";
// Grab the best available price across bookmakers
const bestPrice = getBestPrice(event, market, side === "home" ? event.home_team : event.away_team);
return bestPrice ?? "N/A";
}
function getBestPrice(event, marketKey, teamName) {
let best = null;
for (const book of (event.bookmakers || [])) {
const mkt = (book.markets || []).find(m => m.key === marketKey);
if (!mkt) continue;
const outcome = (mkt.outcomes || []).find(o =>
o.name.toLowerCase().includes(teamName.toLowerCase())
);
if (!outcome) continue;
const price = outcome.price;
if (best === null || price > best) best = price;
}
return best;
}
Save. Now in your sheet:
- B2:
=MLODDS("baseball_mlb","Dodgers","Padres","h2h","home") - C2:
=MLODDS("baseball_mlb","Dodgers","Padres","h2h","away")
The formulas in D through M auto-calculate from there. Every time you open the sheet (or force a recalc with Ctrl+Shift+F9), the live market lines pull in fresh.
The free tier at mlapi.bet gives you 1,000 API credits per month — enough to refresh a 10-game slate several times daily without paying anything.
Extending the Sheet: Multi-Game Slate View
For a full game-day slate, use this pattern. Put team pairs in rows 2–20. The =MLODDS() call is the same in each row; just change the team names. The no-vig and Kelly formulas copy straight down.
Add a conditional-format rule on column L (EV):
- Green fill if L > 0.03 (3%+ EV — a threshold worth considering)
- Yellow fill if 0 < L ≤ 0.03
- No fill if L ≤ 0
Now glancing at your sheet tells you instantly which games have actionable edges at the current market price, based on your model probability input in column K.
For tracking line movement, set up a second sheet tab called Snapshots. In Apps Script, add a time-driven trigger (every 30 minutes, 1 hour) that copies the current odds values — not formulas, just the pasted values — into a new row with a timestamp:
function snapshotOdds() {
const src = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Odds");
const snap = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Snapshots");
const row = src.getRange("A2:J20").getValues();
const ts = new Date().toISOString();
for (const r of row) {
snap.appendRow([ts, ...r]);
}
}
Set this trigger: Apps Script → Triggers → Add Trigger → snapshotOdds → Time-driven → Every hour. After a week you have a full line-movement history you can chart.
This is a simple version of what professional line-movement tracking tools charge for. You can compare API options for arbitrage workflows if you want to go deeper on automation.
Practical Notes and Common Mistakes
Don't confuse decimal and American odds. The formulas above assume American format integers from the API. If you ever pull decimal odds, the implied probability is simply 1 / decimal_odds. You won't need the IF() branch.
Vig is not symmetric. A -120 / +100 market has a different vig structure than -110 / -110. The formulas handle this correctly because they normalize based on the sum of both implied probabilities, not an assumed equal split.
The API returns best-available across books, not your book. If you're comparing to what DraftKings is posting to you specifically, you may see slight differences. The /v1/odds endpoint lets you filter by bookmaker key if you want to pin to a specific book.
Avoid live-betting latency mismatches. The Google Apps Script UrlFetchApp call is synchronous and can take 500–2000ms. Don't use this for in-game markets where a line can move while the request is in flight. It's built for pre-game slate analysis.
For more on the API's data architecture and what /v1/edge returns vs /v1/odds, see the MoneyLine API build documentation.
Frequently Asked Questions
What is a no-vig calculator? A no-vig calculator removes the sportsbook's built-in margin from posted odds to reveal the true (fair) probability the market is implying. It normalizes the two sides of a market so their implied probabilities sum to exactly 100% instead of 104–106%.
How do I convert American odds to implied probability in Google Sheets?
Use =IF(A1<0, ABS(A1)/(ABS(A1)+100), 100/(A1+100)) where A1 is your American odds integer. This works for both negative (favorite) and positive (underdog) odds.
How many API calls does the sheet make?
Each =MLODDS() function is one API call. A 10-game slate with two sides per game = 20 calls per recalculation. At 1,000 free credits per month, you have ~50 full slate refreshes before hitting the limit — plenty for daily use on a single sport.
Can I use this with Excel instead of Google Sheets?
Yes, but you'll replace Apps Script with Power Query. In Excel: Data → Get Data → From Web, point it at https://mlapi.bet/v1/odds?sport=baseball_mlb&markets=h2h&oddsFormat=american with an Authorization header. Parse the JSON response with Power Query's built-in JSON parser, then reference the loaded table with =XLOOKUP() or INDEX/MATCH instead of the custom function.
What's the difference between no-vig probability and my model probability? No-vig probability is what the market thinks, stripped of margin. Your model probability is what you think, based on your own analysis. EV is positive when your model probability is higher than the no-vig probability. If they're the same, the market is fairly priced and there's no edge — regardless of what the posted odds look like.