Skip to content

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.

ParamInTypeMeaning
sportquerystringLimit to one broad sport, such as baseball. Omit for all sports.
leaguequerystringNarrow within a sport to one league, case-insensitive, such as MLB. Ignored when signal=arb.
statusquerystringComma-separated: open, live, closed, settled. Default open,live.
signalquerystringspread (default catalog view), arb (priced, confirmed arb rows only), or candidates (review-band rows).
sortquerystringstart_time (default) or -spread_pts to put the widest cross-venue gaps first.
includequerystringComma-separated opt-in blocks: ids, depth, settlement. See Includes.
limitqueryinteger1 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.

ParamInTypeMeaning
sportquerystringLimit to one broad sport, such as baseball.
includequerystringComma-separated opt-in blocks: ids, depth, settlement.
limitqueryinteger1 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.

ParamInTypeMeaning
sportquerystringLimit to one broad sport.
leaguequerystringNarrow within a sport to one league.
includequerystringComma-separated opt-in blocks: ids, depth, settlement.
limitqueryinteger1 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.

ValueAddsMeaning
idsvenues, confidenceVenue handles (kalshi.event_ticker, polymarket.condition_id) and the matcher's entity/resolution confidence, 0 to 1.
depthoutcomes[].kalshi_book, outcomes[].polymarket_bookPer-outcome book detail (ask, depth, ticker/token id) alongside the scalar ask price. A sibling field; it never replaces outcomes[].kalshi or .polymarket.
settlementsettlementparity (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.

ParamInTypeMeaning
market_idpathstringThe 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.

FieldTypeMeaning
opp_idstringThe market's id value (dino_<uuid>).
reasonstringA short reason or note.
sportstringThe sport, when there is no opp_id.
marketstringThe market or game.
detailstringAny 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.