A live feed of cross-venue price gaps, no server required
We publish the matched feed on REST for free, and it's a good way to check which games are lined up across Kalshi and Polymarket right now. It's a poor way to watch a price gap actually open. The REST snapshot runs about two minutes behind live, and we've measured how long a cross-venue gap stays open before it closes: about 9.2 seconds at the median. 55% close within 10 seconds, and 96% within 30. A snapshot taken two minutes ago can't show you something that opened and closed a dozen times since then.
Watching a gap open doesn't need a server sitting somewhere. It needs an open connection, and a browser tab can hold one just as well as anything else. Here's how to build that in a single HTML file.
Why the snapshot can't keep up
REST is fine for browsing what's matched and confirming a pair exists, and the free plan gives you that. It is not execution-grade. The two-minute lag comes from how a cached snapshot endpoint works, and we don't plan to remove it. If you want to see a gap the moment it forms, you need the stream, and the stream needs a short-lived ticket before it will talk to you.
Mint a ticket with one curl call
The stream doesn't take your API key directly. You exchange it for a ticket first, then connect with the ticket instead:
curl -X POST https://api.dino.markets/v1/stream/token \
-H "Authorization: Bearer sk_live_..."
The response carries a ticket good for about 120 seconds, the exact WebSocket URL to connect to, and the channels your key is entitled to. Your key never touches the socket itself.
Open the socket from a single HTML file
With a ticket in hand, connect to the stream. No key goes over this second connection, only the ticket, in the connect frame's data.t field:
<script>
async function mintTicket(apiKey) {
const res = await fetch("https://api.dino.markets/v1/stream/token", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
});
return await res.json();
}
function openStream(mint, onGap) {
const ws = new WebSocket(mint.ws_url);
ws.onopen = () => ws.send(JSON.stringify({ id: 1, connect: { data: { t: mint.ticket } } }));
ws.onmessage = (e) => {
// one WS frame can carry several newline-separated objects
for (const line of e.data.split("\n")) {
if (!line) continue;
const msg = JSON.parse(line);
if (Object.keys(msg).length === 0) { ws.send("{}"); continue; } // server ping, pong straight back
if (msg.push?.pub?.data?.type === "price") onGap(msg.push.pub.data);
}
};
return ws;
}
</script>
onGap receives the price update directly: id, priced_at, spread_pts, potential_arb_pct (set only once the two venues cross into a confirmed arb), and an outcomes array with each side's Kalshi and Polymarket ask. Write straight into a table row from there. No framework required, just a script tag and a browser.
Keep this file on your own machine while you're testing. Anyone who views its source can read the key embedded in it, so keep this pattern to a page you run yourself instead of one you publish publicly.
Reconnecting with a fresh ticket
A ticket is single-use and short-lived on purpose, so the page needs to reconnect with a fresh one rather than assume the first connection holds forever:
<script>
async function keepAlive(apiKey, onGap) {
const mint = await mintTicket(apiKey);
const ws = openStream(mint, onGap);
ws.onclose = () => setTimeout(() => keepAlive(apiKey, onGap), 1000);
}
</script>
That's the whole loop. Every time the socket closes, mint a new ticket and reconnect. The ping handling in openStream above matters here too: the server checks for a live client every so often, and a page that doesn't answer gets disconnected well before the 120-second ticket would have expired on its own.
What a browser tab can't do
This gets you a live table of gaps for as long as the tab stays open, and nothing past that. Close the tab and the connection is gone, along with anything you haven't written down elsewhere. There's no history to look back on, and nothing alerts you when you've stepped away from the screen. It's fine for watching. It can't run unattended.
That's a different problem: keeping a consumer alive around the clock, on something that doesn't close when you put your laptop away. We cover that next, in where to run a stream consumer that stays up.
A Free key already opens this exact connection, on a small curated sample of markets, so the code above works today with no payment involved. The full stream across every sport is on Basic and up. The docs walk through the full connect flow, including how your channel access is derived from your key's tier. This is developer data, not trading or financial advice.
ブログの他の記事
Where to run your 24/7 stream consumer, and how to keep it alive
Where to host a stream consumer that runs continuously: a Mac mini already on the desk, a cheap VPS, or a serverless option like Cloudflare Durable Objects, and how to keep each one alive across reboots and crashes.
Send a Telegram alert the moment a cross-venue price gap opens
A qualifying gap can close before you've refreshed a tab. Here's how to subscribe to the matched feed and alert on the moments the server confirms a real arb, then push each one to Telegram before it's gone.
Kalshi対Polymarket、実際の違いはどこにあるか
一方はCFTC規制下の取引所、もう一方はウォレット・ネイティブでオンチェーン。構造的に何が両者を分けているか、そして私たち自身のマッチング済みデータが、両者の価格が互いにどう動くかについて何を示しているか。