Real-time signals (SSE)
GET /api/v1/signals/stream is a Server-Sent Events
(SSE) stream that pushes new Oracle signals to your client
the moment they are generated — no polling required.
What signals are
Section titled “What signals are”Oracle signals are machine-generated trading signals derived from the platform’s
data: news sentiment, price movements, volume anomalies, technical indicators,
SEC filings, and prediction-market activity. Each signal carries a type, a target
entity (asset, market, or prediction market), a strength (0–100), and a direction
(from VERY_BULLISH to VERY_BEARISH).
The stream delivers signals as they are created. To browse historical signals or
fetch a single one with full metadata, use the REST endpoints under
/signals instead — see
concepts: signals.
Requirements
Section titled “Requirements”| Requirement | Detail |
|---|---|
| Authentication | Required — send Authorization: Bearer mtk_your_key_here. A key is mandatory on this endpoint (the demo key mtk_demo works). |
| Plan | Starter, Professional, Enterprise, or Custom (or the demo key). Free is blocked. |
| Concurrent connections | Capped per plan by maxWebSocketConnections: Free 0, Starter 1, Professional 5, Enterprise 50, Custom 100. Exceeding the cap returns a 429 error event. The field name is legacy; Client API streaming uses SSE. |
| Credits | The stream-open request costs 0 credits. The first prepaid billing period is charged before the open event is sent, then each additional elapsed period is charged at the start of that period. If billing cannot be confirmed, the stream does not open or continue. Default: 1 credit per 30 minutes. |
See Plans & credits for the full table.
Endpoint
Section titled “Endpoint”GET /api/v1/signals/streamAuthorization: Bearer mtk_your_key_hereAccept: text/event-streamQuery parameters
Section titled “Query parameters”Both filters are applied server-side, so you only receive (and only get billed network for) the signals you care about.
| Parameter | Type | Description |
|---|---|---|
entityType | string | Only stream signals for this entity type — ASSET, MARKET, or PREDICTION_MARKET. |
minStrength | number | Only stream signals with signalStrength at or above this value (0–100). |
Events
Section titled “Events”The stream emits four named event types. Every message is standard
text/event-stream: an event: line, a data: line (JSON), and optionally an
id: line.
Sent once, immediately after the stream is established, so clients know the subscription is live.
event: opendata: {"message":"Subscribed to Oracle signals","timestamp":"2026-06-05T12:00:00.000Z"}signal
Section titled “signal”A new Oracle signal. The SSE id: line is set to the signal’s id, so a
reconnecting client can send Last-Event-ID (see Reconnection).
id: 9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5fevent: signaldata: {"id":"9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f","signalType":"VOLUME","entityType":"ASSET","entityId":"3b2a1c4d-...","signalStrength":82,"signalDirection":"BULLISH","signalAt":"2026-06-05T12:00:03.000Z"}The data payload for a signal event contains exactly these fields:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Unique signal identifier. Also sent as the SSE id:. |
signalType | string | Signal type — e.g. NEWS, VOLUME, PRICE, TECHNICAL. |
entityType | string | Target entity type — ASSET, MARKET, PREDICTION_MARKET, etc. |
entityId | string (UUID) | null | Identifier of the target entity, or null. |
signalStrength | number | Strength from 0 to 100. |
signalDirection | string | One of VERY_BULLISH, BULLISH, SLIGHTLY_BULLISH, NEUTRAL, SLIGHTLY_BEARISH, BEARISH, VERY_BEARISH. |
signalAt | string (ISO 8601) | null | When the signal condition occurred. |
heartbeat
Section titled “heartbeat”Sent roughly every 25 seconds when no signals are flowing, to keep the connection (and any intermediary proxy) from idling out.
event: heartbeatdata: {"timestamp":"2026-06-05T12:00:25.000Z"}Sent when there’s an authentication, authorization, or connection-limit problem. The HTTP status is also set accordingly, and the stream then closes.
event: errordata: {"message":"Real-time signal streaming requires a Starter plan or higher. Upgrade your plan to access live signals.","code":"FORBIDDEN"}| HTTP status | code | Cause |
|---|---|---|
| 401 | UNAUTHORIZED | No API key provided. Streaming requires authentication. |
| 403 | FORBIDDEN | Plan lacks real-time SSE access (e.g. Free). Upgrade to Starter+. |
| 429 | RATE_LIMIT_EXCEEDED | Already at the plan’s maxWebSocketConnections cap. Close an open stream first. |
Consuming the stream
Section titled “Consuming the stream”Use --no-buffer (-N) so curl flushes each event as it arrives instead of
waiting for the whole response.
curl --no-buffer \ -H "Authorization: Bearer mtk_your_key_here" \ -H "Accept: text/event-stream" \ "https://api.quantconomy.com/api/v1/signals/stream?entityType=ASSET&minStrength=70"JavaScript
Section titled “JavaScript”const res = await fetch( 'https://api.quantconomy.com/api/v1/signals/stream?entityType=ASSET&minStrength=70', { headers: { Authorization: 'Bearer mtk_your_key_here', Accept: 'text/event-stream', }, });
if (!res.ok || !res.body) {throw new Error(`Stream failed: ${res.status}`);}
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();let buffer = '';
while (true) {const { value, done } = await reader.read();if (done) break;
buffer += value;
// SSE events are separated by a blank line.let boundary;while ((boundary = buffer.indexOf('\n\n')) !== -1) {const raw = buffer.slice(0, boundary);buffer = buffer.slice(boundary + 2);
let event = 'message'; const dataLines = []; for (const line of raw.split('\n')) { if (line.startsWith('event:')) event = line.slice(6).trim(); else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim()); // (id: lines are also present on `signal` events) }
if (dataLines.length === 0) continue; const data = JSON.parse(dataLines.join('\n'));
if (event === 'signal') { console.log('Signal:', data.signalType, data.signalDirection, data.signalStrength); } else if (event === 'error') { console.error('Stream error:', data.code, data.message); return; }
}}// Only works if your own backend (or edge) attaches the Bearer key for you.// EventSource cannot set Authorization headers directly.const source = new EventSource('/proxy/signals/stream');
source.addEventListener('open', (e) => { console.log('Subscribed:', JSON.parse(e.data));});
source.addEventListener('signal', (e) => { const signal = JSON.parse(e.data); console.log('Signal:', signal.signalType, signal.signalDirection, signal.signalStrength);});
source.addEventListener('heartbeat', () => { // keep-alive; no action needed});
source.addEventListener('error', (e) => { // EventSource auto-reconnects on transport errors; close on auth/plan errors. console.error('Stream error', e);});Python
Section titled “Python”import jsonimport httpx
url = "https://api.quantconomy.com/api/v1/signals/stream"headers = {"Authorization": "Bearer mtk_your_key_here","Accept": "text/event-stream",}params = {"entityType": "ASSET", "minStrength": 70}
with httpx.stream("GET", url, headers=headers, params=params, timeout=None) as res:res.raise_for_status()event = "message"data_lines = []for line in res.iter_lines():if line == "": # blank line terminates an eventif data_lines:data = json.loads("\n".join(data_lines))if event == "signal":print("Signal:", data["signalType"], data["signalDirection"], data["signalStrength"])elif event == "error":print("Stream error:", data["code"], data["message"])breakevent, data_lines = "message", []elif line.startswith("event:"):event = line[len("event:"):].strip()elif line.startswith("data:"):data_lines.append(line[len("data:"):].strip())import jsonimport requestsimport sseclient # pip install sseclient-py
url = "https://api.quantconomy.com/api/v1/signals/stream"headers = { "Authorization": "Bearer mtk_your_key_here", "Accept": "text/event-stream",}params = {"entityType": "ASSET", "minStrength": 70}
res = requests.get(url, headers=headers, params=params, stream=True)res.raise_for_status()client = sseclient.SSEClient(res)
for event in client.events(): data = json.loads(event.data) if event.event == "signal": print("Signal:", data["signalType"], data["signalDirection"], data["signalStrength"]) elif event.event == "error": print("Stream error:", data["code"], data["message"]) breakReconnection and backoff
Section titled “Reconnection and backoff”Long-lived streams will eventually drop — network blips, proxy timeouts, or a deploy. Build reconnection in.
-
Distinguish fatal from transient. An
errorevent withUNAUTHORIZED,FORBIDDEN, orRATE_LIMIT_EXCEEDEDis fatal: do not auto-reconnect, fix the key/plan or close another stream first. A dropped connection with no error event is transient — reconnect. -
Back off exponentially with jitter. On a transient drop, wait, then retry, doubling the delay each time up to a ceiling (e.g. 1s → 2s → 4s … capped at ~30s) and adding random jitter to avoid thundering-herd reconnects.
-
Watch the heartbeat. If you don’t see a
signalorheartbeatwithin ~60s (heartbeats arrive about every 25s), treat the connection as dead and reconnect. -
Resume with
Last-Event-ID. Eachsignalevent carries its id as the SSEid:. BrowserEventSourceresends the last id automatically as theLast-Event-IDheader on reconnect; withfetch/httpx, track the lastsignalid yourself and send it as theLast-Event-IDrequest header so you can de-duplicate. Treat the stream as at-least-once: dedupe by signalid. -
Respect the connection cap. Make sure the dropped stream is actually released before reconnecting — opening a new stream while the old slot is still held can trip the
maxWebSocketConnectionscap and return429. Reconnect one stream at a time per account.
Related
Section titled “Related”- Concepts: Signals — what Oracle signals are and how they’re generated.
- Signals API reference — list, get-by-id, and by-entry REST endpoints.
- Plans & credits — connection caps and credit costs.
- Authentication — creating and sending your
mtk_API key. - Errors — status codes and the error envelope.