Skip to content

WebSocketストリーム

リアルタイムプッシュです。チケットを発行して接続すると、あなたのプランが権限を持つ機会を、サーバーが欠落なく配信します。ハートビートと再接続時の復旧付きです。

WebSocketストリームは、APIのリアルタイム側です。機会が開いた、あるいは変化した瞬間にそれぞれをプッシュし、ポーリングも遅延もありません。BasicとProで利用できます。Freeキーは RESTスナップショットは読めますが、ストリームチケットは発行できません。

ストリームはCentrifugoプロトコルを話すため、生のソケットではなく、お使いの言語向けの centrifuge クライアントで接続します。接続は2段階のハンドシェイクです。まずRESTで短命のチケットを発行し、それを使ってソケットを開きます。

1. チケットを発行する

APIキーを使って POST /v1/stream/token を呼び出します。チケットの有効期間は約120秒で、1回の接続につき使い捨てです。

curl -s -X POST "https://api.dino.markets/v1/stream/token" \
  -H "Authorization: Bearer sk_live_..."
{
  "ticket": "eyJhbGciOi...",
  "ws_url": "wss://stream.dino.markets/connection/websocket",
  "expires_in": 120,
  "allowed": {
    "channels": ["markets:all", "status"],
    "max_conns": 3
  }
}

allowed は、あなたのプランが許可する内容、つまりサーバーが購読させるチャンネルと同時接続数の上限をそのまま反映します。Freeまたはプラン切れのキーはここでアップグレードへのリンク付きの 402 を受け取ります。サイレントに空のチケットが返ることはありません。

2. 接続する

ws_url にソケットを開き、接続データの t にチケットを渡します。手動でのチャンネル購読は不要です。サーバーがチケットからあなたのプランを読み取り、権限のあるチャンネルにちょうど購読させるので、接続した瞬間からpublicationが届き始めます。

import { Centrifuge } from "centrifuge";

const { ticket, ws_url } = await mintTicket();

const centrifuge = new Centrifuge(ws_url, {
  data: { t: ticket },
});

centrifuge.on("publication", (ctx) => {
  console.log(ctx.channel, ctx.data);
});

centrifuge.connect();

チケットが短命なのは意図的な設計です。新しい接続ごとに新しいチケットを発行してください。一度確立した接続はクライアントが維持するので、再発行が必要になるのはゼロから再接続するときだけです。

チャンネル

サーバーはあなたのティアからチャンネルの組み合わせを選びます。以下のうち一部を受信することになります。

チャンネルプラン内容
markets:allBasic、Proマッチング済みフィードのストリーム。アクティブな全スポーツを横断する、両venue共通上場のmarketすべてを、状態フレームと価格フレームとして配信します。v2の正規チャンネルです。スコープが全スポーツの場合はこれを受信します。
markets:{sport}Basic、Pro1つの大分類スポーツ(例: markets:baseball)のマッチング済みフィード・ストリームです。スコープが一部のスポーツに限定される場合は、markets:all の代わりにこちらを受信します。
statusBasic、Proハートビートです。ハートビートを参照してください。
quote:{sport}Proスポーツごとの、生のトップ・オブ・ブック(最良気配)の価格変化です。
candidates:{sport}Proスポーツごとのレビュー待ちcandidates、つまり確定前の暫定マッチです。マッチングと確信度を参照してください。
org:{id}Basic、Proあなたの組織のプレゼンスチャンネルです。すべての接続がこれに参加し、あなたの同時接続数を上限と照合してカウントします。このチャンネルでデータを送受信することはありません。

スポーツは大分類のキーを使います: baseballbasketballfootballhockeysoccertennisloldota2valorantcs2mma。1つのスポーツが複数のリーグにまたがることもあります(baseballはMLB、KBO、NPBを含みます)が、チャンネルはリーグ単位ではなくスポーツ単位です。現在シーズン中の組み合わせは/v2/leaguesで確認してください。

メッセージ

届く内容はチャンネルの系統によって異なります。markets:* チャンネルはフラットな market フレームと price フレームを運びます。status チャンネルはハートビートを運びます。すべてのメッセージは type フィールドを持つJSONです。

marketsチャンネルでの内容

markets:* チャンネルはv2の正規ストリームで、marketオブジェクトを運びます。フレームはフラットです。type フィールドで種別を判別し、data によるラップも、channel キーも、schema_version キーもありません。フレームは2種類あります。

market フレームは、ペアの状態変化(新しいペアがアクティブになった、ペアが取り消された、signalが変わった)のたびに送信されます。これは、type 判別子が付いた、正規のmarketオブジェクトそのものです。signal"arb" のときは、outcomeごとの板の厚み(デプス)情報も付与されます。market フレームでの priced_at は、そのフレームが組み立てられた瞬間です。同じフィールドがRESTレスポンスではペアの updated_at(ライブの価格用カラムはまだ存在しません)を反映するため、ストリームの方がより新しいタイムスタンプを運びます。

