Websocket
Trade ideas, market commentary, session signals, and the 0DTE read pushed as they happen
Instead of polling the REST API, your program can hold one websocket connection to Optionomics and be pushed each item the moment it is made: a trade idea as it opens and again as it resolves, each market commentary as it is published, and—on Developer Pro—every session signal and the minute-by-minute 0DTE read for SPY and SPX.
The websocket carries Optionomics’ own readings. It never carries raw exchange prints.
Availability: Developer includes the trade_ideas and market_commentaries topics. Developer Pro adds readings:signals and readings:zero_dte. See Subscription Plans.
Connecting
| Address | wss://optionomics.ai/socket/websocket |
| Protocol version | add vsn=2.0.0 to the query string |
| Authentication | your API key, sent as the token query parameter |
| Full URL | wss://optionomics.ai/socket/websocket?vsn=2.0.0&token=YOUR_API_KEY |
Use the same API key you use for the REST API and the MCP server. You can copy the address from the Where the key goes panel at optionomics.ai/developers/keys.
- A missing, expired, or revoked key is refused when the connection opens (the handshake fails with HTTP
403). - Connect from a program—a server, a script, a bot—not from a web page on another website. Browsers on other sites are refused.
- Keep the key out of source code: read it from an environment variable, as the examples below do.
Topics
After connecting, you join one or more topics. A topic your plan does not include refuses the join with the reason unauthorized; the connection itself stays open, and the other topics keep working.
| Topic | Plan | Event | When it arrives |
|---|---|---|---|
trade_ideas |
Developer and Developer Pro | trade_idea |
When a trade idea opens, and again when it resolves |
market_commentaries |
Developer and Developer Pro | commentary |
When a market commentary is published |
readings:signals |
Developer Pro | signal, signal_retired |
During the session (09:00–16:59 ET), as each session signal is published, restated, or retired |
readings:zero_dte |
Developer Pro | zero_dte |
Every minute while the regular session is open |
trade_ideas
Each message is event trade_idea, with the same item the trade ideas REST endpoint returns: the idea’s id, symbol, strategy, levels, status, track record, timestamps, and so on. An opening and a resolution use the same event and the same shape—read the idea’s status to tell which one you received.
market_commentaries
Each message is event commentary:
| Field | Meaning |
|---|---|
id |
The commentary’s id |
category |
The commentary category |
severity |
info, notable, or critical |
headline |
The headline |
body |
The full text |
symbols |
The symbols it mentions (a list, possibly empty) |
generated_at |
When it was written, ISO 8601 |
return_pct, peak_return_pct |
Follow-up performance figures, when there are any |
readings:signals (Developer Pro)
Session signals are standing statements about a stock or the market—such as a stock touching the edge of its priced move, its priced move repricing far from its norm, or implied volatility’s premium reaching an extreme—each backed by a measurement. They are the same signals the starter alerts fire on. Like every Optionomics reading they are about size, odds, and cost, never direction.
Event signal carries:
| Field | Meaning |
|---|---|
id |
The signal’s id—stable for the life of the signal |
kind |
What kind of signal it is, for example expected_move_touch or expected_move_repricing |
lane |
Its family: vol, event, flow, execution, or regime |
horizon |
What it bears on: right now, the rest of the session, the next few weeks, or a dated event such as a report (for example intraday) |
evidence_class |
How it is backed, for example measured |
symbol |
The stock it is about, or none for a market-wide signal |
score |
Its strength, 0 to 100 |
half_life_s |
How fast its strength fades, in seconds |
facts |
The numbers behind the statement; the contents depend on the kind |
revision |
Starts at 1 |
session_date |
The session it belongs to |
observed_at, emitted_at, expires_at |
When it was observed, published, and when it expires |
When a signal’s facts change, the same signal is sent again with the same id and a higher revision—replace your copy rather than adding a new one. When a signal expires, event signal_retired arrives with only its id.
readings:zero_dte (Developer Pro)
Event zero_dte arrives every minute of the regular session, once for SPY and once for SPX, in the same shape the zero_dte_read MCP tool returns: product, the time of the read (at), minutes_left to the close, the options-implied forward, the at-the-money straddle and straddle_pct, the cone—the calibrated 50%, 68%, and 90% ranges to the close (each with low, high, and move_pct)—and a strikes list giving each strike’s side, distance in sigmas, bid, ask, the odds of closing beyond it (close_beyond), and the odds of touching it before the close (touch).
The frame format
The server speaks a small JSON protocol that any language can implement. Every frame, in both directions, is a JSON array of five elements:
[join_ref, ref, topic, event, payload]
| Position | Name | What it is |
|---|---|---|
| 1 | join_ref |
A string you choose when you join a topic, repeated on that topic’s frames. null for heartbeats and for server pushes. |
| 2 | ref |
A string you choose for each message you send, unique on the connection (a counter works). The server’s reply carries the same ref. Server pushes have null. |
| 3 | topic |
The topic, for example trade_ideas. Heartbeats use the topic phoenix. |
| 4 | event |
What the frame is: phx_join to join, heartbeat, phx_reply for the server’s answer, or a data event such as trade_idea or signal. |
| 5 | payload |
A JSON object: {} for a join or heartbeat, the data for a push. |
The rules:
- Join a topic by sending
[join_ref, ref, topic, "phx_join", {}]. - The server answers with event
phx_replyand a payload of{"status": "ok", "response": {}}, or{"status": "error", "response": {"reason": "unauthorized"}}when your plan does not include the topic. - Data arrives as
[null, null, topic, event, payload]. - Heartbeat every 30 seconds by sending
[null, ref, "phoenix", "heartbeat", {}]. If the server hears nothing from you for 60 seconds, it closes the connection. - If the connection drops, reconnect and join your topics again. Items published while you were disconnected are not replayed—use the REST API to catch up.
Node.js example
Node.js 22 and later have WebSocket built in, so this needs no packages:
// Node.js 22 or later: WebSocket is built in, no packages needed.
const url =
"wss://optionomics.ai/socket/websocket?vsn=2.0.0&token=" + process.env.OPTIONOMICS_API_KEY
const ws = new WebSocket(url)
let ref = 0
const send = (topic, event, payload = {}, joinRef = null) =>
ws.send(JSON.stringify([joinRef, String(++ref), topic, event, payload]))
ws.onopen = () => {
for (const topic of ["trade_ideas", "market_commentaries"]) send(topic, "phx_join", {}, topic)
setInterval(() => send("phoenix", "heartbeat"), 30_000)
}
ws.onmessage = ({ data }) => {
const [, , topic, event, payload] = JSON.parse(data)
if (event === "phx_reply") {
if (payload.status !== "ok") console.error("join refused:", topic, payload.response)
return
}
console.log(topic, event, payload)
}
Run it with OPTIONOMICS_API_KEY=your-key node stream.js. On Developer Pro, add "readings:signals" and "readings:zero_dte" to the list of topics.
Python example
This uses the websockets package (pip install websockets):
import asyncio
import itertools
import json
import os
from urllib.parse import urlencode
import websockets
URL = "wss://optionomics.ai/socket/websocket?" + urlencode(
{"vsn": "2.0.0", "token": os.environ["OPTIONOMICS_API_KEY"]}
)
# On Developer Pro, add "readings:signals" and "readings:zero_dte".
TOPICS = ["trade_ideas", "market_commentaries"]
async def main():
refs = itertools.count(1)
async with websockets.connect(URL) as ws:
async def send(topic, event, payload=None, join_ref=None):
frame = [join_ref, str(next(refs)), topic, event, payload or {}]
await ws.send(json.dumps(frame))
async def heartbeat():
while True:
await asyncio.sleep(30)
await send("phoenix", "heartbeat")
for topic in TOPICS:
await send(topic, "phx_join", join_ref=topic)
beating = asyncio.create_task(heartbeat())
try:
async for raw in ws:
_join_ref, _ref, topic, event, payload = json.loads(raw)
if event == "phx_reply":
if payload["status"] != "ok":
print("join refused:", topic, payload["response"])
continue
print(topic, event, payload)
finally:
beating.cancel()
asyncio.run(main())
Run it with OPTIONOMICS_API_KEY=your-key python stream.py.
Tips
- Signals are updated in place. Keep signals in a dictionary keyed by
id; overwrite on eachsignaland delete onsignal_retired. - The 0DTE read is a snapshot. Each
zero_dtemessage replaces the previous one for that product. - Plan changes take effect on the next connection. If your plan changes, reconnect so the server checks your topics again.
- Requests made over the websocket do not count against your REST/MCP per-minute allowance; only the REST API and MCP server are rate limited.