# InferSports > Sharp Asian odds, built for code. A read-only REST API for live betting odds from 6 Asian > bookmakers plus Pinnacle (football & basketball; 1x2 / Asian handicap / totals; full-time & > half-time). Prices are normalized to decimal and renderable in six formats. Current/live odds > only — no historical data. Read-only; never a betting/execution API. Base URL: https://api.infersports.dev/v1 (local development: http://localhost:8000/v1) Auth: send `Authorization: Bearer `. Keyless requests are served the Free tier with strict rate limits and the sharp book (Pinnacle) excluded. Get a key at https://docs.infersports.dev. All timestamps are ISO-8601 UTC; every odds payload carries an `as_of` freshness stamp. ## MCP tools Agent-first tools. Call them as REST (`POST https://api.infersports.dev/v1/mcp/`, JSON body) or run the MCP server (`python -m app.mcp`, with `INFERSPORTS_API_KEY` set) and connect any MCP client. - [get_sharp_line](https://api.infersports.dev/v1/mcp/get_sharp_line): **Start here for odds questions.** One call: a natural-language fixture → the resolved match plus the worked sharp line (consensus line, best price per outcome across books, de-vigged fair odds from the sharpest book) and a ready-to-read `summary`. Collapses the find_match → compare_lines round-trip. On an ambiguous fixture it returns `status:"ambiguous"` with an `ask_user` prompt instead of guessing. Carries a `decision` block (`safe_to_proceed` / `ask_user` / `next_action`) — branch on the boolean, don't re-judge. - [find_match](https://api.infersports.dev/v1/mcp/find_match): Resolve a natural-language fixture (e.g. "Man City vs Arsenal" or a single team) to a canonical event id, with a confidence score and alternatives, plus a `decision` block (`safe_to_proceed` / `ask_user` / `next_action`): matched ⇒ proceed to get_match_odds, ambiguous ⇒ ask the user, not_found ⇒ list_today_matches. - [match_info](https://api.infersports.dev/v1/mcp/match_info): **Casual basics in ONE call** — the score, whether it's live, the kickoff time (pass an IANA `timezone` for local time), and who's favored (de-vigged from the 1x2 line) for a natural-language fixture. No betting knowledge needed: answers "who's winning?", "what's the score?", "what time does Brazil play (in my timezone)?", "who's the favorite?". Same go/no-go as get_sharp_line; `favorite` is best-effort (null when no 1x2 is on file). - [get_match_odds](https://api.infersports.dev/v1/mcp/get_match_odds): All current quotes for a match across books, in any odds format, with the fixture header. Filter by market, bookmaker and period. Carries `as_of` + a `stale` flag (W4): true when the snapshot is older than the live/pre-match freshness window — warn the user before quoting as live (compare_lines / get_sharp_line carry it too). - [compare_lines](https://api.infersports.dev/v1/mcp/compare_lines): One market across all books — best price per outcome, the consensus line, each book's overround, de-vigged fair odds from the sharpest book, and a ready-to-read `summary`. Pass `verbosity:"terse"` (W2) to drop the per-book `books` array (the bulk of the payload) and keep just the worked answer — `book_count` still reports how many books backed it. get_sharp_line accepts the same `verbosity`. - [explain_handicap](https://api.infersports.dev/v1/mcp/explain_handicap): Split an Asian handicap line (e.g. -0.75 → -0.5 / -1.0) and state the full-win / half-win / push / loss conditions. - [list_today_matches](https://api.infersports.dev/v1/mcp/list_today_matches): Today's (UTC) fixtures, filterable by sport, status (live/scheduled/finished) and league. Each carries a ready-to-read `summary` (live score & clock, or the kickoff time — pass an IANA `timezone` for local time). `finished` is time-derived (kickoff + the sport's max duration), so a just-ended match stops showing as live even while its closed (0/suspended) quotes linger. - [list_bookmakers](https://api.infersports.dev/v1/mcp/list_bookmakers): The bookmakers available on your tier (curated catalogue; Free tier excludes the sharp book). Use the returned `key`s in the `bookmakers` filter. - [get_opening_line](https://api.infersports.dev/v1/mcp/get_opening_line): ONE call: a natural-language fixture → each book's **true opening odds (初盘)** paired with its current price, so you can read movement. For 1x2 it's a price move (the sharp book often opens days earlier); for totals/AH compare `line` (opening) vs `current_line` for the line move. Same `status`/`ask_user` go/no-go as get_sharp_line; best-effort (a book/line with no opening on file is omitted). - [find_value](https://api.infersports.dev/v1/mcp/find_value): ONE call: a natural-language fixture → outcomes whose best price across books **beats the sharp (Pinnacle) de-vigged fair line**, with the edge %. **Detection only — read-only: no stake sizing, no bet links.** Needs the sharp book to de-vig; markets it doesn't quote (and the Free tier, which gates it) yield no value + a `note`. Implausibly large edges (a stale/settled in-play quote, not value) are filtered out. - [find_arbitrage](https://api.infersports.dev/v1/mcp/find_arbitrage): ONE call: a natural-language fixture → **cross-book price inefficiencies** — best price per outcome at the same line whose inverse prices sum to < 1 — with the guaranteed margin % and which book holds each leg (legs from ≥2 distinct books). **Detection only — read-only: no stakes, no bet links.** Implausibly large margins (a stale in-play leg, not a real arb) are filtered out. - [scan_slate](https://api.infersports.dev/v1/mcp/scan_slate): **Batch.** Today's whole slate in one call — each fixture with honest status (finished is excluded from `live`), live score/clock, and a pre-computed value/arb signal. Value/arb matches sort to the top; truncated to `limit`. Use this instead of looping find_match → get_sharp_line per match. No line movement here (drill in with get_opening_line). ## REST endpoints - [List events](https://api.infersports.dev/v1/events): fixtures, filter by sport / league / date / status. - [Live events](https://api.infersports.dev/v1/events/live): in-play fixtures. - [Event odds](https://api.infersports.dev/v1/events/{event_id}/odds): all books, all markets, `?format=` to switch notation. - [Event opening](https://api.infersports.dev/v1/events/{event_id}/opening): true opening odds (初盘) per book/market, `?format=`. - [Event state](https://api.infersports.dev/v1/events/{event_id}/state): live score, cards, match clock. - [Resolve event](https://api.infersports.dev/v1/resolve/event): fuzzy name → event id (POST). - [Convert odds](https://api.infersports.dev/v1/convert/odds): decimal ⇄ HK / Malay / American / Indonesian / probability (POST). - [Convert handicap](https://api.infersports.dev/v1/convert/handicap): split a quarter line + win conditions (POST). - Metadata: [sports](https://api.infersports.dev/v1/sports), [bookmakers](https://api.infersports.dev/v1/bookmakers), [leagues](https://api.infersports.dev/v1/leagues), [teams](https://api.infersports.dev/v1/teams). ## Choosing a tool (natural-language → call) Map the user's intent to one call. Most questions resolve in one or two steps. - "What are the odds / best price / who's sharpest for ?" → `get_sharp_line` (ONE call — pass the natural-language fixture as `query`; no need to find_match first). Shape it with `market_type` ("1x2" | "asian_handicap" | "totals"), `period`, `format`. Returns the resolved match + worked line + a `summary`; prefer it whenever you'd otherwise chain find_match → compare_lines. - "Who's winning / what's the score / what time does play (my timezone) / who's the favorite?" → `match_info` (natural-language `query`; pass an IANA `timezone` for local kickoff time). Casual basics in one call — no betting knowledge needed. - "What games are on today / right now?" → `list_today_matches` (add `status:"live"` for in-play, `sport:"football"|"basketball"` to filter; pass `timezone` for local kickoff times in each `summary`). "Rolling/in-play/滚球" ⇒ `status:"live"`. - "Is vs on?" / "Who does play?" → `find_match` (natural-language `query`). Use the returned `event_id` for the calls below — do NOT guess ids. - "Show me the odds for this match" → `get_match_odds` (needs `event_id`). "European/欧盘/1X2" ⇒ `markets:["1x2"]`; "Asian handicap/让球" ⇒ `["asian_handicap"]`; "over-under/大小球" ⇒ `["totals"]`. "In American odds / Hong Kong / as a probability" ⇒ `format:"american"|"hk"|"probability"`. "Only the first half / 半场" ⇒ `period:"half_time"`. "Only Pinnacle" ⇒ `bookmakers:["pinnacle"]`. - "Which book has the best price / who's sharpest?" → `compare_lines` (needs `event_id`, `market_type`): returns best price per outcome, consensus line, per-book overround, fair odds. - "What does handicap -0.75 mean / how does it settle?" → `explain_handicap` (just `line`). - "What was the opening line / 初盘? How has it moved (open→current)?" → `get_opening_line` (natural-language `query`; same shaping args as get_sharp_line). Returns the true open: 1x2 gives a price move; totals/AH give `line` (opening) vs `current_line` (now) for the line move. - "Where's the value / +EV / which price beats the fair line for ?" → `find_value` (natural-language `query`; `min_edge_pct` to set the bar). Measured vs the sharp (Pinnacle) de-vigged fair line — detection only, never a stake or bet link. - "Is there an arb / sure bet / arbitrage in ?" → `find_arbitrage` (natural-language `query`; `min_margin_pct` to set the bar). Cross-book inefficiency only — detection, no stakes/links. - "Scan tonight's card / where's the value or movement across today's matches?" → `scan_slate` (ONE call for the whole slate, value/arb signal already on each fixture; add `only_signal:true` for just the hits, `status:"live"` for in-play). Then drill into one fixture with get_sharp_line / get_opening_line. Typical chain: `list_today_matches` or `find_match` → take `event_id` → `get_match_odds` / `compare_lines`. The odds response is `{ "event_id", "home_team", "away_team", ..., "odds": [...] }` — a flat list of quotes (no nested `bookmakers` wrapper). For a single fixture's odds you can skip the chain entirely: `get_sharp_line` does resolve + compare in one call. ## Rate limits & efficient use (read before looping) Every key has a per-minute budget (token bucket): **Free = 60 req/min** (burst 30, no sharp books), **Standard = 600 req/min** (burst 120, all books). Keyless requests use the Free budget. Each response carries `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers. To stay within budget (this matters for multi-step agents): - Call `list_today_matches` ONCE to get ids, then query the specific matches you need — do NOT probe every fixture's odds in a loop. A single agent reasoning session can otherwise exhaust even the Standard budget. - On `429 rate_limit_exceeded`, the error carries `retry_after_seconds` — wait that long, then retry. **A 429 means "slow down", NOT "no data".** Do not report the absence of a result to the user. - On `503 upstream_unavailable` (the upstream snapshot is still warming up at startup, or the feed reconnecting), it is retriable — wait `retry_after_seconds` and retry; this is transient, not "no matches today". ## Errors Stable envelope: `{ "error": { "code", "message", "fix", "is_retriable", "retry_after_seconds", "documentation_url" } }`. When `fix` is non-null it is the concrete corrective action — apply it (change the named param / fetch the id it points to) instead of blind-retrying. Switch on `code`: - `invalid_request` (400): malformed input — e.g. an `event_id` that fails to decode, a bad `format`/`market`/`period`, or a `league` value that is not an `lg_…` id (the `league` filter takes an id, not a league name). Fix the request; do not retry unchanged. - `not_found` (404): a well-formed id that has no current match/odds (e.g. it ended or isn't live). - `unauthorized` (401): missing/invalid/revoked key. Note: a copy-pasted key with a stray trailing character is the common cause — keys are exactly `isk_` + 32 chars. - `forbidden` / `plan_limit` (403/402): your tier can't access this (e.g. Free requesting a sharp book, or a market your plan excludes). Not retriable without an upgrade. - `rate_limit_exceeded` (429) / `upstream_unavailable` (503): retriable — honor `retry_after_seconds`. Treat `is_retriable:true` as "back off and retry"; `is_retriable:false` as "apply `fix`, or stop". ## Determinism & caching contract What you can build against — so you can cache aggressively and write stable tests: - **Frozen surface.** The `/v1` path layout is permanent; breaking changes ship under a new prefix (`/v2`), never by mutating `/v1`. Entity ids (`evt_`, `tm_`, `lg_`, `bk_`, `sp_`, `mkt_`) are Sqids-encoded with a fixed alphabet — the same entity keeps the same id across calls and restarts, so ids are safe to persist and to use as cache keys. - **Fixed response shapes.** For a given endpoint the field set never changes between calls. Optional values are sent as `null`, never dropped from the object — deserialize into a fixed struct and skip defensive key-existence checks. The one exception is `POST /v1/convert/odds`, which returns only the `to` formats you ask for. - **Pure functions — same input, same output, forever.** `convert/odds`, `convert/handicap`, `explain_handicap`, and id encode/decode never touch the feed: cache them indefinitely and write exact-value golden tests against them. - **Snapshot reads — shape frozen, values live.** `events*`, `get_match_odds`, `compare_lines`, `get_sharp_line`, `find_match`, `list_today_matches` hold a fixed contract (fields, types, id stability, enum domains) but their numbers move with the market. Assert on shape and field presence in tests, not on exact prices. - **Closed enum domains** (fixed sets — switch on them safely): market_type `1x2 | asian_handicap | totals`; period `full_time | half_time`; event status `scheduled | live | finished`; quote status `open | suspended`; format `decimal | hk | malay | american | indonesian | probability`. - **Explicitly NOT guaranteed:** odds *values*, and the *count* of live fixtures — an empty list is a valid answer when nothing is in-play, not an error or an outage. There is no historical data. ## Reference - [OpenAPI 3.1 spec](https://api.infersports.dev/openapi.json) — full machine-readable contract (every endpoint, parameter, and response schema). Fetch this when you need exact parameter shapes. - `GET /llms-full.txt` (keyless) — the **full agent reference**: this map plus every tool's worked request/response, the complete field tables, Asian-handicap settlement math, the bookmaker id table, and an end-to-end example session. Fetch it once instead of paging through OpenAPI. - [Documentation](https://docs.infersports.dev) - Bookmakers: 7 books — Crown, Sbobet, Macau, M8bet, Pinnacle (sharp), Nova88, HKJC. The sharp book (Pinnacle) is Free-tier gated. - Markets: `1x2`, `asian_handicap`, `totals`. Periods: `full_time`, `half_time`. - Odds formats: `decimal` (default), `hk`, `malay`, `american`, `indonesian`, `probability`. - Coverage is **goals markets only** — corner / card / player-prop markets are out of scope and are not in the feed. Football & basketball only. ## Notes - Read-only API. There are no order/bet/execution endpoints and never will be. If a user asks to place a bet, decline — this API cannot execute wagers. - No historical data — only the current live snapshot is served. "Past results / odds movement / last month" cannot be answered; say so rather than guessing. - All timestamps are ISO-8601 UTC; every odds payload carries an `as_of` freshness stamp. A quote may be `"status":"suspended"` (the book has pulled the line mid-event) — that is normal, not an error. # ───────────────────────────────────────────────────────────────────────────── # llms-full.txt — full agent reference (extended; everything above, plus the detail) # ───────────────────────────────────────────────────────────────────────────── The map above is the quick reference. Below is everything an integration needs without fetching the OpenAPI spec: exact response shapes, settlement math, the bookmaker table, and a full example session. All of it obeys the Determinism & caching contract above. ## Field reference (every field is always present; null where not applicable) ### OddsQuote — one bookmaker × one market line - `bookmaker` (string) — book key, e.g. "pinnacle". - `market_type` (string) — 1x2 | asian_handicap | totals. - `period` (string) — full_time | half_time. - `line` (number|null) — handicap/total line; null for 1x2. - `handicap_components` (number[]|null) — quarter-line split, e.g. [-0.5, -1.0]; null when not a split line. - `prices` (object) — 1x2 → {home, draw, away}; asian_handicap → {home, away}; totals → {over, under}. - `format` (string) — the format these prices are rendered in. - `status` (string) — open | suspended. "suspended" = the book pulled the line mid-event; normal, not an error. - `as_of` (string) — ISO-8601 UTC, last upstream update applied. ### MatchSummary — find_match / list_today_matches / get_sharp_line.match - `event_id` (string) — evt_…; pass to get_match_odds / compare_lines (never invent ids). - `sport` (string) — football | basketball. - `league` (string) — league name. - `home_team` / `away_team` (string). - `scheduled_at` (string|null) — kickoff, ISO-8601 UTC. - `status` (string) — scheduled | live. - `score` (object|null) — {home, away} when live, else null. - `confidence` (number|null) — fuzzy-match score 0–1 (find_match only; null elsewhere). ### decision — the W3 go/no-go block (find_match, get_sharp_line) Pre-computed so you branch on a boolean instead of re-judging: - `safe_to_proceed` (bool) — true → act on the result without asking the user. - `confidence` (number|null) — resolution confidence 0–1. - `ask_user` (string|null) — a disambiguation question to put to the human; null when safe. - `next_action` (object|null) — {tool, args, reason}: the single best next call (e.g. get_match_odds). ### error envelope (every error, every endpoint) { "error": { "code", "message", "fix" | null, "is_retriable", "retry_after_seconds" | null, "documentation_url" } } When `fix` is non-null, it is the concrete corrective action — apply it instead of blind-retrying. ## Worked examples (request → response) ### get_sharp_line — NL fixture → worked sharp line in one call POST https://api.infersports.dev/v1/mcp/get_sharp_line { "query": "Man City vs Arsenal", "market_type": "asian_handicap", "period": "full_time", "format": "decimal" } → { "status": "ok", "query": "Man City vs Arsenal", "summary": "Man City vs Arsenal — consensus AH -0.75 (full_time). Best home 1.98 (crown), best away 1.95 (pinnacle); fair (de-vigged from pinnacle) home 1.96 / away 1.97. As of 2026-05-31T18:24:03Z.", "match": { "event_id": "evt_R5fG2pq", "sport": "football", "league": "English Premier League", "home_team": "Manchester City", "away_team": "Arsenal", "scheduled_at": "2026-05-31T18:00:00Z", "status": "live", "score": { "home": 1, "away": 0 }, "confidence": 0.97 }, "comparison": { "...": "the compare_lines shape below" }, "alternatives": [], "ask_user": null, "as_of": "2026-05-31T18:24:03Z", "stale": false, "decision": { "safe_to_proceed": true, "confidence": 0.97, "ask_user": null, "next_action": null } } Ambiguous query ⇒ status "ambiguous", comparison null, alternatives populated, ask_user set, and decision.safe_to_proceed false. Do not guess — put ask_user to the human. ### compare_lines — one market across all books POST https://api.infersports.dev/v1/mcp/compare_lines { "event_id": "evt_R5fG2pq", "market_type": "asian_handicap", "period": "full_time", "format": "decimal" } → { "event_id": "evt_R5fG2pq", "home_team": "Manchester City", "away_team": "Arsenal", "market_type": "asian_handicap", "period": "full_time", "format": "decimal", "as_of": "2026-05-31T18:24:03Z", "stale": false, "consensus_line": -0.75, "lines_offered": [-0.5, -0.75, -1.0], "best_prices": [ { "outcome": "home", "bookmaker": "crown", "price": 1.98, "decimal": 1.98 }, { "outcome": "away", "bookmaker": "pinnacle", "price": 1.95, "decimal": 1.95 } ], "fair_odds": { "home": 1.96, "away": 1.97 }, "fair_from": "pinnacle", "books": [ { "bookmaker": "pinnacle", "line": -0.75, "prices": { "home": 1.94, "away": 1.95 }, "overround": 0.027, "status": "open" } ], "book_count": 7 } ### get_match_odds — every quote for a match POST https://api.infersports.dev/v1/mcp/get_match_odds { "event_id": "evt_R5fG2pq", "markets": ["asian_handicap"], "bookmakers": ["pinnacle"], "period": "full_time", "format": "decimal" } → { "event_id": "evt_R5fG2pq", "home_team": "Manchester City", "away_team": "Arsenal", "league": "English Premier League", "status": "live", "as_of": "2026-05-31T18:24:03Z", "stale": false, "odds": [ { "bookmaker": "pinnacle", "market_type": "asian_handicap", "period": "full_time", "line": -0.75, "handicap_components": [-0.5, -1.0], "prices": { "home": 1.94, "away": 1.95 }, "format": "decimal", "status": "open", "as_of": "2026-05-31T18:24:03Z" } ] } ### convert/odds — pure function (decimal → other formats) POST https://api.infersports.dev/v1/convert/odds { "value": 2.41, "from": "decimal", "to": ["american","hk","malay","probability"] } → { "decimal": 2.41, "american": 141, "hk": 1.41, "malay": -0.709, "probability": 0.4149 } Only the source format + requested `to` keys are returned — this is the one projected response shape. ### explain_handicap — settlement of an Asian line (pure function) POST https://api.infersports.dev/v1/mcp/explain_handicap { "line": -0.75 } → { "handicap_raw": "-0.75", "handicap_decimal": -0.75, "handicap_components": [-0.5, -1.0], "explanation": "Home gives 0.75 (split -0.5 / -1.0).", "win_conditions": { "full_win": "a >= 0.5", "half_win": "a == 0.25", "push": null, "half_loss": "a == -0.25", "full_loss": "a <= -0.5" } } `win_conditions` are predicates over `a` = the home margin after the handicap = (home − away goals) + line. Integer lines settle win/push/loss; half lines (…-0.5) win/loss only; quarter lines (…-0.75) split the stake into half-win / half-loss. Call explain_handicap for any line — it is deterministic. ### list_today_matches POST https://api.infersports.dev/v1/mcp/list_today_matches { "sport": "football", "status": "live", "league": null, "limit": 50 } → { "date": "2026-05-31", "count": 12, "matches": [ "...MatchSummary..." ] } ### list_bookmakers POST https://api.infersports.dev/v1/mcp/list_bookmakers {} → { "count": 7, "bookmakers": [ { "key": "crown", "name": "Crown", "class": "asian" }, "..." ], "note": "..." } ### get_opening_line — NL fixture → opening (初盘) + current, per book POST https://api.infersports.dev/v1/mcp/get_opening_line { "query": "Man City vs Arsenal", "markets": ["1x2"], "format": "decimal" } → { "status": "ok", "query": "...", "summary": "... crown 1x2 open→current: home 1.78→1.73, ...", "match": { "...MatchSummary..." }, "lines": [ { "bookmaker": "crown", "market_type": "1x2", "period": "full_time", "line": null, "opening": { "home": 1.78, "draw": 3.55, "away": 4.2 }, "current": { "home": 1.73, "draw": 3.6, "away": 4.45 }, "opened_at": "2026-05-30T18:08:49Z", "format": "decimal" }, "..." ], "as_of": "...", "stale": false, "source": "infersports", "decision": { "...W3..." } } Same go/no-go as get_sharp_line: ambiguous ⇒ status "ambiguous" + ask_user (no guess); not_found ⇒ list. 1x2 `opening` is the true match open (compare to `current` for movement); totals/AH `opening` is at the current line. A book/line with no opening on file is omitted (single-point, best-effort lookup). ### find_value — NL fixture → +EV outcomes vs the sharp fair line (DETECTION ONLY, read-only) POST https://api.infersports.dev/v1/mcp/find_value { "query": "Netherlands vs Algeria", "min_edge_pct": 1.0, "format": "decimal" } → { "status": "ok", "query": "...", "summary": "... 1 value spot vs pinnacle; best: away 4.6 @ crown ...", "match": { "...MatchSummary..." }, "fair_source": "pinnacle", "note": null, "value_bets": [ { "market_type": "1x2", "period": "full_time", "line": null, "outcome": "away", "bookmaker": "crown", "price": 4.6, "decimal": 4.6, "fair_price": 3.986, "edge_pct": 15.41, "format": "decimal" } ], "as_of": "...", "stale": false, "decision": { "...W3..." } } edge_pct = (decimal / fair_price − 1) × 100. fair_price = the de-vigged Pinnacle price. With no sharp book visible (Free tier / market not quoted by Pinnacle) `value_bets` is empty and `note` says why. Implausibly large edges (a stale/settled in-play quote, not value — there's no per-quote timestamp, only the match as_of) are filtered out. Surfaces the edge only — never a stake or bet link (read-only). ### find_arbitrage — NL fixture → cross-book sure bets (DETECTION ONLY, read-only) POST https://api.infersports.dev/v1/mcp/find_arbitrage { "query": "Netherlands vs Algeria", "min_margin_pct": 0.0 } → { "status": "ok", "query": "...", "summary": "... 1 arbitrage opportunity; best +5.0% on totals 2.5 ...", "match": { "...MatchSummary..." }, "opportunities": [ { "market_type": "totals", "period": "full_time", "line": 2.5, "legs": [ { "outcome": "over", "bookmaker": "crown", "price": 2.1, "decimal": 2.1 }, { "outcome": "under", "bookmaker": "macau", "price": 2.1, "decimal": 2.1 } ], "inverse_sum": 0.9524, "margin_pct": 5.0 } ], "as_of": "...", "stale": false, "decision": { "...W3..." } } inverse_sum = Σ(1/decimal) over the legs; < 1 ⇒ a locked margin = (1/inverse_sum − 1) × 100. Legs come from ≥2 distinct books (a single-book Σ<1 is a stale/underround line, not an arb, and is not reported). Implausibly large margins (a stale in-play leg, not a real arb) are filtered out. Margin only — no stake split, no bet links. ### scan_slate — today's whole card with the signal already on it (batch; DETECTION ONLY) POST https://api.infersports.dev/v1/mcp/scan_slate { "status": "live", "only_signal": false, "min_edge_pct": 1.0, "limit": 20 } → { "date": "2026-06-03", "count": 21, "with_signal": 3, "returned": 20, "truncated": true, "summary": "21 matches on 2026-06-03; 3 with a value/arb signal (sorted to the top).", "entries": [ { "event_id": "...", "sport": "football", "league": "World Cup", "home_team": "Netherlands", "away_team": "Algeria", "status": "live", "score": { "home": 1, "away": 0 }, "clock": "2H 67", "scheduled_at": "...", "as_of": "...", "stale": false, "book_count": 6, "value_count": 1, "best_value": { "...ValueBet..." }, "arb_count": 1, "best_arb": { "...ArbOpportunity..." }, "signal": "live 1-0 2H 67 · value away +15.4% @crown (1x2) · arb +5% totals 2.5" }, "..." ] } Value/arb matches sort to the top, so `truncated:true` only drops quiet fixtures. `status` is honest — a finished match (now > kickoff + the sport's max duration) is reported `finished`, not `live`, even while its closed (0/suspended) quotes linger. Line movement is NOT here — drill into one fixture with get_opening_line. Same read-only stance as find_value / find_arbitrage. ## Bookmakers (7 books; the sharp book is gated off the Free tier) | key | upstream id | class | Free tier | |---|---|---|---| | pinnacle | 69 | sharp | excluded | | crown | 1 | asian | included | | sbobet | 2 | asian | included | | macau | 10 | asian | included | | m8bet | 52 | asian | included | | nova88 | 78 | asian | included | | hkjc | 90 | asian | included | The id is the upstream company id; use the `key` in `bookmakers=` filters. Live list: GET https://api.infersports.dev/v1/bookmakers. ## Access tiers | tier | requests/min | burst | sharp books | |---|---|---|---| | Free (keyless ok) | 60 | 30 | no | | Standard | 600 | 120 | yes | | Real-time | 6000 | 600 | yes | | Enterprise | 60000 | 2000 | yes | Every response carries X-RateLimit-Limit / -Remaining / -Reset. On 429, honor retry_after_seconds. ## End-to-end example (one complete agent turn) User: "What's the sharpest Asian handicap on Man City vs Arsenal tonight, in Hong Kong odds?" 1. POST https://api.infersports.dev/v1/mcp/get_sharp_line { "query": "Man City vs Arsenal", "market_type": "asian_handicap", "format": "hk" } 2. Branch on `decision.safe_to_proceed`: - true → read `summary` (already user-ready); cite `as_of`; if `stale` is true, say the line may be stale. - false → ask the user `decision.ask_user`; do NOT pick an alternative yourself. 3. Want per-book margins or more books? Reuse the same `event_id` with compare_lines / get_match_odds — one resolve, many reads. Never re-resolve a fixture you already hold an id for. The whole loop: one intent-complete call, branch on the pre-computed decision, never guess.