Middles
Find middle opportunities where two sportsbooks have different lines, creating a gap where both bets can win.
GET /api/v1/opportunities/middlesAuthentication
Requires API key. Pro tier or higher required.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
sport | string | all | Filter by sport(s), comma-separated (e.g. basketball, football) |
league | string | all | Filter by league(s), comma-separated (e.g. nba, nfl, nhl) |
sportsbook | string | tier-allowed | Filter by sportsbook(s), comma-separated. Tier limits enforced. |
market | string | all | Filter by market type(s), comma-separated (e.g. point_spread, total_points). Supports category aliases — see Odds: Market Category Aliases. |
min_profit | number | — | Minimum ROI % — filters by each middle’s roi_percentage |
live | boolean | — | true = live only, false = prematch only, omit = both |
max_odds_age | number | — | Max odds age in seconds. Filters out stale opportunities. |
sort | string | quality | Sort field: quality (default), ev, probability, middle_size |
limit | integer | 50 | Results per page (max 200) |
offset | integer | 0 | Pagination offset (max 5000) |
Example Requests
cURL
curl -X GET "https://api.sharpapi.io/api/v1/opportunities/middles?league=nfl&min_profit=1" \
-H "X-API-Key: YOUR_API_KEY"Response
Success (200)
Real response for GET /api/v1/opportunities/middles?league=mlb&live=false&sort=middle_size&limit=1 (captured 2026-07-02; alt_side2 truncated for brevity):
{
"data": [
{
"alt_side1": [
{
"book": "kalshi",
"external_event_id": "KXMLBTOTAL-26JUL022210SDLAD",
"market_id": "KXMLBTOTAL-26JUL022210SDLAD-4",
"odds": -1329,
"selection_id": "KXMLBTOTAL-26JUL022210SDLAD-4_yes"
},
{
"book": "novig",
"external_event_id": "019f1eb1-8730-7851-aab2-7d433205d1fc",
"market_id": "019f20bb-5b05-7841-9c2c-e4fabf16bdcf",
"odds": -3471,
"selection_id": "019f20bb-5b05-7841-9c2c-e500de0ba6e2"
}
],
"alt_side2": [
{
"book": "kalshi",
"external_event_id": "KXMLBTOTAL-26JUL022210SDLAD",
"market_id": "KXMLBTOTAL-26JUL022210SDLAD-8",
"odds": 182,
"selection_id": "KXMLBTOTAL-26JUL022210SDLAD-8_no"
},
{
"book": "novig",
"external_event_id": "019f1eb1-8730-7851-aab2-7d433205d1fc",
"market_id": "019f1eb1-8767-7393-9a85-b234a45f6e58",
"odds": 170,
"selection_id": "019f1eb1-8767-7393-9a85-b25a26e03f2c"
}
],
"away_team": "San Diego Padres",
"best_case_profit": 89.75,
"break_even_percent": 5.4,
"detected_at": "2026-07-02T18:24:11Z",
"event_id": "mlb_dodgers_padres_2026-07-02_b3",
"event_name": "San Diego Padres @ Los Angeles Dodgers",
"expected_value": 28.32,
"guaranteed_roi": null,
"home_team": "Los Angeles Dodgers",
"id": "102f0c34a65c64b6",
"is_guaranteed_profit": false,
"is_live": false,
"is_player_prop": false,
"is_team_total": false,
"key_number_probability": 0,
"key_numbers": [],
"league": "mlb",
"league_label": "MLB",
"market_label": "Total Runs",
"market_overround": 1.054,
"market_type": "total_runs",
"middle_numbers": [
4,
5,
6,
7
],
"middle_probability": 0.3525,
"middle_size": 4,
"odds_age_seconds": 546,
"player_name": null,
"quality_score": 1.84,
"roi_percentage": 28.32,
"side1": {
"book": "polymarket",
"external_event_id": "mlb_dodgers_padres_2026-07-02_b3",
"fair_probability": 0.655,
"line": 3.5,
"market_id": "pm_2763202",
"odds_age_seconds": 244.6,
"odds_american": -223,
"odds_decimal": 1.448,
"odds_probability": 0.6904024767801857,
"selection": "Over",
"selection_id": "2763202_over",
"stake_percent": 65.5
},
"side2": {
"book": "sx_bet",
"external_event_id": "L18646245",
"fair_probability": 0.345,
"line": 7.5,
"market_id": "sx_0x5e051a0f483fc08b7b3d9d5893ca1970a90d693b8072be89ae272e5684be2c6d",
"odds_age_seconds": 546,
"odds_american": 175,
"odds_decimal": 2.75,
"odds_probability": 0.36363636363636365,
"selection": "Under",
"selection_id": "0x5e051a0f483fc08b7b3d9d5893ca1970a90d693b8072be89ae272e5684be2c6d_2",
"stake_percent": 34.5
},
"sport": "baseball",
"start_time": "2026-07-03T02:11Z",
"stat_category": null,
"team_name": null,
"warnings": [
"HIGH_PROBABILITY",
"LARGE_GAP",
"STALE_ODDS"
],
"worst_case_loss": -5.13,
"worst_case_pnl": -5.13
}
],
"pagination": {
"count": 1,
"has_more": true,
"limit": 1,
"next_offset": 1,
"offset": 0,
"total": 31
},
"updated_at": "2026-07-02T18:24:11.737562235Z"
}Response Headers
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 296
X-RateLimit-Reset: 1707401000
X-Data-Delay: 0
X-Request-Id: 1782526326424224-82519Error Responses
401 Unauthorized
No key supplied returns missing_api_key; a bad key returns invalid_api_key.
{
"error": {
"code": "missing_api_key",
"message": "API key required. Pass via X-API-Key header, api_key query parameter, or Bearer token.",
"docs": "https://sharpapi.io/docs/authentication"
}
}403 Tier Restricted
{
"error": {
"code": "tier_restricted",
"message": "This endpoint requires Pro tier or higher",
"docs": "https://sharpapi.io/pricing",
"tier": "hobby",
"required_tier": "pro"
}
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique middle identifier (hash) |
event_id | string | Event identifier |
event_name | string | Human-readable event name |
sport | string | Sport identifier |
league | string | League identifier |
market_type | string | Market type (point_spread, total_points, player prop types) |
market_label | string | Human-readable market name |
league_label | string | Human-readable league name |
home_team | string | Home team name |
away_team | string | Away team name |
start_time | string|null | Game start time (ISO 8601) |
side1 | object | First side of the middle bet |
side2 | object | Second side of the middle bet |
middle_size | number | Size of the middle gap in points |
middle_numbers | number[] | Integer values that would hit the middle |
middle_probability | number | Estimated probability of the middle hitting. Sport- and market-specific: a per-sport multiplicative calibration factor (backtested against settled outcomes and refreshed as data accrues, most recently 2026-07-02) is applied to the raw per-number sum, then clamped to a ceiling of 0.50. |
expected_value | number | EV in dollars per $100 total wagered |
roi_percentage | number | EV as percentage of total stake |
worst_case_loss | number | worst_case_pnl clamped to <= 0 — the downside when the middle misses (0 on guaranteed-profit middles, negative otherwise) |
worst_case_pnl | number | Raw worst-case profit-and-loss per $100 total stake. Positive on guaranteed-profit middles (arb + middle — the worst case still profits), negative otherwise. Equals worst_case_loss only when negative. |
best_case_profit | number | Profit when middle hits (both bets win) |
break_even_percent | number | Minimum hit rate needed to break even |
odds_age_seconds | number|null | Age of stalest underlying odds at detection time |
is_guaranteed_profit | boolean | True when worst_case_loss >= 0 (arb + middle) |
guaranteed_roi | number|null | Worst-case ROI% when guaranteed (null otherwise) |
key_numbers | number[] | Key numbers in middle range (e.g., NFL 3, 7) |
key_number_probability | number | Combined probability of key numbers only |
quality_score | number | Multi-factor quality metric (0-100) |
market_overround | number | Combined implied probability across both sides |
is_live | boolean | Whether the game is currently in-play |
is_player_prop | boolean | Whether this is a player prop middle |
player_name | string|null | Player name (if player prop) |
stat_category | string|null | Stat type (if player prop, e.g., points, rebounds) |
is_team_total | boolean | Whether this is a team-total middle |
team_name | string|null | Team name (if team total) |
alt_side1 | object[] | Other books offering side 1’s selection at the same line — {book, odds, selection_id, market_id, external_event_id} per entry, sorted best price first. Empty array when no alternates. |
alt_side2 | object[] | Other books offering side 2’s selection at the same line (same shape as alt_side1) |
warnings | string[] | Warning flags (see below) |
detected_at | string | When the middle was detected (ISO 8601) |
Side Object
| Field | Type | Description |
|---|---|---|
book | string | Sportsbook for this side |
selection | string | The selection name |
line | number | The line value at this sportsbook |
odds_american | number | American odds |
odds_decimal | number | Decimal odds |
odds_probability | number | Implied probability (0-1) |
fair_probability | number | Fair probability adjusted for overround |
stake_percent | number | Recommended stake allocation (% of total) |
odds_age_seconds | number|null | Age of this side’s odds at detection time |
selection_id | string|null | Sportsbook’s native selection/outcome ID |
market_id | string|null | Sportsbook’s native market ID |
external_event_id | string|null | Sportsbook’s native event ID |
home | object|undefined | Reserved — not currently emitted on this endpoint. Nested home-team reference: {id, numerical_id, name, abbreviation, logo, city, mascot, conference, division}. See Entity reference IDs. |
away | object|undefined | Reserved — not currently emitted on this endpoint. Nested away-team reference (same shape as home). |
sport_ref | object|undefined | Reserved — not currently emitted on this endpoint. Nested sport reference: {id, numerical_id, name}. |
league_ref | object|undefined | Reserved — not currently emitted on this endpoint. Nested league reference: {id, numerical_id, label}. |
market_ref | object|undefined | Reserved — not currently emitted on this endpoint. Nested market reference: {id, numerical_id, label}. |
sportsbook_ref | object|undefined | Reserved — not currently emitted on this endpoint. Nested sportsbook reference for this side’s book: {id, numerical_id, label}. |
Nested reference blocks on each side (reserved)
The side schema reserves six nested reference objects (home, away, sport_ref, league_ref, market_ref, sportsbook_ref) mirroring the blocks on /odds. As of 2026-07-02 the middles engine does not stamp them, so live side1 / side2 objects never include these blocks — today they appear only on /odds rows for atlas-mapped books. Treat every block (and every inner field) as optional and never require them; when emitted they will be purely additive — the flat top-level fields (sport, league, home_team, away_team, market_type) and per-side book / selection strings are unchanged. See Entity reference IDs for the shape and guarantees.
Warning Flags
| Warning | Meaning |
|---|---|
HIGH_PROBABILITY | Calibrated middle probability exceeds 35% — at or beyond the highest empirically realized hit rate; treat as notable (and double-check the lines) |
LARGE_GAP | Middle size exceeds 3 points — verify lines are correct |
STALE_ODDS | One or both odds are older than 300 seconds (5 minutes) |
LIVE_GAME | Game is currently in progress |
GUARANTEED_PROFIT | Worst-case loss is positive — this is also an arbitrage |
Detail Endpoint
Fetch a single middle opportunity by its ID.
GET /api/v1/opportunities/middles/:id| Parameter | Type | Description |
|---|---|---|
id | path | Required. 16-character hex hash (e.g., a1b2c3d4e5f67890) |
Returns { "data": <middle>, "updated_at": "<ISO>" } — the middle uses the same schema as the list items. Returns 404 if the ID is unknown or malformed.
curl -X GET "https://api.sharpapi.io/api/v1/opportunities/middles/a1b2c3d4e5f67890" \
-H "X-API-Key: YOUR_API_KEY"What Are Middles?
A middle occurs when two sportsbooks offer different lines on the same market, creating a numerical gap where both bets can win simultaneously.
Unlike arbitrage (which guarantees profit), middles give you a chance to win both sides of a bet if the final result lands in the “middle” of the two lines.
Spread Middle Example
DraftKings: Chiefs -2.5 @ -110
FanDuel: Bills +7.5 @ -108You bet:
- Chiefs -2.5 at DraftKings
- Bills +7.5 at FanDuel
Possible outcomes:
| Chiefs Win By | Chiefs -2.5 | Bills +7.5 | Result |
|---|---|---|---|
| 1-2 points | Lose | Win | Split (small loss) |
| 3, 4, 5, 6, or 7 | Win | Win | MIDDLE HIT |
| 8+ points | Win | Lose | Split (small loss) |
If the Chiefs win by 3-7, both bets win. The numbers 3 and 7 are NFL key numbers (field goal and touchdown), making this middle especially valuable.
Key Numbers
Certain sports have scoring increments that spike the probability distribution:
| Sport | Key Numbers | Why |
|---|---|---|
| NFL | 3, 7, 10, 14 | Field goal (3), touchdown (7), combinations |
| NHL | 1 | Most games decided by 1 goal (~47%) |
| MLB | 1 | Most games decided by 1 run (~30%) |
| Soccer | 1 | Most matches decided by 1 goal (~38%) |
When a middle contains key numbers, the key_numbers and key_number_probability fields highlight this. An NFL middle spanning 3 and 7 has a combined key number probability of ~24.2%.
Guaranteed Profit (Arb + Middle)
When is_guaranteed_profit is true, the middle is also an arbitrage — the worst case still yields a profit. This is the rarest and most valuable type of middle. The guaranteed_roi field shows the minimum return even if the middle misses.
Middle Size Guide
| Middle Size | Quality | Typical Markets |
|---|---|---|
| 0.5 points | Narrow | Rare opportunities, low hit rate |
| 1.0 points | Standard | Common in spreads and totals |
| 1.5 - 2.0 points | Good | Look for these actively |
| 2.5+ points | Excellent | Uncommon but highly profitable |
Sort by middle_size to surface the widest gaps first, and use min_profit for a minimum-ROI floor. Larger middles have a higher probability of landing in the gap, making them more consistently profitable over time.
Risk Considerations
Middles are not risk-free like arbitrage (unless is_guaranteed_profit is true). When the result falls outside the middle, you lose the vig on the losing side (typically 4-5% of one leg). However, when a middle hits, you win both bets, which more than compensates over time.
Odds Freshness
The odds_age_seconds field shows how old the underlying odds were when the middle was detected. Each side also has its own odds_age_seconds. If the stalest side exceeds 300 seconds (5 minutes), the STALE_ODDS warning is added. Stale odds may have already moved — act quickly on fresh opportunities.
Best Practices
- Focus on larger middles — Sort by
middle_sizeand prefer wider gaps for better hit rates - Watch for key numbers — Middles containing NFL 3/7 or NHL/MLB/soccer 1 have elevated hit rates
- Prioritize guaranteed profit — Filter for
is_guaranteed_profitto find risk-free opportunities - Check odds freshness — Low
odds_age_secondsmeans the lines are current and actionable - Combine with the summary endpoint — Use
/middles/summaryfor dashboard overviews - Act quickly — Lines converge as books adjust, closing the middle window
- Use streaming — Subscribe to
middles:detectedevents via SSE or WebSocket for real-time alerts
Related Endpoints
- Middles Summary — Aggregate statistics without the full list
- +EV Opportunities — Positive expected value bets
- Arbitrage Opportunities — Guaranteed profit opportunities
- SSE Stream — Real-time
middles:detectedandmiddles:expiredevents - Best Odds — Compare odds across all books