Send a Telegram alert the moment a cross-venue price gap opens
Not every gap is worth a message. Most of what crosses the feed is small and closes in seconds on its own, and a phone that buzzes for all of it stops getting checked. The stream already tells you which gaps are real: potential_arb_pct on a price update is only ever set once the two venues cross into a confirmed, settlement-verified arb, so filtering on it costs nothing extra to compute. In our own measurements, the widest gaps we've recorded held for a median of 13.7 seconds before closing, longer than the 9.2-second median across every gap, but still not long enough to notice by checking a screen. This is what an alert is for.
Subscribing to the matched feed
The connect flow is the same one covered in watching gaps in a browser and running a consumer that stays up: mint a short-lived ticket, then connect and pass it in the connect frame's data.t field. The channels available to you come from your key's tier, so there's no separate subscription step once you're connected.
import json
import os
import time
import requests
import websocket
API_KEY = os.environ["DINO_API_KEY"]
TELEGRAM_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
TELEGRAM_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
THRESHOLD_PCT = 5.0
DEBOUNCE_SECONDS = 30
_last_sent = {}
def mint_ticket():
resp = requests.post(
"https://api.dino.markets/v1/stream/token",
headers={"Authorization": f"Bearer {API_KEY}"},
)
return resp.json()
def run():
while True:
mint = mint_ticket()
ws = websocket.create_connection(mint["ws_url"])
ws.send(json.dumps({"id": 1, "connect": {"data": {"t": mint["ticket"]}}}))
try:
while True:
# one WS frame can carry several newline-separated objects
for line in ws.recv().split("\n"):
if not line:
continue
msg = json.loads(line)
if msg == {}:
ws.send("{}") # server ping, pong straight back or the connection drops
continue
data = (msg.get("push") or {}).get("pub", {}).get("data")
if data and data.get("type") == "price":
handle_event(data)
except Exception:
pass
finally:
ws.close()
time.sleep(1)
if __name__ == "__main__":
run()
Filtering for a qualifying gap
handle_event is where the threshold gets applied. potential_arb_pct is absent on most price updates and only appears once the server confirms a real arb, so anything below the threshold, or not an arb at all, gets dropped before it ever reaches Telegram:
def handle_event(event):
arb_pct = event.get("potential_arb_pct")
if arb_pct is None or arb_pct < THRESHOLD_PCT:
return
outcomes = event.get("outcomes") or []
label = " vs ".join(o["key"] for o in outcomes[:2]) if len(outcomes) >= 2 else event["id"]
maybe_alert(event["id"], label, arb_pct)
Pushing to Telegram
A Telegram bot's API takes a plain POST, and sending a message is a few lines once you have a bot token and a chat ID from BotFather:
def send_telegram(text):
requests.post(
f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
data={"chat_id": TELEGRAM_CHAT_ID, "text": text},
)
A Discord webhook takes the same shape. Swap the URL for the webhook Discord gives you, and put the text in a content field instead of text. Everything upstream of this function stays the same either way.
Debouncing one gap into one message
A gap that clears 5% and holds for its full 13.7-second median can still cross your socket as several events while the two prices keep adjusting toward each other. Without a check in place, that's several messages for something a person experiences as one moment. maybe_alert keeps a timestamp per market and skips anything within a debounce window of the last message it sent for that same market:
def maybe_alert(mm_id, label, arb_pct):
now = time.time()
if now - _last_sent.get(mm_id, 0) < DEBOUNCE_SECONDS:
return
_last_sent[mm_id] = now
send_telegram(f"Arb on {label}: {arb_pct:.1f}%")
Thirty seconds comfortably covers the 13.7-second median of even the widest gaps without collapsing two separate gaps on the same game into one message.
What this doesn't do
This script reads the feed and checks a threshold, then sends a message if the gap clears it. It doesn't place an order, and it never touches either venue's trading API. This is developer data, not trading or financial advice, and the alert is the whole deliverable here. Where this actually runs, so it's still awake at 3am when the gap opens, is covered in where to run a stream consumer.
The matched feed and the threshold in this post are free to read. Getting the alert the instant the gap opens, instead of on a two-minute-old snapshot, needs the real-time stream on Basic and up.
More from the blog
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.
A live feed of cross-venue price gaps, no server required
The REST snapshot runs about two minutes behind, long after a nine-second gap has already closed. Here's how to watch one open in real time from a single HTML file, no server involved.
Kalshi vs Polymarket, how they actually differ
One is a CFTC-regulated exchange, the other is wallet-native and on-chain. What separates them structurally, and what our own matched data shows about how their prices move against each other.