Stream#
Beta. The stream may still change, and may go quiet while it's worked on.
One WebSocket pushes every top-of-book change for game winners in the NBA, NFL, NHL and NCAAF, on Kalshi and Polymarket US, with the spread after both venues' taker fees each way round. A page that refreshes every few seconds misses most cross-venue spreads; the stream sends each change as Layer receives it.
wss://stream.uselayer.sh/v0/streamSend your Layer API key as Authorization: Bearer <key> (or x-api-key). A browser can't set headers, so it can send ?api_key=<key> instead. Each key can hold 5 connections at once.
Subscribe#
Choose games when you connect, or later with a message:
| Connect to | You get |
|---|---|
/v0/stream |
All four leagues |
/v0/stream?league=nba,nhl |
Those leagues (nba, nfl, nhl, ncaaf) |
/v0/stream?event_id=<event_id> |
One game (both teams' markets); comma-separate several |
/v0/stream?coin=btc,eth,sol |
15-minute crypto windows for those coins (see below) |
{"type": "subscribe", "league": "nfl"}
{"type": "subscribe", "event_id": "KXNBAGAME-26NOV10MILNYK"}
{"type": "subscribe", "coin": "btc"}
{"type": "unsubscribe", "league": "nhl"}The games are those Layer has matched as identical on both venues, live or starting within 24 hours. A game appears when it's matched and leaves when it settles; you get a new markets message each time your list changes.
Crypto windows#
Ask for a coin (btc, eth or sol) to get its 15-minute up-or-down window on Kalshi and Polymarket US. You get crypto only when you ask for a coin; /v0/stream with no filter stays sports only, and ?league=nba&coin=btc gets both.
Each window is one market: YES means the price is up at the end of the window, on both venues. Kalshi opens a window only when it starts, so a window joins the stream about 20 seconds after it opens and leaves at its end. You get a markets message each time one window gives way to the next.
A crypto book message has coin, window_start and window_end in place of league. Its event_id is Kalshi's event ticker for the window (KXBTC15M-26NOV121245), and polymarket_us.slug is the window's market (cpc-btc-updown-15m-2026-11-12-1730z).
Polymarket US and Kalshi only: the stream doesn't carry polymarket.com.
Messages#
Every message is one JSON object with a type.
markets: the markets your subscription covers, sent when you connect, after each subscribe or unsubscribe, and when a game is added or ends. A game is two markets, one per team.
book: one market's best prices on both venues. You get one for each market when you subscribe, then one each time either venue's best bid or ask changes, in price or size.
{
"type": "book",
"market_id": "lyr_fd3b51298cd3",
"event_id": "lyr_evt_54357dd135a3",
"league": "ncaaf",
"question": "Ohio vs Miami (OH): Miami (OH) wins",
"kalshi": { "ticker": "KXNCAAFGAME-26NOV10OHIOMOH-MOH", "yes_bid": 0.49, "yes_ask": 0.5, "bid_size": 45302.09, "ask_size": 217132.88, "received_at": "2026-11-10T17:30:20.325Z" },
"polymarket_us": { "slug": "aec-cfb-ohio-miaoh-2026-11-10:short", "yes_bid": 0.49, "yes_ask": 0.495, "bid_size": 365457.38, "ask_size": 305316.01, "received_at": "2026-11-10T17:30:10.982Z" },
"edge_cents": { "kalshi_yes_pmus_no": -5.12, "pmus_yes_kalshi_no": -4.62 },
"stale_cents": { "kalshi": 0.25, "polymarket_us": -0.25 }
}| Field | Meaning |
|---|---|
market_id |
The Layer market for this team's win, as in Markets. null for a game Layer hasn't made a market for yet. |
event_id |
The game: its Layer event id, or Kalshi's event ticker until Layer has one. Both teams' markets share it. |
league |
nba, nfl, nhl or ncaaf. |
question |
What YES pays on, on both venues: this team wins. |
kalshi.ticker |
Kalshi's market for this team. |
polymarket_us.slug |
Polymarket US's market and side. Polymarket US lists one market per game: :long is its YES, :short its NO, so YES here is always this team winning. |
yes_bid, yes_ask |
The venue's best bid and ask for YES, in dollars. null when that side is empty. Kalshi's YES ask is 1 minus its best NO bid. |
bid_size, ask_size |
Contracts at those prices. |
received_at |
When Layer received that venue's latest change to this book (UTC). The venue's own message time isn't sent. |
edge_cents.kalshi_yes_pmus_no |
Profit per contract, in cents, from buying YES on Kalshi and NO on Polymarket US at the best asks, after both venues' taker fees. Positive means both legs together cost less than the $1 they pay. null when a side has no asks. |
edge_cents.pmus_yes_kalshi_no |
The same the other way round. |
stale_cents |
How far each venue's mid (halfway between bid and ask) is from the other's, in cents. |
The edge is at the best prices only, for one contract. For the size you can trade at a positive edge, send both books to POST /v0/size, or read GET /v0/quote.
Venue prices and sizes (yes_bid, yes_ask, bid_size, ask_size) come with keys that have live price access. Other keys get every other field.
heartbeat: every 5 seconds, with each venue's feed up or down. If none arrives for 15 seconds, reconnect.
{"type": "heartbeat", "at": "2026-11-10T22:00:05.000Z", "venues": {"kalshi": "up", "polymarket_us": "up"}}gap: a venue's feed dropped, so no message for that venue's books doesn't mean they didn't change. since is the last message Layer received from it.
resumed: the venue is back. Each of its markets gets a fresh book message.
{"type": "gap", "venue": "kalshi", "since": "2026-11-10T22:00:00.000Z"}
{"type": "resumed", "venue": "kalshi", "since": "2026-11-10T22:00:00.000Z", "until": "2026-11-10T22:00:03.000Z"}error: a message Layer couldn't use, with error and detail. The connection stays open.
A connection is refused before it opens with 401 (no key or an unknown key), 400 (an unknown league), or 429 (more than 5 connections for the key). A client that reads too slowly for messages to keep up is disconnected.
Example#
import asyncio, json, os
import websockets # pip install websockets
async def main():
url = "wss://stream.uselayer.sh/v0/stream?league=nba"
headers = {"Authorization": f"Bearer {os.environ['LAYER_API_KEY']}"}
async with websockets.connect(url, additional_headers=headers) as ws:
async for raw in ws:
m = json.loads(raw)
if m["type"] == "book":
best = max(m["edge_cents"].values(), key=lambda e: -1e9 if e is None else e)
print(m["question"], best)
elif m["type"] in ("gap", "resumed"):
print(m)
asyncio.run(main())To trade an edge, your code still places both orders itself, with your own venue keys. Books can change between the message and your orders.