Market Making on Polymarket: Use Sportsbook Odds as Your Edge
Polymarket’s order book is thin on NBA totals and spreads. FanDuel and DraftKings price the same lines with professional traders. One side has wide spreads and crowd-driven pricing. The other has tight, sharp pricing backed by decades of sportsbook infrastructure.
If you know what market making is, you already see the opportunity. If you don’t: market making means quoting both sides of a market (bid and ask), earning the spread when both fill. The trick is knowing the fair value so you don’t get picked off. That’s where FanDuel and DraftKings come in.
In this tutorial, you’ll build a market making scanner that identifies the best opportunities on Polymarket by comparing its prices against professional sportsbook lines — all from a single API.
Market Making 101: The Concept
Market makers provide liquidity. On Polymarket, every contract trades between $0.00 and $1.00 (representing 0-100% probability). When you market make, you:
- Determine fair value — What’s the “true” probability of this outcome?
- Quote a bid — Place a buy order slightly below fair value
- Quote an ask — Place a sell order slightly above fair value
- Earn the spread — When both sides fill, the difference is your profit
The hard part is Step 1. Where do you get fair value? Most retail traders on Polymarket use gut feel, Twitter sentiment, or stale polling data. That’s why the spreads are wide.
Professional sportsbooks like FanDuel and DraftKings spend millions on oddsmaking infrastructure. Their implied probabilities are some of the most accurate prices on earth for sporting events. Use their prices as your fair value, and you have an edge over every retail trader on Polymarket.
Why Polymarket’s Niche Markets Are the Target
Polymarket’s moneyline markets (who wins the game) are relatively efficient — they attract the most volume and tightest spreads. But Polymarket also lists specific-line totals, spreads, and first-half markets on NBA and soccer fixtures:
| Market Type | Example | Polymarket Liquidity | MM Opportunity |
|---|---|---|---|
| Moneyline | Heat to win | Thick — most volume | Low — tight spreads |
| Totals (O/U) | Over 237.5 points | Thin — fewer traders | Medium — wider spreads |
| Spreads | Heat -15.5 | Thin | Medium-High |
| First Half | 1H Winner / 1H O/U | Very thin | High — widest spreads |
| BTTS (Soccer) | Both Teams Score | Thin | Medium-High |
The thinner the Polymarket book, the wider the spread, and the more room you have to quote around sportsbook fair value. That’s the niche.
The Data Problem (and the Solution)
| What You Need | DIY Approach | OddsPapi |
|---|---|---|
| Polymarket prices + lay side | Polymarket CLOB API + wallet | ✅ Included (exchangeMeta) |
| FanDuel odds | ❌ No public API | ✅ Included (slug: fanduel) |
| DraftKings odds | ❌ No public API | ✅ Included (slug: draftkings) |
| Same market IDs | Manual mapping needed | ✅ Unified IDs across all books |
| Historical data | Build your own database | ✅ Free on all tiers |
The key insight: OddsPapi uses unified market IDs across all bookmakers. If Polymarket has market 11280 (O/U 237.5 points), FanDuel and DraftKings use the same ID. No mapping, no translation — just compare prices directly.
Step 1: Fetch Sportsbook Fair Value
Get odds for an NBA fixture and extract the implied probability from FanDuel and DraftKings. Average them for a more robust fair value estimate.
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"
# Get an NBA fixture
fixtures = requests.get(f"{BASE}/fixtures", params={
"apiKey": API_KEY, "sportId": 11, "tournamentId": 132, "limit": 10
}).json()
fixture = fixtures[0]
fid = fixture["fixtureId"]
name = f"{fixture['participant1Name']} vs {fixture['participant2Name']}"
print(f"Fixture: {name}")
# Get odds from all bookmakers
odds = requests.get(f"{BASE}/odds", params={
"apiKey": API_KEY, "fixtureId": fid
}).json()
bo = odds["bookmakerOdds"]
def implied_prob(price):
"""Convert decimal odds to implied probability."""
return round(1 / price * 100, 2) if price and price > 0 else None
def get_price(bo, slug, market_id, outcome_id):
"""Get back price for a specific outcome."""
try:
return bo[slug]["markets"][market_id]["outcomes"][outcome_id]["players"]["0"]["price"]
except (KeyError, TypeError):
return None
# Example: Market 11280 = O/U 237.5
market_id = "11280"
outcome_id = "11280" # Over
fd_price = get_price(bo, "fanduel", market_id, outcome_id)
dk_price = get_price(bo, "draftkings", market_id, outcome_id)
if fd_price and dk_price:
fd_prob = implied_prob(fd_price)
dk_prob = implied_prob(dk_price)
fair_value = round((fd_prob + dk_prob) / 2, 2)
print(f"\nO/U 237.5 (Over):")
print(f" FanDuel: {fd_prob}% (odds: {fd_price})")
print(f" DraftKings: {dk_prob}% (odds: {dk_price})")
print(f" Fair Value: {fair_value}%")
Output:
Fixture: Heat vs Wizards
O/U 237.5 (Over):
FanDuel: 53.28% (odds: 1.877)
DraftKings: 54.95% (odds: 1.820)
Fair Value: 54.11%
Why average two sportsbooks instead of one? Because individual book prices include vig (the house edge). Averaging across FanDuel and DraftKings partially cancels out the vig and gives you a cleaner fair value estimate. You can add Pinnacle for even more accuracy — OddsPapi returns it in the same response.
Step 2: Read the Polymarket Order Book
Polymarket odds in OddsPapi include an exchangeMeta field with the lay price — the other side of the market. The regular price field is the back (buy) price. Together, they define the current spread.
def get_poly_spread(bo, market_id, outcome_id):
"""Get Polymarket back price, lay price, and spread."""
try:
outcome = bo["polymarket"]["markets"][market_id]["outcomes"][outcome_id]
back_price = outcome["players"]["0"]["price"]
lay_price = outcome["players"]["0"].get("exchangeMeta", {}).get("lay")
back_prob = implied_prob(back_price)
lay_prob = implied_prob(lay_price) if lay_price else None
spread = round(lay_prob - back_prob, 2) if lay_prob and back_prob else None
return {
"back_price": back_price,
"lay_price": lay_price,
"back_prob": back_prob,
"lay_prob": lay_prob,
"spread_pp": spread
}
except (KeyError, TypeError):
return None
poly = get_poly_spread(bo, "11280", "11280")
if poly:
print(f"Polymarket O/U 237.5 (Over):")
print(f" Back (buy): {poly['back_prob']}% (odds: {poly['back_price']})")
print(f" Lay (sell): {poly['lay_prob']}% (odds: {poly['lay_price']})")
print(f" Spread: {poly['spread_pp']}pp")
Output:
Polymarket O/U 237.5 (Over):
Back (buy): 51.5% (odds: 1.942)
Lay (sell): 54.9% (odds: 1.822)
Spread: 3.4pp
A 3.4 percentage point spread means there’s room between the current best bid and ask on Polymarket. If your sportsbook-derived fair value falls inside that spread, you can quote both sides and profit.
Step 3: Calculate Your MM Quotes
Now combine the fair value from sportsbooks with the Polymarket spread to calculate your bid and ask prices.
def calculate_mm_quotes(fair_value_pct, half_spread_pct=1.5):
"""
Calculate market making bid/ask prices.
Args:
fair_value_pct: Fair value as percentage (e.g., 54.11)
half_spread_pct: Your desired half-spread in pp (e.g., 1.5)
Returns:
bid and ask in cents (Polymarket format)
"""
bid_cents = round(fair_value_pct - half_spread_pct, 1)
ask_cents = round(fair_value_pct + half_spread_pct, 1)
profit_per_contract = round(ask_cents - bid_cents, 1)
return {
"bid_cents": bid_cents,
"ask_cents": ask_cents,
"profit_per_contract": profit_per_contract,
"roi_pct": round(profit_per_contract / bid_cents * 100, 2)
}
quotes = calculate_mm_quotes(fair_value=54.11, half_spread_pct=1.5)
print(f"MM Quotes for O/U 237.5 (Over):")
print(f" Bid: ${quotes['bid_cents']/100:.3f} ({quotes['bid_cents']}¢)")
print(f" Ask: ${quotes['ask_cents']/100:.3f} ({quotes['ask_cents']}¢)")
print(f" Profit if both fill: {quotes['profit_per_contract']}¢/contract")
print(f" ROI per round trip: {quotes['roi_pct']}%")
Output:
MM Quotes for O/U 237.5 (Over):
Bid: $0.526 (52.6¢)
Ask: $0.556 (55.6¢)
Profit if both fill: 3.0¢/contract
ROI per round trip: 5.7%
Your bid at 52.6¢ sits above the current Polymarket back (51.5¢), and your ask at 55.6¢ sits below the current lay (54.9¢). You’re tightening the spread — which is exactly what market makers do. If both orders fill, you pocket 3¢ per contract.
Step 4: Understanding the Risks
Market making isn’t free money. Before you deploy capital, understand the risks:
| Risk | What Happens | Mitigation |
|---|---|---|
| Adverse selection | Informed traders pick off your stale quotes | Widen spread, update quotes when sportsbook lines move |
| Inventory risk | One side fills but not the other — you’re directionally exposed | Hedge with sportsbook bet, or size small |
| Execution risk | Polymarket CLOB doesn’t fill your order | Target markets with some baseline activity |
| Event risk | Game gets cancelled/postponed | Don’t deploy all capital on one event |
The edge from sportsbook fair value helps with adverse selection — you’re less likely to be wrong on direction because FanDuel and DraftKings have already done the pricing work. But you still need to manage position sizing and update quotes as lines move.
Step 5: Build the MM Opportunity Scanner
Here’s the complete scanner that finds the best market making opportunities across NBA fixtures. It identifies markets where Polymarket’s spread is widest relative to sportsbook fair value.
import requests, time
API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"
SPORTSBOOKS = ["fanduel", "draftkings"]
def implied(price):
return round(1 / price * 100, 2) if price and price > 0 else None
def scan_fixture(bo):
"""Find MM opportunities: markets where Polymarket and sportsbooks overlap."""
if "polymarket" not in bo:
return []
poly_markets = bo["polymarket"].get("markets", {})
opportunities = []
for mid, market in poly_markets.items():
# Check if any sportsbook has this market
sb_prices = {}
for sb in SPORTSBOOKS:
if sb in bo and mid in bo[sb].get("markets", {}):
sb_prices[sb] = bo[sb]["markets"][mid]
if not sb_prices:
continue # No sportsbook overlap
# Check each outcome
for oid, outcome in market.get("outcomes", {}).items():
player = outcome.get("players", {}).get("0", {})
back_price = player.get("price")
lay_price = player.get("exchangeMeta", {}).get("lay") if player.get("exchangeMeta") else None
if not back_price or not lay_price:
continue
back_prob = implied(back_price)
lay_prob = implied(lay_price)
if not back_prob or not lay_prob:
continue
poly_spread = round(lay_prob - back_prob, 2)
# Get sportsbook fair value (average implied prob)
sb_probs = []
for sb, sb_market in sb_prices.items():
sb_outcome = sb_market.get("outcomes", {}).get(oid, {})
sb_price = sb_outcome.get("players", {}).get("0", {}).get("price")
if sb_price:
sb_probs.append(implied(sb_price))
if not sb_probs:
continue
fair_value = round(sum(sb_probs) / len(sb_probs), 2)
# Is fair value inside the Polymarket spread?
inside = back_prob < fair_value < lay_prob
gap = round(abs(fair_value - (back_prob + lay_prob) / 2), 2)
opportunities.append({
"market_id": mid,
"outcome_id": oid,
"poly_back": back_prob,
"poly_lay": lay_prob,
"poly_spread": poly_spread,
"fair_value": fair_value,
"inside_spread": inside,
"gap": gap
})
# Sort by spread width (widest = most opportunity)
return sorted(opportunities, key=lambda x: x["poly_spread"], reverse=True)
# Scan NBA fixtures
params = {"apiKey": API_KEY, "sportId": 11, "tournamentId": 132, "limit": 10}
fixtures = requests.get(f"{BASE}/fixtures", params=params, timeout=15).json()
now = time.strftime("%Y-%m-%dT%H:%M:%S")
fixtures = [f for f in fixtures if f.get("startTime", "") >= now][:5]
print(f"Scanning {len(fixtures)} upcoming NBA fixtures...\n")
print(f"{'Match':<35} {'Market':<10} {'Poly Spread':>12} {'Fair Value':>11} "
f"{'Inside?':>8}")
print("-" * 85)
for fx in fixtures:
name = f"{fx['participant1Name'][:15]} vs {fx['participant2Name'][:15]}"
bo = requests.get(f"{BASE}/odds", params={
"apiKey": API_KEY, "fixtureId": fx["fixtureId"]
}, timeout=15).json().get("bookmakerOdds", {})
opps = scan_fixture(bo)
for opp in opps[:3]: # Top 3 per fixture
marker = "YES" if opp["inside_spread"] else "no"
print(f"{name:<35} {opp['market_id']:<10} " f"{opp['poly_spread']:>10.1f}pp "
f"{opp['fair_value']:>10.1f}% {marker:>7}")
Example output:
Scanning 5 upcoming NBA fixtures...
Match Market Poly Spread Fair Value Inside?
-------------------------------------------------------------------------------------
Heat vs Wizards 11288 13.9pp 46.5% YES
Heat vs Wizards 11390 6.3pp 47.2% YES
Heat vs Wizards 11394 3.4pp 53.7% YES
Hawks vs Mavericks 11256 8.2pp 52.1% YES
Hawks vs Mavericks 11344 5.1pp 55.8% no
Rockets vs Raptors 11270 4.7pp 51.2% YES
Markets marked “YES” in the Inside column are the best MM candidates — the sportsbook fair value falls between Polymarket’s bid and ask, meaning you can quote both sides with confidence that your fair value estimate is between them.
Step 6: From Scanner to Execution
This scanner finds the opportunities. Execution on Polymarket happens through their CLOB API. The workflow:
- Run the scanner to find wide-spread markets where fair value is inside the Polymarket spread
- Calculate your quotes — bid below fair value, ask above, adjust half-spread based on your risk tolerance
- Place orders on Polymarket via their CLOB API (requires a funded wallet)
- Monitor with OddsPapi — when FanDuel/DraftKings lines move, update your quotes. Use our prediction market dashboard or CLI terminal for monitoring.
The OddsPapi side (pricing + monitoring) works on the free tier. Polymarket execution is separate — you need a Polymarket account and wallet for that.
Why This Works (And Why Most People Don’t Do It)
The edge is structural. Sportsbooks employ teams of traders who adjust lines based on sharp action, injury news, and statistical models. Polymarket’s prices are set by retail order flow — fewer participants, less information efficiency, and wider spreads on niche markets.
Most Polymarket traders don’t have access to sportsbook pricing. FanDuel and DraftKings don’t have public APIs. Pinnacle’s API is closed to non-commercial accounts. Getting all these prices in one place normally requires enterprise contracts or scraping.
OddsPapi solves the data side: 350+ bookmakers including FanDuel, DraftKings, Pinnacle, Polymarket, and Kalshi — all with unified market IDs, all from one API key. The free tier includes everything you need to run this scanner.
Going Further
- Add more sportsbooks: Include Pinnacle (the sharpest book) in your fair value calculation. OddsPapi returns it alongside FanDuel/DraftKings — just add
"pinnacle"to theSPORTSBOOKSlist. - Soccer markets: Polymarket covers BTTS, Asian Handicaps, and multiple O/U lines on Champions League fixtures. Same scanner works — just change
sportIdto 10. - Historical backtesting: Use OddsPapi’s free historical data to backtest your MM strategy. See our prediction market accuracy backtest for the methodology.
- Real-time updates: Upgrade to WebSockets for sub-second price updates. Stale quotes are the biggest risk in MM — faster data means tighter spreads and less adverse selection.
Stop Guessing Fair Value
Every Polymarket trader without sportsbook data is pricing outcomes with less information than you. FanDuel and DraftKings spend millions on pricing — use their work as your edge.
Get your free API key and run the scanner. Find the widest spreads, calculate your quotes, and start providing liquidity where it matters.
For more on prediction markets and OddsPapi, see our Polymarket & Kalshi API guide.