{
  "type": "market",
  "id": "dino_8f3a1c92-47e8-4c3f-b9d2-f1a8e6c4d5f2",
  "...": "the rest of the market object"
}

price フレームは、価格のみの変化のたびに送信されます。安定したoutcomeの key をキーにしたコンパクトな差分で、価格に由来するトップレベルのスカラー値をすべて運ぶので、キャッシュ済みの market オブジェクトが古くなることはありません。

{
  "type": "price",
  "id": "dino_8f3a1c92-47e8-4c3f-b9d2-f1a8e6c4d5f2",
  "priced_at": "2026-07-01T22:55:02+00:00",
  "spread_pts": 2.0,
  "potential_arb_pct": 1.83,
  "outcomes": [
    { "key": "NYY", "kalshi": 0.52, "polymarket": 0.54 },
    { "key": "BOS", "kalshi": 0.50, "polymarket": 0.46 }
  ]
}

ペアのsignalが arb のとき、price フレームの各outcomeにはさらに kalshi_bookpolymarket_book の板の厚み(デプス)オブジェクトが付与され、フレームにはトップレベルの max_stake(消費される側の、より安いaskにおける各outcome中の最小デプス)が付きます。これにより、ストリーミング中のアービトラージがサイズ不明になることはありません。消費対象の脚のいずれかにデプスの読み取り値がない場合、max_stakenull になります。

フレームは markets:{sport}markets:all の両方に同時配信され、配信方式はat-least-once(最低1回配信)です。部分的な配信のあと、次のスイープで再送されるため、まれに重複フレームが届くことがあります。フレームは自己記述的かつ冪等なので、重複を適用しても害はありません。

status メッセージがハートビートで、次に解説します。

ハートビート

アービトラージは稀なイベントなので、フィードはほとんどの時間、静かです。無音のソケットには曖昧さがあります。アービトラージが何も開いていないのか、それともフィード自体が落ちているのか区別できません。status チャンネルはその曖昧さを取り除きます。brainは、アービトラージが開いているかどうかにかかわらず、タイマーに従ってハートビートを配信します。

{
  "type": "status",
  "schema_version": 1,
  "channel": "status",
  "ts": "2026-06-25T21:00:00+00:00",
  "data": {
    "state": "ok",
    "active_pairs": 312,
    "open_opportunities": 0
  }
}
フィールド意味
stateフィードがライブで正常に稼働しているときは ok。コールドスタートのウォームアップ中や、アービトラージが保留される復旧期間中は degraded
active_pairsbrainが現在監視しているマッチング済みゲームの数。
open_opportunities現在開いているアービトラージの数。ok0 の組み合わせのハートビートは、フィードは健全だが単に行動対象がないことを意味します。

何かがおかしいことの兆候として扱うべきは、静かなアービトラージフィードではなく、ハートビートの途絶です。

接続数の上限

各プランには同時接続数の上限があります。Basicは3、Proは10、Freeには上限がありません(接続不可)。この上限は、あなたの組織のプレゼンスチャンネルからサーバー側でカウントされるため、プロセスやマシンをまたいでも維持されます。上限を超えた接続は拒否されます。この上限は価値の尺度そのものです。並行して複数の戦略を走らせたい、あるいは本番と並行してステージング用の接続を持ちたいという場合、それが上位ティアに払う対価です。

スナップショットからのブートストラップ

まず完全な状態から始めて、その後ライブに追従するには、GET /v2/markets?signal=arb を読んで開いているアービトラージをすべて読み込んでから、ストリームに接続してください。RESTスナップショットにはストリームのオフセット・フィールドがないため、正確な再開ポイントをストリームに渡す方法はありません。RESTの読み取りとストリームの接続は、2つの独立したビューとして扱ってください。実際には、スナップショットを読み込んですぐに接続すると、スナップショットに既にあるメッセージがストリームにも届く、小さな重複ウィンドウが生じます。market の id で重複排除してください。

切断後の復旧

ストリームの各チャンネルは、約100メッセージ・5分間の短い履歴を保持しています。centrifuge クライアントは最後に見たメッセージの offset を記録しているため、再接続時にはその時点から欠落分の再送をサーバーに要求します。短い切断であれば、これは自動的かつ欠落なく行われます。

履歴ウィンドウより長く切断されていた場合、復旧ではギャップを埋められません。クライアントは復旧失敗を通知するので、その場合は部分的なストリームを信用せず、新しいRESTスナップショットからもう一度ブートストラップしてください。