# 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.