Skip to content

Commit 9db4e50

Browse files
Mlaz-codeclaude
andcommitted
docs: fix field names to match actual snake_case API responses
All JSON examples and code samples across WebSocket, SSE, and example docs were using camelCase (eventId, marketType, evPercent, profitPercent) or raw Redis field names (hash_id, normalized_market, game_id) instead of the actual public API snake_case format from transforms.ts. Updated: websocket.mdx, stream.mdx, value-betting.mdx, arbitrage-scanner.mdx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 71d9219 commit 9db4e50

6 files changed

Lines changed: 526 additions & 134 deletions

File tree

content/api-reference/stream.mdx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ A new positive expected value opportunity has been found. Only sent on `opportun
107107
```
108108
event: ev:detected
109109
id: evt_00043
110-
data: {"opportunity_id":"opp_x1y2z3","event_id":"evt_abc123","sportsbook":"draftkings","selection":"PHO Suns -3.5","odds":{"american":-105,"decimal":1.952,"probability":0.512},"sharp_odds":{"american":-115,"decimal":1.870,"probability":0.535},"ev_percent":4.35,"kelly_percent":2.18,"timestamp":"2026-01-26T02:10:38.500Z"}
110+
data: [{"id":"a1b2c3d4e5f6","game_id":"nba_phosuns_phi76ers_2026-02-08","ev_percent":4.35,"odds_american":-105,"odds_decimal":1.952,"no_vig_odds":-101,"selection":"PHO Suns -3.5","market":"point_spread","line":-3.5,"sportsbook":"draftkings","game":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","home_team":"PHI 76ers","away_team":"PHO Suns","start_time":"2026-02-08T19:00:00.000Z","is_live":false,"confidence_score":72,"kelly_fraction":0.038,"book_count":4,"detected_at":"2026-02-08T18:47:20.000Z"}]
111111
```
112112

113113
### `ev:expired`
@@ -117,7 +117,7 @@ A previously detected +EV opportunity is no longer available.
117117
```
118118
event: ev:expired
119119
id: evt_00044
120-
data: {"opportunity_id":"opp_x1y2z3","reason":"odds_moved","timestamp":"2026-01-26T02:10:39.100Z"}
120+
data: {"expired":["a1b2c3d4e5f6"],"timestamp":"2026-02-08T18:47:25.000Z"}
121121
```
122122

123123
### `arb:detected`
@@ -127,7 +127,7 @@ A new arbitrage opportunity has been found. Only sent on `opportunities` or `all
127127
```
128128
event: arb:detected
129129
id: evt_00045
130-
data: {"opportunity_id":"opp_a1b2c3","event_id":"evt_abc123","legs":[{"sportsbook":"draftkings","selection":"PHO Suns ML","odds":{"american":+150,"decimal":2.50,"probability":0.40}},{"sportsbook":"fanduel","selection":"PHI 76ers ML","odds":{"american":-130,"decimal":1.769,"probability":0.565}}],"profit_percent":2.8,"stakes":{"leg_1":0.414,"leg_2":0.586},"timestamp":"2026-01-26T02:10:38.700Z"}
130+
data: [{"id":"61c501b83ce932d1","event_id":"nba_phosuns_phi76ers_2026-02-08","event_name":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","market_type":"moneyline","line":null,"profit_percent":2.8,"implied_total":97.2,"is_live":false,"legs":[{"sportsbook":"draftkings","selection":"PHO Suns","odds_american":150,"odds_decimal":2.5,"implied_probability":0.4,"stake_percent":41.4},{"sportsbook":"fanduel","selection":"PHI 76ers","odds_american":-130,"odds_decimal":1.769,"implied_probability":0.5652,"stake_percent":58.6}],"detected_at":"2026-02-08T18:47:21.000Z"}]
131131
```
132132

133133
### `arb:expired`
@@ -147,7 +147,7 @@ A new middle opportunity has been found. Only sent on `opportunities` or `all` c
147147
```
148148
event: middles:detected
149149
id: evt_00047
150-
data: [{"hash_id":"middle_abc123","game_id":"evt_abc123","eventName":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","marketType":"player_points","selection1":{"book":"draftkings","selection":"Over 22.5","odds":-110},"selection2":{"book":"fanduel","selection":"Under 23.5","odds":-105},"middleProbability":0.12,"expectedValue":3.5,"qualityScore":85}]
150+
data: [{"id":"middle_abc123","event_id":"nba_phosuns_phi76ers_2026-02-08","event_name":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","market_type":"player_points","side1":{"book":"draftkings","selection":"Over 22.5","line":22.5,"odds":{"american":-110,"decimal":1.909,"probability":0.5238,"fair_probability":0.51},"stake_percent":50,"odds_age_seconds":2.1,"deep_link":null},"side2":{"book":"fanduel","selection":"Under 23.5","line":23.5,"odds":{"american":-105,"decimal":1.952,"probability":0.5122,"fair_probability":0.49},"stake_percent":50,"odds_age_seconds":1.5,"deep_link":null},"middle_size":1,"middle_numbers":[23],"middle_probability":0.12,"expected_value":3.5,"quality_score":85,"detected_at":"2026-02-08T18:47:22.000Z"}]
151151
```
152152

153153
### `middles:expired`
@@ -167,7 +167,7 @@ A new low-hold opportunity has been found. Only sent on `opportunities` or `all`
167167
```
168168
event: low_hold:detected
169169
id: evt_00049
170-
data: [{"hash_id":"lowhold_abc123","hold_id":"32825:moneyline:0","hold_percentage":1.2,"normalized_market":"moneyline","game_id":"evt_abc123","sport":"basketball","league":"nba","home_team":"PHI 76ers","away_team":"PHO Suns","is_live":false,"all_books":["draftkings","fanduel"],"side1":{"selection":"PHO Suns","price":-108,"books":["draftkings"]},"side2":{"selection":"PHI 76ers","price":110,"books":["fanduel"]}}]
170+
data: [{"id":"lowhold_abc123","event_id":"nba_phosuns_phi76ers_2026-02-08","event_name":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","market_type":"moneyline","line":null,"home_team":"PHI 76ers","away_team":"PHO Suns","start_time":"2026-02-08T19:00:00.000Z","hold_percentage":1.2,"is_live":false,"all_books":["draftkings","fanduel"],"side1":{"selection":"PHO Suns","books":["draftkings"],"line":null,"odds":{"american":-108,"decimal":1.926,"implied_probability":0.5192,"fair_probability":0.5096},"deep_links":{"draftkings":"https://sportsbook.draftkings.com/event/..."}},"side2":{"selection":"PHI 76ers","books":["fanduel"],"line":null,"odds":{"american":110,"decimal":2.1,"implied_probability":0.4762,"fair_probability":0.4904},"deep_links":{"fanduel":"https://sportsbook.fanduel.com/event/..."}},"detected_at":"2026-02-08T18:47:22.000Z"}]
171171
```
172172

173173
### `low_hold:expired`
@@ -210,7 +210,7 @@ data: {"code":"upstream_error","message":"Temporary issue fetching DraftKings da
210210

211211
## Reconnection
212212

213-
SSE supports automatic reconnection via the `Last-Event-ID` header. Each event includes an `id` field. When the client reconnects, it sends the last received `id` and the server replays any missed events.
213+
SSE supports automatic reconnection via the `Last-Event-ID` header. Each event includes an `id` field. When the client reconnects, the server delivers a **fresh full snapshot** — not a replay of individual missed events. This means your client receives a complete, up-to-date picture on every reconnect.
214214

215215
```javascript
216216
// Browsers handle this automatically with EventSource.
@@ -221,8 +221,12 @@ const headers = {
221221
};
222222
```
223223

224+
<Callout type="warning">
225+
On reconnect, **clear your local state** before processing the new snapshot. The `connected` event includes `"reconnected": true` so you can detect this. If you don't clear state, stale odds from the previous session will mix with fresh data.
226+
</Callout>
227+
224228
<Callout type="info">
225-
Browser `EventSource` handles `Last-Event-ID` automatically. No extra code needed for reconnection in browsers.
229+
Browser `EventSource` handles `Last-Event-ID` and reconnection automatically. No extra code needed for the reconnect itself, but you must handle clearing state on the client side.
226230
</Callout>
227231

228232
## Code Examples
@@ -262,17 +266,17 @@ eventSource.addEventListener('odds:removed', (e) => {
262266

263267
eventSource.addEventListener('ev:detected', (e) => {
264268
const opps = JSON.parse(e.data);
265-
opps.forEach(opp => console.log(`+EV: ${opp.selection} at ${opp.ev_percentage}%`));
269+
opps.forEach(opp => console.log(`+EV: ${opp.selection} at ${opp.ev_percent}%`));
266270
});
267271

268272
eventSource.addEventListener('arb:detected', (e) => {
269273
const arbs = JSON.parse(e.data);
270-
arbs.forEach(arb => console.log(`Arb: ${arb.percentage}% profit`));
274+
arbs.forEach(arb => console.log(`Arb: ${arb.profit_percent}% profit`));
271275
});
272276

273277
eventSource.addEventListener('middles:detected', (e) => {
274278
const middles = JSON.parse(e.data);
275-
middles.forEach(m => console.log(`Middle: ${m.eventName} — EV ${m.expectedValue}%`));
279+
middles.forEach(m => console.log(`Middle: ${m.event_name} — EV ${m.expected_value}%`));
276280
});
277281

278282
eventSource.addEventListener('low_hold:detected', (e) => {
@@ -327,7 +331,7 @@ es.addEventListener('odds:removed', (e) => {
327331
es.addEventListener('ev:detected', (e) => {
328332
const opps = JSON.parse(e.data);
329333
opps.forEach(opp =>
330-
console.log(`+EV: ${opp.selection}${opp.ev_percentage}% EV`)
334+
console.log(`+EV: ${opp.selection}${opp.ev_percent}% EV`)
331335
);
332336
});
333337

@@ -339,7 +343,7 @@ es.addEventListener('ev:expired', (e) => {
339343
es.addEventListener('arb:detected', (e) => {
340344
const arbs = JSON.parse(e.data);
341345
arbs.forEach(arb =>
342-
console.log(`Arb: ${arb.percentage}% across ${arb.legs.length} legs`)
346+
console.log(`Arb: ${arb.profit_percent}% across ${arb.legs.length} legs`)
343347
);
344348
});
345349

@@ -391,21 +395,21 @@ for event in client.events():
391395

392396
elif event.event == 'ev:detected':
393397
for opp in data:
394-
print(f"+EV: {opp['selection']} at {opp['ev_percentage']}%")
398+
print(f"+EV: {opp['selection']} at {opp['ev_percent']}%")
395399

396400
elif event.event == 'ev:expired':
397401
print(f"EV expired: {len(data['expired'])} opportunities")
398402

399403
elif event.event == 'arb:detected':
400404
for arb in data:
401-
print(f"Arb: {arb['percentage']}% profit")
405+
print(f"Arb: {arb['profit_percent']}% profit")
402406

403407
elif event.event == 'arb:expired':
404408
print(f"Arb expired: {len(data['expired'])} opportunities")
405409

406410
elif event.event == 'middles:detected':
407411
for m in data:
408-
print(f"Middle: {m.get('eventName', '?')} — EV {m.get('expectedValue', '?')}%")
412+
print(f"Middle: {m.get('event_name', '?')} — EV {m.get('expected_value', '?')}%")
409413

410414
elif event.event == 'low_hold:detected':
411415
for h in data:

content/api-reference/websocket.mdx

Lines changed: 125 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,27 @@ Snapshot of opportunities for a single channel type. Sent once per subscribed op
184184
"seq": 3,
185185
"ev": [
186186
{
187-
"eventId": "32825-35775-2026-02-08",
188-
"eventName": "Indiana Pacers @ Toronto Raptors",
189-
"sport": "basketball",
190-
"marketType": "player_points",
191-
"sportsbook": "draftkings",
187+
"id": "a1b2c3d4e5f6",
188+
"game_id": "nba_indianapacers_torontoraptors_2026-02-08",
189+
"ev_percent": 4.35,
190+
"odds_american": -110,
191+
"odds_decimal": 1.909,
192+
"no_vig_odds": -101,
192193
"selection": "Tyrese Haliburton Over 22.5",
193-
"odds": -110,
194-
"evPercent": 4.35,
195-
"detectedAt": "2026-02-08T18:47:20.000Z"
194+
"market": "player_points",
195+
"line": 22.5,
196+
"sportsbook": "draftkings",
197+
"game": "Indiana Pacers @ Toronto Raptors",
198+
"sport": "basketball",
199+
"league": "nba",
200+
"home_team": "Toronto Raptors",
201+
"away_team": "Indiana Pacers",
202+
"start_time": "2026-02-08T19:00:00.000Z",
203+
"is_live": false,
204+
"confidence_score": 72,
205+
"kelly_fraction": 0.038,
206+
"book_count": 4,
207+
"detected_at": "2026-02-08T18:47:20.000Z"
196208
}
197209
],
198210
"timestamp": "2026-02-08T18:47:17.700Z"
@@ -201,6 +213,10 @@ Snapshot of opportunities for a single channel type. Sent once per subscribed op
201213

202214
The top-level key matches the channel type: `ev`, `arbitrage`, `middles`, or `low_hold`. Each snapshot message contains only one type. Large snapshots are automatically chunked — when this happens, messages include `chunk` and `totalChunks` fields.
203215

216+
<Callout type="info">
217+
All opportunity fields use **snake_case** naming (e.g. `event_id`, `market_type`, `profit_percent`, `detected_at`). This applies consistently across all channels, message types, and protocols (REST, SSE, and WebSocket).
218+
</Callout>
219+
204220
#### `initial`
205221

206222
Per-sportsbook odds snapshot. Sent once per sportsbook when the `odds` channel is subscribed. Requires the `odds` channel.
@@ -279,15 +295,27 @@ New +EV opportunity found. Pro tier or higher only.
279295
"seq": 48,
280296
"data": [
281297
{
282-
"eventId": "32825-35775-2026-02-08",
283-
"eventName": "Indiana Pacers @ Toronto Raptors",
284-
"sport": "basketball",
285-
"marketType": "player_points",
286-
"sportsbook": "draftkings",
298+
"id": "a1b2c3d4e5f6",
299+
"game_id": "nba_indianapacers_torontoraptors_2026-02-08",
300+
"ev_percent": 4.35,
301+
"odds_american": -110,
302+
"odds_decimal": 1.909,
303+
"no_vig_odds": -101,
287304
"selection": "Tyrese Haliburton Over 22.5",
288-
"odds": -110,
289-
"evPercent": 4.35,
290-
"detectedAt": "2026-02-08T18:47:20.000Z"
305+
"market": "player_points",
306+
"line": 22.5,
307+
"sportsbook": "draftkings",
308+
"game": "Indiana Pacers @ Toronto Raptors",
309+
"sport": "basketball",
310+
"league": "nba",
311+
"home_team": "Toronto Raptors",
312+
"away_team": "Indiana Pacers",
313+
"start_time": "2026-02-08T19:00:00.000Z",
314+
"is_live": false,
315+
"confidence_score": 72,
316+
"kelly_fraction": 0.038,
317+
"book_count": 4,
318+
"detected_at": "2026-02-08T18:47:20.000Z"
291319
}
292320
],
293321
"timestamp": "2026-02-08T18:47:20.000Z"
@@ -321,13 +349,35 @@ New arbitrage opportunity found. Pro tier or higher only.
321349
"seq": 50,
322350
"data": [
323351
{
324-
"eventId": "32825-35775-2026-02-08",
325-
"eventName": "Indiana Pacers @ Toronto Raptors",
352+
"id": "61c501b83ce932d1",
353+
"event_id": "nba_indianapacers_torontoraptors_2026-02-08",
354+
"event_name": "Indiana Pacers @ Toronto Raptors",
326355
"sport": "basketball",
327-
"marketType": "moneyline",
328-
"profitPercent": 2.8,
329-
"legs": [ /* ArbitrageLeg[] */ ],
330-
"detectedAt": "2026-02-08T18:47:21.000Z"
356+
"league": "nba",
357+
"market_type": "moneyline",
358+
"line": null,
359+
"profit_percent": 2.8,
360+
"implied_total": 97.2,
361+
"is_live": false,
362+
"legs": [
363+
{
364+
"sportsbook": "draftkings",
365+
"selection": "Indiana Pacers",
366+
"odds_american": 125,
367+
"odds_decimal": 2.25,
368+
"implied_probability": 0.4444,
369+
"stake_percent": 52.8
370+
},
371+
{
372+
"sportsbook": "fanduel",
373+
"selection": "Toronto Raptors",
374+
"odds_american": -110,
375+
"odds_decimal": 1.909,
376+
"implied_probability": 0.5238,
377+
"stake_percent": 47.2
378+
}
379+
],
380+
"detected_at": "2026-02-08T18:47:21.000Z"
331381
}
332382
],
333383
"timestamp": "2026-02-08T18:47:21.000Z"
@@ -362,17 +412,36 @@ New middle opportunity found. Requires `middles` channel.
362412
"data": [
363413
{
364414
"id": "abc123",
365-
"eventId": "32825-35775-2026-02-08",
366-
"eventName": "Indiana Pacers @ Toronto Raptors",
415+
"event_id": "nba_indianapacers_torontoraptors_2026-02-08",
416+
"event_name": "Indiana Pacers @ Toronto Raptors",
367417
"sport": "basketball",
368418
"league": "nba",
369-
"marketType": "player_points",
370-
"selection1": { "book": "draftkings", "selection": "Over 22.5", "odds": -110 },
371-
"selection2": { "book": "fanduel", "selection": "Under 23.5", "odds": -105 },
372-
"middleProbability": 0.12,
373-
"expectedValue": 3.5,
374-
"qualityScore": 85,
375-
"detectedAt": "2026-02-08T18:47:22.000Z"
419+
"market_type": "player_points",
420+
"side1": {
421+
"book": "draftkings",
422+
"selection": "Over 22.5",
423+
"line": 22.5,
424+
"odds": { "american": -110, "decimal": 1.909, "probability": 0.5238, "fair_probability": 0.51 },
425+
"stake_percent": 50,
426+
"odds_age_seconds": 3.2,
427+
"deep_link": null
428+
},
429+
"side2": {
430+
"book": "fanduel",
431+
"selection": "Under 23.5",
432+
"line": 23.5,
433+
"odds": { "american": -105, "decimal": 1.952, "probability": 0.5122, "fair_probability": 0.49 },
434+
"stake_percent": 50,
435+
"odds_age_seconds": 1.8,
436+
"deep_link": null
437+
},
438+
"middle_size": 1,
439+
"middle_numbers": [23],
440+
"middle_probability": 0.12,
441+
"expected_value": 3.5,
442+
"roi_percentage": 4.2,
443+
"quality_score": 85,
444+
"detected_at": "2026-02-08T18:47:22.000Z"
376445
}
377446
],
378447
"timestamp": "2026-02-08T18:47:22.000Z"
@@ -404,20 +473,34 @@ New low-hold opportunity found. Requires `low_hold` channel.
404473
"seq": 54,
405474
"data": [
406475
{
407-
"hash_id": "def456",
408-
"hold_id": "32825:moneyline:0",
409-
"hold_percentage": 1.2,
410-
"normalized_market": "moneyline",
411-
"game_id": "32825-35775-2026-02-08",
476+
"id": "def456",
477+
"event_id": "nba_indianapacers_torontoraptors_2026-02-08",
478+
"event_name": "Indiana Pacers @ Toronto Raptors",
412479
"sport": "basketball",
413480
"league": "nba",
481+
"market_type": "moneyline",
482+
"line": null,
414483
"home_team": "Toronto Raptors",
415484
"away_team": "Indiana Pacers",
485+
"start_time": "2026-02-08T19:00:00.000Z",
486+
"hold_percentage": 1.2,
416487
"is_live": false,
417488
"all_books": ["draftkings", "fanduel"],
418-
"side1": { "selection": "Indiana Pacers", "price": -108, "books": ["draftkings"] },
419-
"side2": { "selection": "Toronto Raptors", "price": 110, "books": ["fanduel"] },
420-
"timestamp": 1707414442
489+
"side1": {
490+
"selection": "Indiana Pacers",
491+
"books": ["draftkings"],
492+
"line": null,
493+
"odds": { "american": -108, "decimal": 1.926, "implied_probability": 0.5192, "fair_probability": 0.5096 },
494+
"deep_links": { "draftkings": "https://sportsbook.draftkings.com/event/..." }
495+
},
496+
"side2": {
497+
"selection": "Toronto Raptors",
498+
"books": ["fanduel"],
499+
"line": null,
500+
"odds": { "american": 110, "decimal": 2.1, "implied_probability": 0.4762, "fair_probability": 0.4904 },
501+
"deep_links": { "fanduel": "https://sportsbook.fanduel.com/event/..." }
502+
},
503+
"detected_at": "2026-02-08T18:47:22.000Z"
421504
}
422505
],
423506
"timestamp": "2026-02-08T18:47:22.000Z"
@@ -583,7 +666,7 @@ ws.onmessage = (event) => {
583666
break;
584667
case 'ev:detected':
585668
msg.data.forEach(ev =>
586-
console.log(`+EV: ${ev.selection} at ${ev.evPercent}%`)
669+
console.log(`+EV: ${ev.selection} at ${ev.ev_percent}%`)
587670
);
588671
break;
589672
case 'heartbeat':
@@ -680,7 +763,7 @@ def on_message(ws, raw):
680763
print("Initial data complete")
681764
elif msg_type == "middles:detected":
682765
for m in msg["data"]:
683-
print(f'Middle: {m["eventName"]} — EV {m.get("expectedValue", "?")}%')
766+
print(f'Middle: {m["event_name"]} — EV {m.get("expected_value", "?")}%')
684767
elif msg_type == "middles:expired":
685768
print(f'Expired: {len(msg["data"]["expired"])} middles')
686769
elif msg_type == "heartbeat":
@@ -737,7 +820,7 @@ stream.on('odds_update', ({ data, source }) => {
737820

738821
stream.on('ev:detected', ({ data }) => {
739822
data.forEach(ev =>
740-
console.log(`+EV: ${ev.selection} at ${ev.evPercent}%`)
823+
console.log(`+EV: ${ev.selection} at ${ev.ev_percent}%`)
741824
);
742825
});
743826

0 commit comments

Comments
 (0)