REST API
The endpoints for the matched feed, the live arb view, the pair detail, and the catalog and feedback routes. Every request takes your API key as a bearer token.
The REST API serves the matched cross-venue feed, the arb view on top of it, and pair detail. It runs on every plan, including Free. The base URL is https://api.dino.markets.
Authentication
Every request carries your API key as a bearer token. Create a key in the dashboard; it is shown once.
Authorization: Bearer sk_live_...
Rate limits
Requests are capped per plan, per minute: Free 60, Basic 300, Pro 1200. Past the cap a request returns 429. The stream is not metered this way; it is bounded by the connection cap instead.
GET /v2/markets
The canonical market list. Unfiltered, this is the identity catalog: one row per matched game, with venue tickers and coverage flags, and prices left null. Add signal=arb to get the same rows priced from the live opportunity snapshot, with only confirmed arbs returned. Add signal=candidates for review-band rows priced from the candidates channel history.
| Param | In | Type | Meaning |
|---|---|---|---|
sport | query | string | Limit to one broad sport, such as baseball. Omit for all sports. |
league | query | string | Narrow within a sport to one league, case-insensitive, such as MLB. Ignored when signal=arb. |
status | query | string | Comma-separated: open, live, closed, settled. Default open,live. |
signal | query | string | spread (default catalog view), arb (priced, confirmed arb rows only), or candidates (review-band rows). |
sort | query | string | start_time (default) or -spread_pts to put the widest cross-venue gaps first. |
include | query | string | Comma-separated opt-in blocks: ids, depth, settlement. See Includes. |
limit | query | integer | 1 to 1000. Default 500. |
The valid sport keys are baseball, basketball, football, hockey, soccer, tennis, cricket, lol, dota2, valorant, cs2, r6, ow, and mma. Each row also has a league field, which can be null when the venue series does not name one. Call /v2/leagues to see which sports and leagues are in season.
curl -s "https://api.dino.markets/v2/markets?sport=baseball&league=MLB" \
-H "Authorization: Bearer sk_live_..."
{
"count": 1,
"sport": "baseball",
"has_more": false,
"markets": [
{
"id": "dino_8f3a1c92-47e8-4c3f-b9d2-f1a8e6c4d5f2",
"slug": null,
"sport": "baseball",
"league": "MLB",
"title": "Nyy vs Bos",
"home": null,
"away": null,
"start_time": "2026-07-01T23:05:00+00:00",
"status": "open",
"match": "high_confidence",
"signal": "spread",
"spread_pts": null,
"potential_arb_pct": null,
"coverage": { "kalshi": false, "polymarket": false },
"outcomes": [],
"result": null,
"updated_at": "2026-07-01T22:55:00+00:00",
"priced_at": "2026-07-01T22:55:00+00:00",
"links": { "kalshi": "https://kalshi.com/markets/KXMLBGAME-26JUL01NYYBOS", "polymarket": null }
}
]
}
The unfiltered view carries no prices. outcomes is empty, signal is always spread, and coverage reads false on both venues. That's expected: this is the identity catalog. priced_at on this view is the row's updated_at (the last time the pair itself changed), not a live price tick — there's no separate pricing timestamp stored per row yet. For live prices on the same rows, ask for signal=arb (only confirmed arbs) or add include=depth on a single market over /v2/pairs/{market_id}.
The arb view: GET /v2/markets?signal=arb
The same endpoint, priced from the live opportunity snapshot and filtered to rows the matcher has confirmed as arb: settlement parity confirmed, no blocking settlement risk, and both venues priced on every outcome. This replaces the old dedicated opportunities feed. There is no separate arb endpoint in v2; it's a view over the market catalog. The snapshot backing it is best-effort and about two minutes behind on every plan.
| Param | In | Type | Meaning |
|---|---|---|---|
sport | query | string | Limit to one broad sport, such as baseball. |
include | query | string | Comma-separated opt-in blocks: ids, depth, settlement. |
limit | query | integer | 1 to 1000. Default 500. |
The catalog parameters above apply to this view as well: status, sort, and include work the same way (the league filter is the one exception, ignored here).
curl -s "https://api.dino.markets/v2/markets?signal=arb&sport=baseball" \
-H "Authorization: Bearer sk_live_..."
{
"count": 1,
"sport": "baseball",
"has_more": false,
"markets": [
{
"id": "dino_8f3a1c92-47e8-4c3f-b9d2-f1a8e6c4d5f2",
"slug": null,
"sport": "baseball",
"league": "MLB",
"title": "Nyy vs Bos",
"home": null,
"away": null,
"start_time": "2026-07-01T23:05:00+00:00",
"status": "open",
"match": "high_confidence",
"signal": "arb",
"spread_pts": 2.0,
"potential_arb_pct": 1.83,
"coverage": { "kalshi": true, "polymarket": true },
"outcomes": [
{ "key": "NYY", "name": "NYY", "kalshi": 0.52, "polymarket": 0.54, "cheaper": "kalshi" },
{ "key": "BOS", "name": "BOS", "kalshi": 0.50, "polymarket": 0.46, "cheaper": "polymarket" }
],
"result": null,
"updated_at": "2026-07-01T22:55:00+00:00",
"priced_at": "2026-07-01T22:55:00+00:00",
"links": { "kalshi": "https://kalshi.com/markets/KXMLBGAME-26JUL01NYYBOS", "polymarket": null }
}
]
}
An empty markets array with count: 0 is the normal resting state between arbs. potential_arb_pct is the gross ask-to-ask return, before venue fees. It's an observation of the snapshot, best-effort and cache-stale. outcomes[].kalshi and .polymarket are the ask price on each venue, 0 to 1, and cheaper names the venue with the lower ask for that outcome. On REST, priced_at mirrors updated_at (the pair's last write) rather than the live snapshot instant; the WebSocket feed carries the true books-read timestamp on every price tick.
Candidates: GET /v2/markets?signal=candidates
Review-band rows: matches the matcher has made but has not yet confirmed. Priced best-effort from the same channel history that feeds the candidates:{sport} WebSocket channel (Pro). A review-status market always resolves to spread, no matter what prices are attached; this view never returns a signal: "arb" row.
| Param | In | Type | Meaning |
|---|---|---|---|
sport | query | string | Limit to one broad sport. |
league | query | string | Narrow within a sport to one league. |
include | query | string | Comma-separated opt-in blocks: ids, depth, settlement. |
limit | query | integer | 1 to 1000. Default 500. |
curl -s "https://api.dino.markets/v2/markets?signal=candidates&sport=baseball" \
-H "Authorization: Bearer sk_live_..."
The catalog parameters above apply to this view as well: status, sort, and include work the same way.
Rows come back in the same canonical shape as the catalog view. Where the candidate has a priced match, outcomes and coverage are populated the same way as the arb view; where pricing isn't available yet, prices are left null.
Includes
include adds opt-in blocks to each market object. Absent blocks mean the key is missing entirely, not null. Request what you need.
| Value | Adds | Meaning |
|---|---|---|
ids | venues, confidence | Venue handles (kalshi.event_ticker, polymarket.condition_id) and the matcher's entity/resolution confidence, 0 to 1. |
depth | outcomes[].kalshi_book, outcomes[].polymarket_book | Per-outcome book detail (ask, depth, ticker/token id) alongside the scalar ask price. A sibling field; it never replaces outcomes[].kalshi or .polymarket. |
settlement | settlement | parity (bool), risks[], venue text, and verdict once settled. |
GET /v2/pairs/{market_id}
One canonical market object by id, with include=ids,depth,settlement applied by default. This is the detail view. Accepts dino_<uuid> (the id field from /v2/markets) or a bare UUID.
| Param | In | Type | Meaning |
|---|---|---|---|
market_id | path | string | The market id from /v2/markets, either dino_<uuid> or the bare UUID. |
Example: MLB arb (confirmed settlement parity)
curl -s "https://api.dino.markets/v2/pairs/dino_8f3a1c92-47e8-4c3f-b9d2-f1a8e6c4d5f2" \
-H "Authorization: Bearer sk_live_..."
{
"id": "dino_8f3a1c92-47e8-4c3f-b9d2-f1a8e6c4d5f2",
"slug": null,
"sport": "baseball",
"league": "MLB",
"title": "Nyy vs Bos",
"home": null,
"away": null,
"start_time": "2026-07-01T23:05:00+00:00",
"status": "open",
"match": "high_confidence",
"signal": "arb",
"spread_pts": 2.0,
"potential_arb_pct": 1.83,
"coverage": { "kalshi": true, "polymarket": true },
"outcomes": [
{ "key": "NYY", "name": "NYY", "kalshi": 0.52, "polymarket": 0.54, "cheaper": "kalshi",
"kalshi_book": { "ask": 0.52, "bid": null, "depth": 3100, "ticker": "KXMLBGAME-26JUL01NYYBOS-NYY" },
"polymarket_book": { "ask": 0.54, "bid": null, "depth": 2900, "token_id": "0x1a2b..." } },
{ "key": "BOS", "name": "BOS", "kalshi": 0.50, "polymarket": 0.46, "cheaper": "polymarket",
"kalshi_book": { "ask": 0.50, "bid": null, "depth": 2400, "ticker": "KXMLBGAME-26JUL01NYYBOS-BOS" },
"polymarket_book": { "ask": 0.46, "bid": null, "depth": 3300, "token_id": "0x9f8e..." } }
],
"result": null,
"updated_at": "2026-07-01T22:55:00+00:00",
"priced_at": "2026-07-01T22:55:00+00:00",
"venues": {
"kalshi": { "event_ticker": "KXMLBGAME-26JUL01NYYBOS" },
"polymarket": { "condition_id": "0x1a2b...", "slug": null }
},
"confidence": { "entity": 1.0, "resolution": 1.0 },
"settlement": { "parity": true, "risks": [], "kalshi_text": null, "poly_text": null, "verdict": null },
"links": { "kalshi": "https://kalshi.com/markets/KXMLBGAME-26JUL01NYYBOS", "polymarket": null }
}
Example: KBO spread (cross-venue settlement risk)
curl -s "https://api.dino.markets/v2/pairs/dino_5c2b9f7e-d4a1-4e6c-8b3f-a9c2e5f1d8a4" \
-H "Authorization: Bearer sk_live_..."
{
"id": "dino_5c2b9f7e-d4a1-4e6c-8b3f-a9c2e5f1d8a4",
"sport": "baseball",
"league": "KBO",
"title": "Kia vs Kiw",
"start_time": "2026-07-15T10:00:00+00:00",
"status": "open",
"signal": "spread",
"spread_pts": null,
"potential_arb_pct": null,
"coverage": { "kalshi": false, "polymarket": false },
"outcomes": [],
"settlement": {
"parity": false,
"risks": [
{
"event": "tie",
"severity": "high",
"note": "This league can tie, but one venue's market does not carry an explicit tie->50/50 clause. A held two-leg position may not return $1 on a tie. Verify before trading."
}
],
"kalshi_text": null,
"poly_text": null,
"verdict": null
},
"links": { "kalshi": "https://kalshi.com/markets/KXMLBGAME-26JUL15KIWKIA", "polymarket": null }
}
A high or critical severity risk against the pair forces signal to spread, even if the prices line up; that check is one of the four gates behind signal and runs on the pair's complete internal risk set. The served risks[] never includes cancellation (cancel_*) events, which are filtered server-side. Read settlement.risks before acting on any row, arb or spread. If the id doesn't resolve, the response is 404 with { "error": "pair not found" }.
GET /v2/leagues
The sports and leagues in season right now, derived from the active feed, with lifecycle counts per sport.
The leagues array lists the non-null leagues seen for that sport; count is every matched game in it, including rows whose league is null. open mirrors count today; live and settled are always 0, since the game-clock and retracted-row counts aren't wired into this endpoint yet.
curl -s "https://api.dino.markets/v2/leagues" \
-H "Authorization: Bearer sk_live_..."
{
"sports": [
{ "sport": "baseball", "leagues": ["KBO", "MLB", "NPB"], "count": 54, "open": 54, "live": 0, "settled": 0 },
{ "sport": "soccer", "leagues": ["World Cup"], "count": 15, "open": 15, "live": 0, "settled": 0 },
{ "sport": "tennis", "leagues": ["ITFW"], "count": 165, "open": 165, "live": 0, "settled": 0 }
]
}
GET /v2/coverage
The venue-coverage census: every head-to-head league listed on Kalshi, wired into the matched feed or not, with its aggregation status. The matcher rebuilds it on every pass, about hourly. Status values: aggregated (paired live), pending (wired, awaiting a counterpart or its confirmation passes), quiet (wired, nothing open), kalshi_only (listed there, no pairing wired), dormant (series exists, nothing open). poly_unpaired carries the sport-level Polymarket surplus: parsed fixtures the pass held no Kalshi pair for.
curl -s "https://api.dino.markets/v2/coverage" \
-H "Authorization: Bearer sk_live_..."
{
"as_of": "2026-07-04T03:30:21+00:00",
"counts": { "aggregated": 20, "pending": 7, "quiet": 43, "kalshi_only": 13, "dormant": 139 },
"series": [
{ "series_ticker": "KXFIBAGAME", "league": "FIBA", "sport": "basketball", "wired": true,
"status": "aggregated", "kalshi_open_events": 49, "paired": 46, "sport_poly_fixtures": 88,
"last_pass_at": "2026-07-04T03:30:21+00:00" }
],
"poly_unpaired": [ { "sport": "soccer", "fixtures": 258, "paired": 7, "unpaired": 251 } ]
}
The census is API-only; the same data backs the internal coverage view.
POST /v1/stream/token
Mint a short-lived ticket for the real-time stream. This endpoint did not move to /v2; it stays at /v1/stream/token. It requires Basic or Pro; a Free key gets a 402. The full connect flow is on the WebSocket stream page.
{
"ticket": "eyJhbGciOi...",
"ws_url": "wss://stream.dino.markets/connection/websocket",
"expires_in": 120,
"allowed": { "channels": ["markets:all", "status"], "max_conns": 3 }
}
POST /v2/report-bad-arb
Flag an opportunity that looks wrong. It files a report for our review queue. Include at least one of the fields below. For opp_id, pass the market's id value from /v2/markets.
| Field | Type | Meaning |
|---|---|---|
opp_id | string | The market's id value (dino_<uuid>). |
reason | string | A short reason or note. |
sport | string | The sport, when there is no opp_id. |
market | string | The market or game. |
detail | string | Any extra detail. |
curl -s -X POST "https://api.dino.markets/v2/report-bad-arb" \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "opp_id": "dino_8f3a1c92-47e8-4c3f-b9d2-f1a8e6c4d5f2", "reason": "legs do not settle the same way" }'
A successful report returns 202 with { "status": "received" }. Send at least one of opp_id, reason, sport, market, detail; an empty body returns 400.