puntersedge 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -37
  3. package/dist/cjs/client.js +487 -0
  4. package/dist/cjs/client.js.map +1 -0
  5. package/dist/cjs/credits.js +82 -0
  6. package/dist/cjs/credits.js.map +1 -0
  7. package/dist/cjs/errors.js +130 -0
  8. package/dist/cjs/errors.js.map +1 -0
  9. package/dist/cjs/http.js +218 -0
  10. package/dist/cjs/http.js.map +1 -0
  11. package/dist/cjs/index.js +58 -0
  12. package/dist/cjs/index.js.map +1 -0
  13. package/dist/cjs/package.json +3 -0
  14. package/dist/cjs/types.generated.js +13 -0
  15. package/dist/cjs/types.generated.js.map +1 -0
  16. package/dist/cjs/webhook.js +95 -0
  17. package/dist/cjs/webhook.js.map +1 -0
  18. package/dist/esm/client.d.ts +480 -0
  19. package/dist/esm/client.d.ts.map +1 -0
  20. package/dist/esm/client.js +483 -0
  21. package/dist/esm/client.js.map +1 -0
  22. package/dist/esm/credits.d.ts +78 -0
  23. package/dist/esm/credits.d.ts.map +1 -0
  24. package/dist/esm/credits.js +78 -0
  25. package/dist/esm/credits.js.map +1 -0
  26. package/dist/esm/errors.d.ts +132 -0
  27. package/dist/esm/errors.d.ts.map +1 -0
  28. package/dist/esm/errors.js +117 -0
  29. package/dist/esm/errors.js.map +1 -0
  30. package/dist/esm/http.d.ts +52 -0
  31. package/dist/esm/http.d.ts.map +1 -0
  32. package/dist/esm/http.js +214 -0
  33. package/dist/esm/http.js.map +1 -0
  34. package/dist/esm/index.d.ts +22 -0
  35. package/dist/esm/index.d.ts.map +1 -0
  36. package/dist/esm/index.js +23 -0
  37. package/dist/esm/index.js.map +1 -0
  38. package/dist/esm/types.generated.d.ts +1453 -0
  39. package/dist/esm/types.generated.d.ts.map +1 -0
  40. package/dist/esm/types.generated.js +12 -0
  41. package/dist/esm/types.generated.js.map +1 -0
  42. package/dist/esm/webhook.d.ts +56 -0
  43. package/dist/esm/webhook.d.ts.map +1 -0
  44. package/dist/esm/webhook.js +92 -0
  45. package/dist/esm/webhook.js.map +1 -0
  46. package/package.json +52 -19
  47. package/src/client.ts +799 -0
  48. package/src/credits.ts +131 -0
  49. package/src/errors.ts +166 -0
  50. package/src/http.ts +268 -0
  51. package/src/index.ts +68 -206
  52. package/src/types.generated.ts +1514 -0
  53. package/src/webhook.ts +113 -0
  54. package/dist/index.d.ts +0 -117
  55. package/dist/index.js +0 -99
@@ -0,0 +1,1453 @@
1
+ export interface AcceptancesOut {
2
+ /** Meeting day, Australia/Sydney calendar date. */
3
+ date: string;
4
+ /**
5
+ * 'past', 'today' or 'future', relative to the Australia/Sydney date. On 'past' this card is a
6
+ * FROZEN SNAPSHOT: the scraper only fetches today-and-forward, so a meeting still showing
7
+ * acceptances_published false will never fill in and the scratching set can never converge. On
8
+ * 'today'/'future' the next scrape CAN add acceptances and scratchings, every two hours — but
9
+ * that is not a promise that a meeting converges, which is what this field used to make.
10
+ * Racing Australia removes a meeting from home.aspx once it is UNDERWAY and home.aspx is the
11
+ * scraper's only discovery surface, so a meeting's rows stop advancing around 12:15-14:15 AEST
12
+ * while its own races are still running to 17:00-19:30. Log-verified 2026-09-09: the 02:15Z
13
+ * pass discovered all six of that day's meetings, the 04:15Z pass discovered one, the 06:15Z
14
+ * pass discovered none. The in-band signal is per meeting, not per day — watch its own as_of
15
+ * and scratching_sheet_as_of: a stamp that stops moving while later passes land on other
16
+ * meetings is a meeting that has gone underway, not a quiet one. From that point race-day
17
+ * movement is on /v1/racing/changes, not here.
18
+ */
19
+ date_status: string;
20
+ meeting_count: number;
21
+ race_count: number;
22
+ /**
23
+ * Acceptance rows on this card — scratched runners and emergencies INCLUDED. A floor, not a
24
+ * guarantee, wherever a race reports runner_set_status other than 'checks_passed'.
25
+ */
26
+ runner_count: number;
27
+ /**
28
+ * Rows flagged as an emergency acceptance. NULL unless every meeting with rows on this card
29
+ * has emergency_data 'parsed' — see completeness. A FLOOR, not an exact figure, wherever a
30
+ * race reports runner_set_status other than 'checks_passed': it counts only the acceptance
31
+ * rows this card holds.
32
+ */
33
+ emergency_count?: number | null;
34
+ /**
35
+ * runner_count minus emergency_count: the DECLARED field, i.e. the acceptances that are not
36
+ * emergencies. NULL when emergency_count is. A FLOOR wherever a race reports runner_set_status
37
+ * other than 'checks_passed'.
38
+ */
39
+ declared_count?: number | null;
40
+ /**
41
+ * Sheet rows MATCHED to an acceptance row this card holds — NOT every scratching Racing
42
+ * Australia lists. A sheet row with no acceptance row here is not counted; it appears in that
43
+ * race's missing_runners with scratched true. Measured 2026-09-08: Townsville R6 has
44
+ * scratching_data 'parsed' and scratched_count 0 while the sheet holds #4 Five Star King and
45
+ * #9 Kickoff, so the true figure is at least 2. Read it as a FLOOR wherever a race reports
46
+ * runner_set_status other than 'checks_passed'; that race's runner_set_note states the bound.
47
+ * NULL unless every meeting with rows on this card has scratching_data 'parsed' — a meeting
48
+ * with no sheet cannot be said to have nobody scratched.
49
+ */
50
+ scratched_count?: number | null;
51
+ /**
52
+ * runner_count minus scratched_count. A RECONCILIATION number against the sheet, NOT a field
53
+ * size: it still includes emergencies, which run only if promoted. A FLOOR wherever a race
54
+ * reports runner_set_status other than 'checks_passed'. NULL when scratched_count is.
55
+ */
56
+ unscratched_count?: number | null;
57
+ /**
58
+ * Rows that are scratched AND not emergencies. Like scratched_count it counts only sheet rows
59
+ * MATCHED to an acceptance row held here, so it is a FLOOR wherever a race reports
60
+ * runner_set_status other than 'checks_passed'. NULL unless BOTH emergency_data and
61
+ * scratching_data are 'parsed' for every meeting with rows.
62
+ */
63
+ declared_scratched_count?: number | null;
64
+ /**
65
+ * declared_count minus declared_scratched_count: the horses expected to jump barring an
66
+ * emergency promotion, which Racing Australia does not publish. It is the number to render as
67
+ * the field ONLY where every race behind it reports runner_set_status 'checks_passed'.
68
+ * Anywhere else it is a FLOOR — render it as 'at least N', never as an exact field: a runner
69
+ * Racing Australia lists that this card does not hold, and that is not on the scratching
70
+ * sheet, may well be jumping. Measured 2026-09-08: Townsville R5 is 'gap_detected' with
71
+ * saddlecloth #6 unaccounted for and published 11. The race's runner_set_note states the
72
+ * bound. NULL unless BOTH facts are known everywhere.
73
+ */
74
+ declared_remaining_count?: number | null;
75
+ /**
76
+ * Newest RA scrape backing the meetings actually returned, UTC — computed after the venue
77
+ * filter.
78
+ */
79
+ as_of?: string | null;
80
+ /** What could and could not be verified about this card. */
81
+ completeness: Record<string, unknown>;
82
+ meetings: Record<string, unknown>[];
83
+ note: string;
84
+ }
85
+ export interface ArchiveCoverageOut {
86
+ archive_from: string | null;
87
+ archive_to: string | null;
88
+ rows: number;
89
+ races: number;
90
+ bookmakers: number;
91
+ is_closing_line_pct: number;
92
+ open_is_baseline_pct: number;
93
+ resulted_races_pct: number;
94
+ results_from: string | null;
95
+ settleable_races: number;
96
+ settleable_resulted_pct: number;
97
+ results_scope: string;
98
+ venue_split_suspect_rows: number;
99
+ name_fragment_suspect_rows: number;
100
+ by_category: Record<string, unknown>[];
101
+ }
102
+ export interface BackfillStatusOut {
103
+ job_id: string;
104
+ submitted_at?: string | null;
105
+ total: number;
106
+ done: number;
107
+ queued: number;
108
+ failed: number;
109
+ /** 3 per horse delivered so far. */
110
+ credits_charged: number;
111
+ /**
112
+ * True when your remaining credits are below the cost of one horse, which pauses collection
113
+ * until they reset or you upgrade.
114
+ */
115
+ credits_blocked: boolean;
116
+ /** Horses ahead of this job's oldest waiting row, across all jobs. */
117
+ queue_position?: number | null;
118
+ estimated_ready?: string | null;
119
+ /** Completed rows you have not yet read. */
120
+ ready_to_collect: number;
121
+ }
122
+ export interface BackfillSubmitIn {
123
+ /**
124
+ * Horse names or ra: refs. Max 500 per call. A ref is exact; a name is resolved against
125
+ * acceptances and archived results.
126
+ */
127
+ horses: string[];
128
+ /** Your own tag for this batch, echoed back on status. */
129
+ label?: string | null;
130
+ }
131
+ export interface BackfillSubmitOut {
132
+ job_id: string;
133
+ label?: string | null;
134
+ /** Horses accepted and waiting on the worker. */
135
+ queued: number;
136
+ /**
137
+ * Horses we already hold a fresh copy of. Collectable immediately, still billed at 3 credits
138
+ * each.
139
+ */
140
+ already_stored: number;
141
+ /** Already queued for you; not queued twice. */
142
+ duplicates: number;
143
+ /** Names we have never seen on our own feeds. */
144
+ not_found?: string[];
145
+ /** Names matching several horses. Re-submit with the runner_ref you meant. */
146
+ ambiguous?: Record<string, unknown>[];
147
+ /** UTC estimate for the last horse, from the worker's measured pace. */
148
+ estimated_ready?: string | null;
149
+ }
150
+ export interface BestOddsEventOut {
151
+ id: string;
152
+ sport_key: string;
153
+ home_team: string | null;
154
+ away_team: string | null;
155
+ commence_time: string;
156
+ selections: Record<string, unknown>[];
157
+ arb_exists: boolean;
158
+ arb_profit_pct: number;
159
+ data_age_seconds?: number | null;
160
+ freshest_age_seconds?: number | null;
161
+ stale?: boolean;
162
+ stale_bookmakers?: string[];
163
+ }
164
+ export interface BestPricesOut {
165
+ id: string;
166
+ sport_key: string;
167
+ home_team: string | null;
168
+ away_team: string | null;
169
+ commence_time: string;
170
+ selections: Record<string, unknown>[];
171
+ }
172
+ export interface ChangedRaceOut {
173
+ /**
174
+ * Stable race identifier — the same id /v1/racing/next-to-go, /v1/racing/price-history and
175
+ * /v1/racing/results use.
176
+ */
177
+ race_id: string;
178
+ venue?: string | null;
179
+ race_number?: number | null;
180
+ /** horse, greyhound or harness. */
181
+ category?: string | null;
182
+ /** ISO country code, or null while the meeting is unresolved. */
183
+ country?: string | null;
184
+ /**
185
+ * Advertised start, UTC. It is revised as the meeting firms, and a revision bumps this race
186
+ * into the feed.
187
+ */
188
+ start_time: string;
189
+ /**
190
+ * Set when the race row is created and revised exactly once, if at all: to 'abandoned' when
191
+ * Racing Australia posts an official abandoned, postponed or transferred notice for the
192
+ * meeting — that revision bumps every runner of the race into this feed (2026-09-06).
193
+ * Otherwise it reads 'open' for every race here. It is not a live market-state signal — do not
194
+ * poll this endpoint for it.
195
+ */
196
+ status?: string | null;
197
+ /**
198
+ * Betting-market state (OPEN, SUSPENDED, CLOSED). Reported by PointsBet, which sends CLOSED
199
+ * when it shuts its book at the jump, and by the Betfair Exchange, which is withheld from
200
+ * customer responses pending a data licence — so a value last written by the exchange reads
201
+ * null for customers. Same shape as /v1/racing/events.
202
+ */
203
+ market_status?: string | null;
204
+ /**
205
+ * True once the market is in-play. Exchange-only — bookmakers do not run racing in-play — so
206
+ * it carries the same licence hold as `market_status` and reads null for customers.
207
+ */
208
+ inplay?: boolean | null;
209
+ /**
210
+ * UTC time the market was FIRST seen shut. Poll-observed and set once — see /v1/racing/events
211
+ * for the full semantics. Served when a BOOKMAKER reported the transition; null for customers
212
+ * when the exchange was the only feed that saw it. Null never means the market stayed open.
213
+ */
214
+ market_closed_at?: string | null;
215
+ /**
216
+ * UTC time the race was FIRST seen in-play — the actual off, as opposed to `start_time`, which
217
+ * is advertised and moves. Exchange-only, so null for customer plans.
218
+ */
219
+ inplay_at?: string | null;
220
+ /**
221
+ * Which feed's market state produced `market_closed_at` / `inplay_at` — 'pointsbetau' or
222
+ * 'betfair_ex_au'. Set once, with the first stamp, and null whenever those stamps are
223
+ * withheld.
224
+ */
225
+ market_state_source?: string | null;
226
+ /**
227
+ * The COMPLETE current scratchings for this race, merged across every book that reports one —
228
+ * not a delta, so a runner leaving the list is visible as an absence. Greyhound cards also
229
+ * carry box-vacancy placeholders here (a literal '....' or 'Vacant Box'), which name an empty
230
+ * box rather than a withdrawn runner; match on `number` and treat a name from that set as a
231
+ * vacant box.
232
+ */
233
+ scratchings?: Record<string, unknown>[];
234
+ /**
235
+ * UTC instant of the most recent logged track-condition, weather or rail transition for this
236
+ * race's meeting (2026-09-05). Null when nothing has changed since we began watching it. Read
237
+ * it with `track_condition`: the value says WHAT, this says WHEN.
238
+ */
239
+ track_condition_changed_at?: string | null;
240
+ /**
241
+ * The race's CURRENT track condition, not a change log — a poller diffing against what it
242
+ * holds sees a condition change the same way it sees a scratching. Null where no source
243
+ * reports it.
244
+ */
245
+ track_condition?: string | null;
246
+ /** Current weather for the meeting, same read-as-current rule as track_condition. */
247
+ weather?: string | null;
248
+ /**
249
+ * Runner prices that moved inside the window. An empty list means this race arrived on the
250
+ * race-level arm alone — expected, and explained in the endpoint description.
251
+ */
252
+ changed_runners?: ChangedRunnerOut[];
253
+ }
254
+ export interface ChangedRunnerOut {
255
+ /** Runner name as the bookmaker publishes it. */
256
+ name: string;
257
+ /** Saddlecloth / rug number, where the book supplies one. */
258
+ number?: number | null;
259
+ /** Bookmaker this price belongs to. */
260
+ bookmaker_key: string;
261
+ /** Current fixed win price at this bookmaker. */
262
+ win_price?: number | null;
263
+ /**
264
+ * Current fixed place price. A place-only move does not advance `updated_at` — see the
265
+ * endpoint description.
266
+ */
267
+ place_price?: number | null;
268
+ /** Current Top 2 (first-two) price at this bookmaker, where quoted. */
269
+ top2_price?: number | null;
270
+ /** Current Top 3 price at this bookmaker, where quoted. */
271
+ top3_price?: number | null;
272
+ /** Current Top 4 price at this bookmaker, where quoted. */
273
+ top4_price?: number | null;
274
+ /**
275
+ * UTC instant this runner's win or lay price last CHANGED at this bookmaker. Not the last
276
+ * poll: ingest carries this timestamp forward while the price is steady.
277
+ */
278
+ updated_at: string;
279
+ }
280
+ export interface ClosingLinesOut {
281
+ rows_returned: number;
282
+ /** Rows matching the filter before limit/offset */
283
+ total_rows: number;
284
+ limit: number;
285
+ offset: number;
286
+ /**
287
+ * Earliest start_time in the whole archive. The archive only grows; this is its true floor as
288
+ * of right now, not a promise.
289
+ */
290
+ archive_from?: string | null;
291
+ /** Set when the caller's plan clamps the lookback. NULL means the full archive was searched. */
292
+ window_days?: number | null;
293
+ /**
294
+ * Rows in the matching set that have a result_status. Published because result coverage is
295
+ * thin and growing (5.3% of archived races on 2026-08-18) — do not assume a NULL
296
+ * finish_position means the runner lost. It means the runner is not in the published placings.
297
+ */
298
+ resulted_rows: number;
299
+ rows: Record<string, unknown>[];
300
+ }
301
+ export interface ConnectorHealthOut {
302
+ connector: string;
303
+ last_ok: string | null;
304
+ last_poll?: string | null;
305
+ status: string;
306
+ records_written: number;
307
+ message?: string | null;
308
+ }
309
+ export interface DogCandidate {
310
+ dog_id: number;
311
+ dog_name?: string | null;
312
+ /** Career starts on this feed, scratchings included. */
313
+ runs: number;
314
+ /** Meeting date of the most recent run. */
315
+ last_start?: string | null;
316
+ }
317
+ export interface GreyhoundFormOut {
318
+ dog_id?: number | null;
319
+ dog_name?: string | null;
320
+ /** True when the name matched several dogs. `runs` is then empty and `candidates` lists them. */
321
+ ambiguous: boolean;
322
+ candidates?: DogCandidate[];
323
+ runs?: GreyhoundRun[];
324
+ source: SourceBlock;
325
+ }
326
+ export interface GreyhoundRun {
327
+ /** Meeting date, AEST calendar day, YYYY-MM-DD. */
328
+ date?: string | null;
329
+ /** Official Topaz track name, sponsor included (e.g. 'Bet Deluxe Capalaba'). */
330
+ track?: string | null;
331
+ distance_m?: number | null;
332
+ /** Topaz raceTypeCode, e.g. '5', 'M', 'X45'. */
333
+ grade?: string | null;
334
+ race_number?: number | null;
335
+ box?: number | null;
336
+ rug?: number | null;
337
+ weight_kg?: number | null;
338
+ /** Official starting price, decimal. */
339
+ sp?: number | null;
340
+ /** Finishing place. NULL when the dog did not complete the course or was scratched. */
341
+ position?: number | null;
342
+ scratched: boolean;
343
+ /** Fell, TailedOff, PulledUp, Disqualified or StayedInBox. NULL for a normal run. */
344
+ abnormal?: string | null;
345
+ time_s?: number | null;
346
+ /** Winning time of this race, i.e. the place-1 dog's time. */
347
+ win_time_s?: number | null;
348
+ /** Margin to the winner in lengths, as Topaz publishes it (e.g. '5.50L'). */
349
+ margin?: string | null;
350
+ /**
351
+ * Margin to the winner in seconds. On the winner's own run this is the winning margin over the
352
+ * runner-up.
353
+ */
354
+ margin_s?: number | null;
355
+ first_split_time?: number | null;
356
+ first_split_position?: number | null;
357
+ /**
358
+ * Second sectional split. Only some tracks time two points: 21% of all runs carry one (603,734
359
+ * of 2,861,690, measured 2026-09-02).
360
+ */
361
+ second_split_time?: number | null;
362
+ second_split_position?: number | null;
363
+ /** The dog's grade entering this race, as Topaz graded it. */
364
+ grade_in?: string | null;
365
+ /**
366
+ * The dog's grade after this race's result was applied. A value differing from grade_in is a
367
+ * grade change — a win typically moves the dog down a number (5 -> 4).
368
+ */
369
+ grade_out?: string | null;
370
+ /** Position in running, one digit per section. */
371
+ pir?: string | null;
372
+ trainer: TrainerRef;
373
+ /** Topaz raceId. Every runner in the same race shares it. */
374
+ topaz_race_id?: number | null;
375
+ dog_id: number;
376
+ dog_name?: string | null;
377
+ }
378
+ export interface GreyhoundStatGroup {
379
+ track?: string | null;
380
+ distance_m?: number | null;
381
+ box?: number | null;
382
+ grade?: string | null;
383
+ starts: number;
384
+ wins: number;
385
+ /** Finishes in the first three. */
386
+ places: number;
387
+ win_pct: number;
388
+ place_pct: number;
389
+ avg_time_s?: number | null;
390
+ best_time_s?: number | null;
391
+ avg_first_split?: number | null;
392
+ }
393
+ export interface GreyhoundStatsOut {
394
+ dog_id?: number | null;
395
+ dog_name?: string | null;
396
+ ambiguous: boolean;
397
+ candidates?: DogCandidate[];
398
+ group_by?: string[];
399
+ groups?: GreyhoundStatGroup[];
400
+ source: SourceBlock;
401
+ }
402
+ export interface HorseCandidate {
403
+ /** Stable ra: horsecode — re-query with this. */
404
+ runner_ref: string;
405
+ runner_name?: string | null;
406
+ /** Most recent date this ref appeared on our feeds (acceptance or result). */
407
+ last_seen?: string | null;
408
+ /** Where it was seen: acceptances or results. */
409
+ source: string;
410
+ }
411
+ export interface HorseCareer {
412
+ starts?: number | null;
413
+ wins?: number | null;
414
+ seconds?: number | null;
415
+ thirds?: number | null;
416
+ /** Career prizemoney, whole AUD. */
417
+ prizemoney?: number | null;
418
+ /** Total bonus (e.g. BOBS), whole AUD. */
419
+ bonus?: number | null;
420
+ min_dist_win_m?: number | null;
421
+ max_dist_win_m?: number | null;
422
+ /**
423
+ * RA's own career splits, keyed first_up, second_up, firm, good, soft, heavy, synthetic — plus
424
+ * track, distance and track_distance when RA renders them. Each value is
425
+ * starts/wins/seconds/thirds.
426
+ */
427
+ splits?: Record<string, SplitRecord>;
428
+ }
429
+ export interface HorseFormOut {
430
+ horse_name?: string | null;
431
+ /**
432
+ * Stable ra: horsecode — the same identifier on /v1/racing/results runners and
433
+ * acceptance-enriched live runners.
434
+ */
435
+ runner_ref?: string | null;
436
+ /** True when the name matched several horses. `runs` is then empty and `candidates` lists them. */
437
+ ambiguous: boolean;
438
+ candidates?: HorseCandidate[];
439
+ profile?: HorseProfile | null;
440
+ career?: HorseCareer | null;
441
+ runs?: HorseRun[];
442
+ source?: HorseSource | null;
443
+ }
444
+ export interface HorseProfile {
445
+ /** As published, e.g. '4yo Bay Gelding'. */
446
+ age_sex?: string | null;
447
+ dob?: string | null;
448
+ sire?: string | null;
449
+ dam?: string | null;
450
+ status?: string | null;
451
+ trainer?: string | null;
452
+ trainer_location?: string | null;
453
+ owners?: string | null;
454
+ colours?: string | null;
455
+ /** As published, date and gear, e.g. '20-Aug-2026, Blinkers, First Time, ...'. */
456
+ last_gear_change?: string | null;
457
+ }
458
+ export interface HorseRun {
459
+ date?: string | null;
460
+ /** RA's compressed track code as published ('W FM' is Warwick Farm, 'RAND' Randwick). */
461
+ track?: string | null;
462
+ /** RA meeting code; every runner in the same meeting shares it. */
463
+ meet_code?: string | null;
464
+ race_number?: number | null;
465
+ /** True for a barrier trial: $0 prize, 0kg weights and no market are how RA publishes them. */
466
+ trial: boolean;
467
+ position?: number | null;
468
+ field_size?: number | null;
469
+ /** The position cell as published, kept for rows that don't parse to a number. */
470
+ result_raw?: string | null;
471
+ distance_m?: number | null;
472
+ /** Condition word: Firm, Good, Soft, Heavy, Synthetic. */
473
+ going?: string | null;
474
+ /** The number after the condition (Soft7 -> 7). Null where RA prints none. */
475
+ going_rating?: number | null;
476
+ /** RA's official class string for that race: 'MDN-SW', 'CTRY MDN', 'BM64', '3Y MDN-TRL'. */
477
+ race_class?: string | null;
478
+ prize_total?: number | null;
479
+ /** This horse's cut, when RA shows it. */
480
+ prize_won?: number | null;
481
+ jockey?: string | null;
482
+ /** RA's stable jockey code, consistent across every page that names the same rider. */
483
+ jockey_code?: string | null;
484
+ /**
485
+ * Trainer of the day for THIS run, filled from our own results archive — RA's form table never
486
+ * names the trainer per run. Populated for runs from 2026-09-01 onward (the archive's
487
+ * full-runner floor) and NULL before that; `profile.trainer` is the horse's current trainer,
488
+ * which is usually but not always the same person.
489
+ */
490
+ trainer?: string | null;
491
+ weight_kg?: number | null;
492
+ barrier?: number | null;
493
+ /** Official race time in seconds. */
494
+ time_s?: number | null;
495
+ /** The published closing sectional's distance, usually 600. */
496
+ sectional_distance_m?: number | null;
497
+ sectional_time_s?: number | null;
498
+ /** Margin in lengths. On this horse's own win it is the winning margin over the runner-up. */
499
+ margin_l?: number | null;
500
+ /** Position in running at the published marks (800m/400m). */
501
+ pir?: RunPIR[];
502
+ /** The published price path, opening quote through SP, oldest first. */
503
+ prices?: number[];
504
+ /** Starting price — the last entry of `prices`. */
505
+ sp?: number | null;
506
+ /**
507
+ * The two nearest finishers RA lists (2nd and 3rd on this horse's win; the winner and
508
+ * runner-up otherwise), each with its own ra: code.
509
+ */
510
+ placegetters?: RunPlacegetter[];
511
+ }
512
+ export interface HorseSource {
513
+ feed?: string;
514
+ region?: string;
515
+ sport?: string;
516
+ /** UTC time this horse's page was last read from RA. */
517
+ fetched_at?: string | null;
518
+ /**
519
+ * True when RA could not be reached just now and this is the stored copy served past its
520
+ * six-hour refresh window.
521
+ */
522
+ stale?: boolean;
523
+ }
524
+ export interface KeyResendIn {
525
+ email: string;
526
+ }
527
+ export interface LinesArbOut {
528
+ event_id: string;
529
+ sport_key: string;
530
+ home_team: string | null;
531
+ away_team: string | null;
532
+ commence_time: string;
533
+ market_type: string;
534
+ opportunities: Record<string, unknown>[];
535
+ }
536
+ export interface MoverOut {
537
+ venue: string | null;
538
+ race_number: number | null;
539
+ category: string | null;
540
+ country: string | null;
541
+ start_time: string;
542
+ mins_to_jump?: number | null;
543
+ runner: string;
544
+ number?: number | null;
545
+ direction: string;
546
+ open_price: number;
547
+ current_price: number;
548
+ move_pct: number;
549
+ /** Books with a price for this runner, including ones we have never observed move. */
550
+ books_quoting: number;
551
+ /**
552
+ * Books with two or more captured prices — the only ones that can evidence a move. The
553
+ * consensus and the `min_books` gate use THIS count, not `books_quoting`.
554
+ */
555
+ books_with_history: number;
556
+ books_firming: number;
557
+ books_drifting: number;
558
+ /**
559
+ * Books whose captured price has not moved, including books seen only once. books_firming +
560
+ * books_drifting + books_unchanged == books_quoting.
561
+ */
562
+ books_unchanged: number;
563
+ bookmakers: Record<string, unknown>[];
564
+ }
565
+ export interface NextRaceOut {
566
+ race_id: string;
567
+ source_id: string | null;
568
+ venue: string | null;
569
+ /**
570
+ * Stable slug for this venue spelling ('sandown-hillside'). Spelling variants of one venue
571
+ * share one id.
572
+ */
573
+ venue_id?: string | null;
574
+ /** Canonical display name for venue_id ('Sandown Hillside'). */
575
+ venue_canonical?: string | null;
576
+ /**
577
+ * Physical-complex key: tracks sharing one venue complex share one site ('sandown' covers
578
+ * Sandown, Sandown Hillside, Sandown Lakeside and the Sandown Park greyhound track).
579
+ */
580
+ venue_site?: string | null;
581
+ race_number: number | null;
582
+ category: string | null;
583
+ start_time: string;
584
+ country: string | null;
585
+ race_name?: string | null;
586
+ distance_m?: number | null;
587
+ track_condition?: string | null;
588
+ weather?: string | null;
589
+ places_paid?: number | null;
590
+ /**
591
+ * UTC instant of the most recent logged track-condition, weather or rail transition for this
592
+ * race's meeting — Racing Australia's re-published rating or a bookmaker's race page moving
593
+ * between two of our polls. Null when nothing has changed since we began watching the meeting.
594
+ * /v1/racing/track-conditions serves the sequence.
595
+ */
596
+ track_condition_changed_at?: string | null;
597
+ /**
598
+ * The first term of RA's official conditions line for this race ('BenchMark 64', 'Maiden',
599
+ * 'Group 1'). AU thoroughbred only; null elsewhere.
600
+ */
601
+ race_class?: string | null;
602
+ /**
603
+ * RA's official conditions line, verbatim — class, race type, minimum weight, age/sex
604
+ * restrictions, claiming allowances.
605
+ */
606
+ conditions?: string | null;
607
+ /** Total advertised prize for the race, whole AUD. */
608
+ prize_total?: number | null;
609
+ /** Rail position for the meeting, as RA publishes it ('+3m Entire'). AU thoroughbred only. */
610
+ rail?: string | null;
611
+ /**
612
+ * Racing Australia's own name for the course this race runs on, verbatim: 'Hillside' at
613
+ * Sandown, 'Randwick' vs the Kensington inner course, 'Main' elsewhere. Combine with
614
+ * venue_site to tell dual-course meetings apart. AU thoroughbred only; null elsewhere.
615
+ */
616
+ track_name?: string | null;
617
+ /**
618
+ * UTC time at which the market was FIRST seen shut — the book stopped taking bets on this
619
+ * race. Poll-observed: racing polls every 8s inside 120s of a jump, 15s inside 10 minutes and
620
+ * 20s otherwise, so this is the first poll that saw the state, not the exact instant; it lands
621
+ * within about one poll interval after it. Set once: a market that flickers SUSPENDED -> OPEN
622
+ * -> SUSPENDED keeps the FIRST suspension. TWO SOURCES report market state. PointsBet closes
623
+ * its book at the real jump — measured 2026-09-03 over 9 live races followed across their
624
+ * jump, 0.2 to 1.4 minutes AFTER the advertised start on AU/NZ cards — and the Betfair
625
+ * Exchange reports it while it quotes the market, which is until about two minutes before the
626
+ * jump. `market_state_source` names the one that produced this value. Exchange-sourced stamps
627
+ * are withheld from customer plans (operator ruling 2026-09-01): they republish licence-held
628
+ * market state, so a race seen shut only by the exchange reads null for customers and carries
629
+ * the value for internal callers. Bookmaker-sourced stamps are served to everybody. Null also
630
+ * means no feed observed a transition — it never means the market stayed open.
631
+ */
632
+ market_closed_at?: string | null;
633
+ /**
634
+ * UTC time at which the race was FIRST seen in-play — the actual off, as opposed to
635
+ * `start_time`, which is the ADVERTISED off and moves. Same poll-observed, set-once semantics
636
+ * as `market_closed_at`, and withheld on the same source rule. Reported by the Betfair
637
+ * Exchange only: bookmakers do not run racing in-play, so a bookmaker feed says when it CLOSED
638
+ * its book, not when the race went. For a race the exchange has stopped quoting,
639
+ * `market_closed_at` from a bookmaker is the closest measured instant to the off.
640
+ */
641
+ inplay_at?: string | null;
642
+ /**
643
+ * Which feed's market state produced `market_closed_at` / `inplay_at` on this race —
644
+ * 'pointsbetau' for the bookmaker, 'betfair_ex_au' for the exchange. Set once, alongside the
645
+ * first stamp. Null when no transition has been observed, and null for customer plans whenever
646
+ * the stamps themselves are withheld, so a non-null value here is always the provenance of
647
+ * values you can see.
648
+ */
649
+ market_state_source?: string | null;
650
+ scratchings?: Record<string, unknown>[];
651
+ runners: Record<string, unknown>[];
652
+ /**
653
+ * Age in seconds of the OLDEST bookmaker quote in this race — the staleness you are exposed to
654
+ * if you act on any price here. Since 2026-08-15 this is the worst leg, not the freshest;
655
+ * `freshest_age_seconds` carries the best-case figure.
656
+ */
657
+ data_age_seconds?: number | null;
658
+ /** Age in seconds of the freshest bookmaker quote in this race. */
659
+ freshest_age_seconds?: number | null;
660
+ /**
661
+ * Which refresh rule this race is on, and therefore what the race-level `stale` means. `live`
662
+ * = inside 3 hours of the jump: books re-price it on every 8-20s poll, and `stale` fires past
663
+ * 120 seconds. `card` = further out: books re-price it about every 15 minutes so a full day
664
+ * fits inside their rate limits, and `stale` fires past 1800 seconds. Every published quote
665
+ * carries ITS OWN `refresh_tier`, and it can be `card` on a `live` race: Betr's upstream
666
+ * rate-limits us, so that book stops re-pricing every poll 30 minutes before the jump rather
667
+ * than 3 hours (PlayUp, on a 6-requests-a-minute budget, at 6 minutes), and its quotes between
668
+ * there and 3 hours are refreshed every 5-15 minutes, published as `card`, and judged against
669
+ * 1800 seconds (since 2026-09-07; before that they were marked stale). A book's tier is never
670
+ * tighter than its race's. `age_seconds` is the TRUE age in every case — only the threshold
671
+ * changes.
672
+ */
673
+ refresh_tier?: string;
674
+ /**
675
+ * True when the oldest contributing bookmaker is older than this race's `refresh_tier`
676
+ * threshold — 120s on `live`, 1800s on `card`. It does NOT mean the whole race is dead — read
677
+ * `stale_bookmakers` and each bookmaker's own `stale`/`age_seconds`.
678
+ */
679
+ stale?: boolean;
680
+ /**
681
+ * Bookmaker keys in this race whose quote is older than the `refresh_tier` threshold (120s
682
+ * live, 1800s card).
683
+ */
684
+ stale_bookmakers?: string[];
685
+ cached?: boolean | null;
686
+ cache_age_seconds?: number | null;
687
+ }
688
+ export interface PremiershipOut {
689
+ role: string;
690
+ season_start_year?: number | null;
691
+ /** Echo of the filter; null when a name search crossed all states. */
692
+ state?: string | null;
693
+ scope?: string | null;
694
+ name?: string | null;
695
+ rows?: PremiershipRow[];
696
+ source: PremiershipSource;
697
+ }
698
+ export interface PremiershipRow {
699
+ /** AUS is the national table — RA's own combined leaderboard, not a sum of the state rows. */
700
+ state: string;
701
+ /** RA's split: all, metro, provincial, country or picnic. */
702
+ scope: string;
703
+ /** Position in RA's table, 1-20. */
704
+ rank: number;
705
+ name: string;
706
+ /**
707
+ * RA's stable code from the row's engagement link — the same namespace as trainer_code on
708
+ * acceptance data and jockey_code on /v1/racing/horses/form runs, so rows join on identity,
709
+ * not name spelling.
710
+ */
711
+ ra_code?: string | null;
712
+ /** Wins. REAL, not int: dead heats appear as halves in some tables (RA prints 23½). */
713
+ firsts?: number | null;
714
+ seconds?: number | null;
715
+ thirds?: number | null;
716
+ fourths?: number | null;
717
+ fifths?: number | null;
718
+ /** Season prize money, AUD. */
719
+ prize_money?: number | null;
720
+ /** RA's own figure, percent. */
721
+ strike_rate?: number | null;
722
+ starts?: number | null;
723
+ /** Start of the window this row measures (RA seasons run 1 Aug to 31 Jul). */
724
+ period_from?: string | null;
725
+ /**
726
+ * End of the measured window. On the running season this is the day RA rendered the table,
727
+ * which makes it the per-row freshness stamp: rows from a table scraped two nights ago carry
728
+ * that day, not today.
729
+ */
730
+ period_to?: string | null;
731
+ }
732
+ export interface PremiershipSource {
733
+ feed?: string;
734
+ region?: string;
735
+ sport?: string;
736
+ /**
737
+ * UTC time of the newest row served in this response. For the usual single-table lookup (a
738
+ * `state` + `scope`) every row comes from one scrape, so this IS that table's age. A `name`
739
+ * search spans tables scraped on different nights, so there read it as the freshest of them
740
+ * and use `period_to` for the age of each individual row.
741
+ */
742
+ scraped_at?: string | null;
743
+ /**
744
+ * RA publishes each premiership as a top-20 table. Absence from `rows` means outside the top
745
+ * 20, never inactivity.
746
+ */
747
+ top_n?: number;
748
+ }
749
+ export interface PriceHistoryOut {
750
+ race_id: string;
751
+ venue: string | null;
752
+ venue_id?: string | null;
753
+ venue_canonical?: string | null;
754
+ venue_site?: string | null;
755
+ race_number: number | null;
756
+ category: string | null;
757
+ country: string | null;
758
+ start_time: string;
759
+ points_returned: number;
760
+ truncated: boolean;
761
+ /**
762
+ * Start time of the earliest race this endpoint can answer for. Since 2026-09-01 races older
763
+ * than the ~45-day live window are served from the permanent archive, so this floor is fixed
764
+ * at capture start (2026-08-04) and history only deepens.
765
+ */
766
+ history_from?: string | null;
767
+ runners: Record<string, unknown>[];
768
+ }
769
+ export interface PricePathsOut {
770
+ rows_returned: number;
771
+ /** Points matching the filter before limit/offset */
772
+ total_points: number;
773
+ limit: number;
774
+ offset: number;
775
+ archive_from?: string | null;
776
+ rows: Record<string, unknown>[];
777
+ }
778
+ export interface RaceEventOut {
779
+ race_id: string;
780
+ venue: string | null;
781
+ venue_id?: string | null;
782
+ venue_canonical?: string | null;
783
+ venue_site?: string | null;
784
+ race_number: number | null;
785
+ category: string | null;
786
+ start_time: string;
787
+ country: string | null;
788
+ race_name?: string | null;
789
+ distance_m?: number | null;
790
+ track_condition?: string | null;
791
+ weather?: string | null;
792
+ /**
793
+ * The first term of RA's official conditions line ('BenchMark 64', 'Maiden', 'Group 1'). AU
794
+ * thoroughbred only.
795
+ */
796
+ race_class?: string | null;
797
+ /** RA's official conditions line, verbatim. */
798
+ conditions?: string | null;
799
+ /** Total advertised prize, whole AUD. */
800
+ prize_total?: number | null;
801
+ /** Rail position for the meeting, as RA publishes it. */
802
+ rail?: string | null;
803
+ /**
804
+ * RA's own name for the course this race runs on ('Hillside', 'Randwick', 'Main'). AU
805
+ * thoroughbred only; null elsewhere.
806
+ */
807
+ track_name?: string | null;
808
+ /**
809
+ * UTC instant of the most recent logged track-condition, weather or rail transition for this
810
+ * race's meeting; null when nothing has changed since we began watching it. The sequence is on
811
+ * /v1/racing/track-conditions.
812
+ */
813
+ track_condition_changed_at?: string | null;
814
+ /**
815
+ * Set when the race row is created and revised exactly once, if at all: to 'abandoned' when
816
+ * Racing Australia posts an official abandoned, postponed or transferred notice for the
817
+ * meeting (2026-09-06). Otherwise it reads 'open' for every race in this window. It is not a
818
+ * live market-state signal - /v1/racing/results carries the settled status (interim, final,
819
+ * abandoned).
820
+ */
821
+ status?: string | null;
822
+ /**
823
+ * Scratched runners for this race: {name, number, barrier, scratched_at, late, emergency}.
824
+ * `scratched_at` (UTC) is the book's own stamp where it sends one (Ladbrokes, Neds, BetRight)
825
+ * and Racing Australia's official scratching sheet otherwise (AU thoroughbreds, read
826
+ * two-hourly); absent when neither knows. `late` marks a late scratching, `emergency` an
827
+ * emergency that did not gain a start. Merged across every book that reports one. Greyhound
828
+ * cards also carry box-vacancy placeholders here (a literal '....' or 'Vacant Box'), which are
829
+ * the reporting book naming an empty box rather than a withdrawn runner; match on `number` and
830
+ * treat a name from that set as a vacant box.
831
+ */
832
+ scratchings?: Record<string, unknown>[];
833
+ /**
834
+ * Betting-market state (OPEN, SUSPENDED, CLOSED). Reported by PointsBet, which sends CLOSED
835
+ * when it shuts its book at the jump, and by the Betfair Exchange, which is withheld from
836
+ * customer responses pending a data licence — so a value last written by the exchange reads
837
+ * null for customers and carries through for internal callers. Most races are quoted by both:
838
+ * the exchange holds the state until about two minutes before the jump, PointsBet reports the
839
+ * close.
840
+ */
841
+ market_status?: string | null;
842
+ /**
843
+ * True once the market is in-play. Exchange-only — bookmakers do not run racing in-play — so
844
+ * it carries the same licence hold as `market_status` and reads null for customers.
845
+ */
846
+ inplay?: boolean | null;
847
+ /**
848
+ * UTC time at which the market was FIRST seen shut — the book stopped taking bets on this
849
+ * race. Poll-observed: racing polls every 8s inside 120s of a jump, 15s inside 10 minutes and
850
+ * 20s otherwise, so this is the first poll that saw the state, not the exact instant; it lands
851
+ * within about one poll interval after it. Set once: a market that flickers SUSPENDED -> OPEN
852
+ * -> SUSPENDED keeps the FIRST suspension. TWO SOURCES report market state. PointsBet closes
853
+ * its book at the real jump — measured 2026-09-03 over 9 live races followed across their
854
+ * jump, 0.2 to 1.4 minutes AFTER the advertised start on AU/NZ cards — and the Betfair
855
+ * Exchange reports it while it quotes the market, which is until about two minutes before the
856
+ * jump. `market_state_source` names the one that produced this value. Exchange-sourced stamps
857
+ * are withheld from customer plans (operator ruling 2026-09-01): they republish licence-held
858
+ * market state, so a race seen shut only by the exchange reads null for customers and carries
859
+ * the value for internal callers. Bookmaker-sourced stamps are served to everybody. Null also
860
+ * means no feed observed a transition — it never means the market stayed open.
861
+ */
862
+ market_closed_at?: string | null;
863
+ /**
864
+ * UTC time at which the race was FIRST seen in-play — the actual off, as opposed to
865
+ * `start_time`, which is the ADVERTISED off and moves. Same poll-observed, set-once semantics
866
+ * as `market_closed_at`, and withheld on the same source rule. Reported by the Betfair
867
+ * Exchange only: bookmakers do not run racing in-play, so a bookmaker feed says when it CLOSED
868
+ * its book, not when the race went. For a race the exchange has stopped quoting,
869
+ * `market_closed_at` from a bookmaker is the closest measured instant to the off.
870
+ */
871
+ inplay_at?: string | null;
872
+ /**
873
+ * Which feed's market state produced `market_closed_at` / `inplay_at` on this race —
874
+ * 'pointsbetau' for the bookmaker, 'betfair_ex_au' for the exchange. Set once, alongside the
875
+ * first stamp. Null when no transition has been observed, and null for customer plans whenever
876
+ * the stamps themselves are withheld, so a non-null value here is always the provenance of
877
+ * values you can see.
878
+ */
879
+ market_state_source?: string | null;
880
+ }
881
+ export interface RaceResultOut {
882
+ /**
883
+ * Race id, joinable to /v1/racing/price-history and (while the race is still open)
884
+ * /v1/racing/next-to-go. Null if the race cannot be resolved in the price-history store.
885
+ */
886
+ race_id?: string | null;
887
+ /** Start time of the earliest result currently retained. */
888
+ coverage_from?: string | null;
889
+ venue: string | null;
890
+ venue_id?: string | null;
891
+ venue_canonical?: string | null;
892
+ venue_site?: string | null;
893
+ race_number: number | null;
894
+ category: string | null;
895
+ country: string | null;
896
+ start_time: string;
897
+ race_name?: string | null;
898
+ distance_m?: number | null;
899
+ track_condition?: string | null;
900
+ /**
901
+ * Track condition word, normalised: Good, Soft, Heavy, Firm, Fast, Synthetic. Parsed from
902
+ * `track_condition` with the same helper /v1/racing/horses/form uses, so the two surfaces
903
+ * agree. Null when `track_condition` is null or unparseable; `track_condition` remains the raw
904
+ * string exactly as the source published it.
905
+ */
906
+ going?: string | null;
907
+ /**
908
+ * Numeric going rating 1-12 (the 4 in 'Good (4)'), or null. NULL IS THE MAJORITY CASE, not an
909
+ * exception for two rare words: most stored conditions are a bare word carrying no number at
910
+ * all. Measured 2026-09-10 across all 5,579 stored results, a bare 'Good' is 3,954 rows (71%)
911
+ * and yields null, so a rating is present on roughly 29% of rows — greyhound and other
912
+ * non-thoroughbred meetings publish an unrated word as a rule. A rating is never guessed from
913
+ * the word ('Synthetic', 67 rows, and 'Firm', 0 rows, never carry one). Separately, 205 rows
914
+ * store the four-character STRING 'None' in `track_condition`: `going` and `going_rating` are
915
+ * both null on those, while `track_condition` still serves that literal string, because
916
+ * `track_condition` is the raw source value passed through untouched.
917
+ */
918
+ going_rating?: number | null;
919
+ weather?: string | null;
920
+ /**
921
+ * interim | final | abandoned. `interim` until the placings have been stable for 30 minutes,
922
+ * then `final`. `abandoned` (2026-09-06) is a race that did NOT run: Racing Australia's
923
+ * official Abandoned & Transferred Meetings notice for that venue and date, applied to every
924
+ * race of the meeting — placings are empty and runners null. Settle only against `final`;
925
+ * treat `abandoned` as a void for settlement purposes.
926
+ */
927
+ status: string;
928
+ /**
929
+ * Only on `abandoned` rows: RA's own word for the non-run — "abandoned", "postponed", or
930
+ * "transferred to <venue>". A postponed or transferred meeting did not run at THIS venue on
931
+ * THIS date; if it ran elsewhere or later it appears as its own result.
932
+ */
933
+ status_note?: string | null;
934
+ /**
935
+ * The placegetters: {position, number, name, win_price, place_price, sp, sp_note}. `win_price`
936
+ * and `place_price` are the SETTLING BOOKMAKER'S FIXED prices. `sp` is the OFFICIAL STARTING
937
+ * PRICE as a decimal return on a $1 stake (4.6 means $4.60), null where it is unknown — never
938
+ * 0. It is the settling market's own price, not our consensus and not a bookmaker's fixed
939
+ * price: greyhound SP comes from the official Topaz feed, thoroughbred SP from the Racing
940
+ * Australia results table, and HARNESS SP IS ALWAYS NULL — no source we hold publishes it.
941
+ * `sp_note` carries Racing Australia's favouritism marker on thoroughbred runners ('F'
942
+ * favourite, 'EF' equal favourite) and is null everywhere else. Coverage RE-MEASURED
943
+ * 2026-09-08 over the whole retained store, counting only rows whose `status` is `ran` (a
944
+ * barrier scratching is not a starter and its price is not in this denominator): 99.8% of
945
+ * greyhound starters carry `sp` (14,540 of 14,568) and 88.6% of thoroughbred starters do
946
+ * (2,105 of 2,375). The Racing Australia block that held thoroughbred SP at 0% when this was
947
+ * last measured on 2026-09-03 has lifted and the collector has been landing meetings since. A
948
+ * null `sp` is unknown, never zero — RA prints no SP at all on some rows. On placings, `sp` is
949
+ * copied from the matching `runners` entry when the response is built, so the two can never
950
+ * disagree. On a race with no `runners` payload the `sp` and `sp_note` KEYS ARE ABSENT from
951
+ * each placing — not present and null. The copy step is what creates them and it is skipped
952
+ * when there is nothing to copy from, so a schema-validating consumer sees a different object
953
+ * shape, not a null: read them with `.get()`. The keys are deliberately NOT normalised in,
954
+ * because adding them here would change the shipped payload of every COMPLETE race too, and
955
+ * nothing may move under a customer mid-integration. `field_status` tells you which case you
956
+ * are in before you look.
957
+ */
958
+ placings?: Record<string, unknown>[];
959
+ /**
960
+ * One entry per runner: {number, box, name, position, status, abnormal, margin, time_s,
961
+ * dead_heat}. Presence in this list is NOT proof the runner started. `status` is what says so,
962
+ * and `status` is what you settle on. ⚠️ COVERAGE. Read `field_status` on the row FIRST — it
963
+ * is the per-race answer, and it is what separates a field that has not been filled yet from
964
+ * one that never will be. Two different facts live here and they used to be told as one. (1)
965
+ * WHETHER THERE IS A FIELD AT ALL, and mostly there is not. Measured 2026-09-10 across the
966
+ * whole retained store — 5,577 results, 2,902 of them serving `runners: null`: AU greyhound
967
+ * carries a field on 2,360 of 3,219 races, AU thoroughbred on 315 of 1,104, and AU HARNESS, NZ
968
+ * THOROUGHBRED AND NZ HARNESS CARRY NONE AT ALL — 1,254 races in codes no collector has ever
969
+ * existed for, every one of them `field_status: unsupported`. Inside the two supported codes
970
+ * it is not a clean ramp either: each is filled by a scheduled pass, and where that pass
971
+ * cannot match a race with certainty it writes nothing rather than another race's field, so
972
+ * past the fill window 104 of 2,201 AU greyhound races (4.7%) and 44 of 288 AU thoroughbred
973
+ * races (15.3%) keep `runners: null` for good — `field_status: unavailable`, and polling them
974
+ * will not help. `runners` is JSON null in every one of these cases and has never once been an
975
+ * empty array (0 empty arrays in the whole store, measured the same day), so `runners is None`
976
+ * is a safe test and an empty field is not a shape you have to handle. (2) THE SHAPE OF A
977
+ * FIELD THAT IS PRESENT, which is where the 2026-09-08 date belongs. An AU thoroughbred field
978
+ * WRITTEN from 2026-09-08 carries every starter plus every runner Racing Australia printed as
979
+ * scratched; one written before that date carries STARTERS ONLY, because the collector dropped
980
+ * RA's scratched rows and a stored field is never rewritten. Measured 2026-09-08, all 244
981
+ * thoroughbred fields stored at that point were the old shape and 198 of them publish a
982
+ * `deductions` row for a horse absent from `runners`. So on an older race `deductions` is the
983
+ * list of scratchings and `runners` is the starters; do not read a short `runners` array as a
984
+ * small field. Greyhound fields have carried the whole field since greyhound collection began
985
+ * on the 2026-08-21 Sydney card — but 859 AU greyhound races carry no field at all, which is
986
+ * question (1) and not this one. `position` is the finishing position for every runner that
987
+ * completed the race — not just the placegetters — and is null whenever there is none: the
988
+ * runner did not start, or it started and did not finish. `status` is ran | scratched |
989
+ * late_scratching | reserve (an emergency that never got a start). `abnormal` is the official
990
+ * reason cell, and it carries TWO families that settle in OPPOSITE directions, so read it with
991
+ * `status` and never alone. (1) A DID-NOT-FINISH reason on a row whose `status` is `ran` —
992
+ * Fell, PulledUp, Disqualified, TailedOff, StayedInBox: the runner started and simply has no
993
+ * finishing position, and a bet on it lost. (2) A DID-NOT-START reason on a row whose `status`
994
+ * is `scratched` — 'SB', Racing Australia's code for scratched at the barrier: the runner was
995
+ * taken out of the market before the jump, so `position` is null, it is in no `placings`
996
+ * entry, and bets on it were refunded while the rest of the race was deducted. An 'SB' row may
997
+ * still carry `sp` — the last price Racing Australia printed for it, kept rather than blanked
998
+ * because it is a real market fact about the withdrawn runner. It is NOT the basis of the
999
+ * deduction, and you must never derive one from the other: `sp` comes from RA's results table
1000
+ * while a deduction is struck by the settling bookmaker, and they do not agree. Measured
1001
+ * 2026-09-08 across all five stored 'SB' rows that carry a price, 1/`sp` equals the published
1002
+ * win deduction on NONE of them — Varejao, Sunshine Coast R4 2026-09-06, saddlecloth 5,
1003
+ * withdrawn about 2 minutes 20 before the jump: `sp` 3.70, so 1/3.70 is 0.27 in the dollar,
1004
+ * against a published `deductions` row of 0.18 win and 0.15 place; Dubai Dancer, Warrnambool
1005
+ * R5: 1/5.50 is 0.18 against a published 0.10. Settle from `deductions`, which is
1006
+ * authoritative. None of the five carries a `position` or a `margin`. Telling a barrier
1007
+ * scratching from one made days earlier is CODE-SPECIFIC, and the two collectors do not spell
1008
+ * it the same way — do not write one rule across categories. On AU thoroughbred (Racing
1009
+ * Australia) both are `status: scratched`, and `abnormal` is the discriminator ONLY in one
1010
+ * direction: a barrier scratching carries 'SB'; every other scratching carries `abnormal`
1011
+ * null. Both carry their saddlecloth `number`, both carry `box` null (the runner has no
1012
+ * barrier it jumped from — RA prints 0 there and we do not pass that sentinel on), and an
1013
+ * ordinary scratching carries `sp` null while an 'SB' may carry one. So `abnormal == 'SB'`
1014
+ * identifies a barrier scratching; `abnormal` null tells you only that it was not one, not how
1015
+ * early it happened — use `deductions[].scratched_at` for that. On greyhound (Topaz)
1016
+ * `abnormal` is null on every scratching and `status` is the discriminator: the LATE one is
1017
+ * `late_scratching`, an earlier one is `scratched`. So today `late_scratching` is a
1018
+ * greyhound-only value — a thoroughbred barrier scratching does NOT use it — and `scratched`
1019
+ * does not carry identical meaning in the two codes. `deductions` carries `scratched_at` for
1020
+ * either, and is the one cross-code way to see how close to the jump a runner came out.
1021
+ * `dead_heat` is true on each runner sharing a position. `runner_ref` is a STABLE runner
1022
+ * identifier from the official registry (namespaced: ra:<code> thoroughbred, grv:<id>
1023
+ * greyhound) — it persists across meetings, so it is the join key for longitudinal work; null
1024
+ * while a code's registry source is not yet wired. `trainer` and `jockey` come from the same
1025
+ * official results source and may be null where that source is not yet live for the code. `sp`
1026
+ * is the OFFICIAL STARTING PRICE as a decimal return on a $1 stake (4.6 means $4.60), null
1027
+ * where it is unknown — never 0. It is the settling market's own price, not our consensus and
1028
+ * not a bookmaker's fixed price: greyhound SP comes from the official Topaz feed, thoroughbred
1029
+ * SP from the Racing Australia results table, and HARNESS SP IS ALWAYS NULL — no source we
1030
+ * hold publishes it. `sp_note` carries Racing Australia's favouritism marker on thoroughbred
1031
+ * runners ('F' favourite, 'EF' equal favourite) and is null everywhere else. Coverage
1032
+ * RE-MEASURED 2026-09-08 over the whole retained store, counting only rows whose `status` is
1033
+ * `ran` (a barrier scratching is not a starter and its price is not in this denominator):
1034
+ * 99.8% of greyhound starters carry `sp` (14,540 of 14,568) and 88.6% of thoroughbred starters
1035
+ * do (2,105 of 2,375). The Racing Australia block that held thoroughbred SP at 0% when this
1036
+ * was last measured on 2026-09-03 has lifted and the collector has been landing meetings
1037
+ * since. A null `sp` is unknown, never zero — RA prints no SP at all on some rows. Null
1038
+ * `runners` means the full field is not available for this race — never an empty field;
1039
+ * population is rolling out per racing code as our own results collection comes online.
1040
+ * `placings` remains the source for the settling bookmaker's own fixed prices, and now also
1041
+ * carries `sp` copied from here.
1042
+ */
1043
+ runners?: Record<string, unknown>[] | null;
1044
+ /**
1045
+ * Why `runners` is or is not there, per race — the field a caller was missing when `runners:
1046
+ * null` was the only signal and it collapsed several different futures into one. SIX values.
1047
+ * `complete`: `runners` is present. It is set whenever a field is stored, including on an
1048
+ * abandoned race that unexpectedly carries one, so `field_status == 'complete'` is a safe gate
1049
+ * before parsing. `pending`: a collector exists for this code and the pass that NORMALLY fills
1050
+ * this race has not been yet; `field_note` names the UTC instant by which it will have run.
1051
+ * `overdue`: that pass has been and gone without writing a field, but the collector still
1052
+ * selects this row as a candidate and keeps re-attempting it — this state is NOT terminal. For
1053
+ * AU greyhound the Topaz enricher re-reads every field-less row on each daily run for ten days
1054
+ * after the race (its own LOOKBACK_DAYS); for AU thoroughbred a bounded re-fetch reaches six
1055
+ * days but is run by hand rather than on a schedule. Most rows that reach `overdue` stay null,
1056
+ * so do not build on a field arriving — but do not write the race off either, and `field_note`
1057
+ * gives the horizon. `unavailable`: no scheduled pass still selects this row. Its collector's
1058
+ * own candidate window closed at the instant in `field_note` and no field was written — the
1059
+ * pass either never matched the race or refused an ambiguous match, which it does rather than
1060
+ * write another race's field. `unsupported`: no collector has ever existed for this
1061
+ * category/country — AU harness, NZ thoroughbred and NZ harness, 1,254 of the 5,577 stored
1062
+ * results on 2026-09-10, plus any other country that appears because an AU book listed the
1063
+ * meeting — and no amount of waiting will produce one. `not_applicable`: the race did not run
1064
+ * (abandoned, postponed or transferred) and carries no field, so there is nothing to collect.
1065
+ * TRANSITIONS, stated explicitly because acting on the wrong one costs you data. The scheduled
1066
+ * progression is pending -> overdue -> unavailable, one direction only. It is bounded at every
1067
+ * step by a collector's own window rather than by hope: a race can sit
1068
+ * matched-and-never-resulted forever (The Gardens, 2026-09-05, 12 races), and an open-ended
1069
+ * `pending` would have you polling it forever. Any of those three becomes `complete` the
1070
+ * moment a field is written, AND THAT INCLUDES `unavailable` — nothing un-writes a `runners`
1071
+ * array, so a collector re-run by hand, a widened window or a late upstream publication can
1072
+ * still fill a row we have stopped scheduling passes for. So `unavailable` means 'nothing
1073
+ * further is scheduled', never 'this can never arrive'; if you reconcile, re-read rather than
1074
+ * caching it as final. `unsupported` and `not_applicable` are properties of the racing code
1075
+ * and of the race itself, not of a schedule, and do not change on their own.
1076
+ */
1077
+ field_status?: string;
1078
+ /**
1079
+ * Which collector owns `runners` for this race: `racing_australia` (AU thoroughbred), `topaz`
1080
+ * (AU greyhound), or null where no collector exists. Set on `pending`, `overdue`,
1081
+ * `unavailable` and `complete` rows alike — it names who WOULD fill the field, not who did.
1082
+ */
1083
+ field_source?: string | null;
1084
+ /**
1085
+ * Prose for the non-`complete` states: which pass fills this code, when its normal pass for
1086
+ * this race has been and gone, how long the collector keeps re-attempting the row, or that the
1087
+ * code has no source at all. Null on `complete`. Written for a human reading a response —
1088
+ * branch on `field_status`, never on this text.
1089
+ */
1090
+ field_note?: string | null;
1091
+ /**
1092
+ * Scratched runners and the deduction applied to bets already struck. `win` and `place` are
1093
+ * fractions of the dollar in the range 0-1 (0.34 = 34 cents in the dollar), as published by
1094
+ * the settling bookmaker.
1095
+ */
1096
+ deductions?: Record<string, unknown>[];
1097
+ /**
1098
+ * Dividends declared for the race. `straight` holds one row per placegetter per product:
1099
+ * {position, number, name, market ('WIN' or 'PLC'), product, amount}. `exotics` holds one row
1100
+ * per exotic product: {type, code ('QN', 'EX', 'TF', 'FF'), selection, product, amount}, where
1101
+ * `selection` is the winning saddlecloth numbers in finishing order, e.g. '5,1'. Every
1102
+ * `amount` is the return on a $1 stake, the same convention as the odds fields — a 'FIXED'
1103
+ * straight dividend equals that runner's own settled fixed price. `product` names the settling
1104
+ * pool: 'FIXED' is the bookmaker's own price, the rest are tote products (SP, MIDDIV, VIC,
1105
+ * RD+, BT+SP). A product that declared no dividend is omitted rather than returned as 0.
1106
+ * Exotic dividends are only declared once the race fully settles, a few minutes after the
1107
+ * placings, so a just-resulted race can carry `straight` while `exotics` is still empty — it
1108
+ * fills in on a later poll. Quaddie and daily-double dividends span several races and are NOT
1109
+ * reported here. `straight_types` is the book's raw list of product codes, retained for
1110
+ * backwards compatibility.
1111
+ */
1112
+ dividends?: Record<string, unknown> | null;
1113
+ resulted_at?: string | null;
1114
+ /**
1115
+ * UTC time at which the market was FIRST seen shut — the book stopped taking bets on this
1116
+ * race. Poll-observed: racing polls every 8s inside 120s of a jump, 15s inside 10 minutes and
1117
+ * 20s otherwise, so this is the first poll that saw the state, not the exact instant; it lands
1118
+ * within about one poll interval after it. Set once: a market that flickers SUSPENDED -> OPEN
1119
+ * -> SUSPENDED keeps the FIRST suspension. TWO SOURCES report market state. PointsBet closes
1120
+ * its book at the real jump — measured 2026-09-03 over 9 live races followed across their
1121
+ * jump, 0.2 to 1.4 minutes AFTER the advertised start on AU/NZ cards — and the Betfair
1122
+ * Exchange reports it while it quotes the market, which is until about two minutes before the
1123
+ * jump. `market_state_source` names the one that produced this value. Exchange-sourced stamps
1124
+ * are withheld from customer plans (operator ruling 2026-09-01): they republish licence-held
1125
+ * market state, so a race seen shut only by the exchange reads null for customers and carries
1126
+ * the value for internal callers. Bookmaker-sourced stamps are served to everybody. Null also
1127
+ * means no feed observed a transition — it never means the market stayed open. Copied from the
1128
+ * live race row when the result is written; that row is deleted 10 minutes after the jump and
1129
+ * results land a median 4.9 minutes after the start, so a late result keeps null.
1130
+ */
1131
+ market_closed_at?: string | null;
1132
+ /**
1133
+ * UTC time at which the race was FIRST seen in-play — the actual off, as opposed to
1134
+ * `start_time`, which is the ADVERTISED off and moves. Same poll-observed, set-once semantics
1135
+ * as `market_closed_at`, and withheld on the same source rule. Reported by the Betfair
1136
+ * Exchange only: bookmakers do not run racing in-play, so a bookmaker feed says when it CLOSED
1137
+ * its book, not when the race went. For a race the exchange has stopped quoting,
1138
+ * `market_closed_at` from a bookmaker is the closest measured instant to the off. Copied from
1139
+ * the live race row when the result is written; that row is deleted 10 minutes after the jump
1140
+ * and results land a median 4.9 minutes after the start, so a late result keeps null.
1141
+ */
1142
+ inplay_at?: string | null;
1143
+ /**
1144
+ * Which feed's market state produced `market_closed_at` / `inplay_at` on this race —
1145
+ * 'pointsbetau' for the bookmaker, 'betfair_ex_au' for the exchange. Set once, alongside the
1146
+ * first stamp. Null when no transition has been observed, and null for customer plans whenever
1147
+ * the stamps themselves are withheld, so a non-null value here is always the provenance of
1148
+ * values you can see.
1149
+ */
1150
+ market_state_source?: string | null;
1151
+ }
1152
+ export interface RacingBestOddsOut {
1153
+ race_id: string;
1154
+ venue: string | null;
1155
+ race_number: number | null;
1156
+ category: string | null;
1157
+ country: string | null;
1158
+ start_time: string;
1159
+ race_name?: string | null;
1160
+ track_condition?: string | null;
1161
+ market_percentage?: number | null;
1162
+ books_compared: number;
1163
+ scratchings?: Record<string, unknown>[];
1164
+ /** Age in seconds of the OLDEST bookmaker quote behind this race. */
1165
+ data_age_seconds?: number | null;
1166
+ freshest_age_seconds?: number | null;
1167
+ /**
1168
+ * Which refresh rule this race is on, and therefore what the race-level `stale` means. `live`
1169
+ * = inside 3 hours of the jump: books re-price it on every 8-20s poll, and `stale` fires past
1170
+ * 120 seconds. `card` = further out: books re-price it about every 15 minutes so a full day
1171
+ * fits inside their rate limits, and `stale` fires past 1800 seconds. Every published quote
1172
+ * carries ITS OWN `refresh_tier`, and it can be `card` on a `live` race: Betr's upstream
1173
+ * rate-limits us, so that book stops re-pricing every poll 30 minutes before the jump rather
1174
+ * than 3 hours (PlayUp, on a 6-requests-a-minute budget, at 6 minutes), and its quotes between
1175
+ * there and 3 hours are refreshed every 5-15 minutes, published as `card`, and judged against
1176
+ * 1800 seconds (since 2026-09-07; before that they were marked stale). A book's tier is never
1177
+ * tighter than its race's. `age_seconds` is the TRUE age in every case — only the threshold
1178
+ * changes.
1179
+ */
1180
+ refresh_tier?: string;
1181
+ stale?: boolean;
1182
+ stale_bookmakers?: string[];
1183
+ cached?: boolean | null;
1184
+ cache_age_seconds?: number | null;
1185
+ runners: Record<string, unknown>[];
1186
+ }
1187
+ export interface RacingChangesOut {
1188
+ /**
1189
+ * Send this back as `since` on your next poll. Deliberately set back 30 seconds from the
1190
+ * server clock so no in-flight write is skipped — the feed is at-least-once, not exactly-once.
1191
+ */
1192
+ server_time: string;
1193
+ races?: ChangedRaceOut[];
1194
+ }
1195
+ export interface ResultsCoverageOut {
1196
+ /** When these counts were computed (UTC). */
1197
+ as_of: string;
1198
+ /** How many Sydney days the window spans, ending today. */
1199
+ days: number;
1200
+ /** First Sydney date counted (inclusive). */
1201
+ from_day: string;
1202
+ /**
1203
+ * Last Sydney date counted — today in Sydney, so it is a PART day and its races are mostly
1204
+ * `pending`.
1205
+ */
1206
+ to_day: string;
1207
+ /**
1208
+ * Start time of the earliest result retained anywhere in the store, the same value
1209
+ * /v1/racing/results puts on every row.
1210
+ */
1211
+ coverage_from?: string | null;
1212
+ /**
1213
+ * Every stored result in the window, all codes and all states. races == complete + pending +
1214
+ * overdue + unavailable + unsupported + not_applicable, always.
1215
+ */
1216
+ races: number;
1217
+ /** Races carrying a `runners` field. */
1218
+ complete: number;
1219
+ /** A collector exists and the pass that normally fills the race has not been yet. */
1220
+ pending: number;
1221
+ /**
1222
+ * That pass has been and gone with no field written, but the collector still re-attempts the
1223
+ * row. NOT a terminal state — see `field_status` on /v1/racing/results for the horizons.
1224
+ */
1225
+ overdue: number;
1226
+ /** Past the collector's own retry horizon: no scheduled pass still selects the row. */
1227
+ unavailable: number;
1228
+ /** No collector has ever existed for the code. */
1229
+ unsupported: number;
1230
+ /**
1231
+ * The race did not run (abandoned, postponed or transferred) and has no field — nothing to
1232
+ * collect, not a gap.
1233
+ */
1234
+ not_applicable: number;
1235
+ /**
1236
+ * races - unsupported - not_applicable: the races in this window that could carry a field at
1237
+ * all. It is the denominator of `supported_complete_pct`, published so that a null percentage
1238
+ * is readable as 0-of-0 rather than guessed at.
1239
+ */
1240
+ collectable: number;
1241
+ /**
1242
+ * complete / races as a percentage, across the whole window including codes that can never
1243
+ * carry a field. NULL — not 0.0 — when `races` is 0. An empty window has no coverage figure,
1244
+ * and 0.0 there is a fabricated number that reads as total failure: a `days=1` window called
1245
+ * between Sydney midnight and the day's first result is empty on most days (the median first
1246
+ * result of a Sydney day lands at 10:50 local), so this is an ordinary case and not a corner.
1247
+ */
1248
+ complete_pct?: number | null;
1249
+ /**
1250
+ * complete / collectable: the figure to read if you only care about the codes we collect
1251
+ * fields for. The raw `complete_pct` is dragged down by 1,254 stored races in codes that can
1252
+ * never have one. NULL — not 0.0 — when `collectable` is 0, which happens both on an empty
1253
+ * window and on a window holding only AU harness / NZ rows. If you alert on this figure, treat
1254
+ * null as 'no data' and never as 'zero coverage'.
1255
+ */
1256
+ supported_complete_pct?: number | null;
1257
+ /**
1258
+ * One row per country + category over the window: {country, category, field_source, races,
1259
+ * complete, pending, unavailable, unsupported, not_applicable}.
1260
+ */
1261
+ by_category: Record<string, unknown>[];
1262
+ /**
1263
+ * One row per Sydney day + country + category, newest first, with the same counts. This is the
1264
+ * day-by-day answer to 'is yesterday finished yet'.
1265
+ */
1266
+ by_day: Record<string, unknown>[];
1267
+ /**
1268
+ * Venue-days still missing a field in a code we DO collect: {day, venue, venue_id, category,
1269
+ * country, settled} plus every counter above (races, complete, pending, overdue, unavailable,
1270
+ * unsupported, not_applicable), which sum to `races` exactly. Ordered worst first: most
1271
+ * `unavailable`, then the biggest outstanding shortfall, then oldest day. `settled` is true
1272
+ * when nothing on that venue-day can still fill (no `pending`, no `overdue`) — that is the
1273
+ * difference between a genuine terminal gap and a card simply still in progress, and without
1274
+ * it a day mid-fill outranks a real one. A venue-day is listed when complete + not_applicable
1275
+ * < races: an abandoned card has nothing to collect and is NOT a shortfall. A venue-day whose
1276
+ * only gap is `unsupported` is deliberately absent — it would be most of the list and it is
1277
+ * already counted above. Capped at 200 entries; `venues_incomplete_truncated` says when the
1278
+ * cap bit.
1279
+ */
1280
+ venues_incomplete: Record<string, unknown>[];
1281
+ venues_incomplete_truncated?: boolean;
1282
+ /** Prose: which codes are collected, from when, and what the never-collected codes are. */
1283
+ field_scope: string;
1284
+ }
1285
+ export interface RunPIR {
1286
+ /** Metres from the finish, e.g. 800. */
1287
+ at_m: number;
1288
+ position: number;
1289
+ }
1290
+ export interface RunPlacegetter {
1291
+ position: number;
1292
+ name: string;
1293
+ /** That horse's own stable ra: code — walkable straight back into this endpoint. */
1294
+ runner_ref?: string | null;
1295
+ weight_kg?: number | null;
1296
+ }
1297
+ export interface SourceBlock {
1298
+ feed?: string;
1299
+ region?: string;
1300
+ sport?: string;
1301
+ coverage_from: string;
1302
+ /** Meeting date of the latest run held, i.e. how far the last sync got. */
1303
+ coverage_to?: string | null;
1304
+ /** UTC timestamp of the last Topaz sync into this store. */
1305
+ synced_at?: string | null;
1306
+ }
1307
+ export interface SplitRecord {
1308
+ starts: number;
1309
+ wins: number;
1310
+ seconds: number;
1311
+ thirds: number;
1312
+ }
1313
+ export interface SportOddsEventOut {
1314
+ id: string;
1315
+ sport_key: string;
1316
+ sport_title: string;
1317
+ commence_time: string;
1318
+ home_team: string | null;
1319
+ away_team: string | null;
1320
+ competition?: string | null;
1321
+ bookmakers: Record<string, unknown>[];
1322
+ odds_format?: string | null;
1323
+ fetched_at?: string | null;
1324
+ data_age_seconds?: number | null;
1325
+ freshest_age_seconds?: number | null;
1326
+ stale?: boolean;
1327
+ stale_bookmakers?: string[];
1328
+ cached?: boolean | null;
1329
+ cache_age_seconds?: number | null;
1330
+ canonical_event_id?: string | null;
1331
+ data_quality?: Record<string, unknown> | null;
1332
+ }
1333
+ export interface SportOut {
1334
+ key: string;
1335
+ title: string;
1336
+ group: string | null;
1337
+ active: boolean;
1338
+ cached?: boolean | null;
1339
+ cache_age_seconds?: number | null;
1340
+ canonical_event_id?: string | null;
1341
+ data_quality?: Record<string, unknown> | null;
1342
+ }
1343
+ export interface SportsArbOut {
1344
+ event_id: string;
1345
+ sport_key: string;
1346
+ home_team: string | null;
1347
+ away_team: string | null;
1348
+ commence_time: string;
1349
+ is_arb: boolean;
1350
+ arb_pct: number;
1351
+ optimal_stakes: Record<string, unknown>[];
1352
+ max_overlay_pct: number;
1353
+ selections: Record<string, unknown>[];
1354
+ }
1355
+ export interface TrackConditionsOut {
1356
+ /** Meeting day, Australia/Sydney calendar date. */
1357
+ date: string;
1358
+ meetings: Record<string, unknown>[];
1359
+ note: string;
1360
+ }
1361
+ export interface TrainerRef {
1362
+ /** Topaz trainerId. Stable across meetings. */
1363
+ id?: number | null;
1364
+ name?: string | null;
1365
+ }
1366
+ export interface UsageOut {
1367
+ plan: string;
1368
+ account_email_masked?: string | null;
1369
+ credits_used: number;
1370
+ credits_limit: number;
1371
+ credits_remaining: number;
1372
+ reset_at: string | null;
1373
+ period_start?: string | null;
1374
+ next_reset_at?: string | null;
1375
+ burn_rate_per_day?: number;
1376
+ projected_month_end_credits?: number;
1377
+ projected_exhaustion_date?: string | null;
1378
+ warning?: string | null;
1379
+ upgrade?: Record<string, unknown> | null;
1380
+ all_time?: Record<string, unknown>;
1381
+ all_time_by_endpoint?: Record<string, unknown>[];
1382
+ usage_by_endpoint?: Record<string, unknown>[];
1383
+ recent_activity?: Record<string, unknown>[];
1384
+ usage_by_endpoint_period?: Record<string, unknown>[];
1385
+ recent_activity_period?: Record<string, unknown>[];
1386
+ plans?: Record<string, Record<string, unknown>>;
1387
+ }
1388
+ export interface VenueOut {
1389
+ venue_id: string;
1390
+ venue_canonical: string | null;
1391
+ venue_site: string | null;
1392
+ country: string | null;
1393
+ categories: string[];
1394
+ spellings: string[];
1395
+ }
1396
+ export interface WebhookCreate {
1397
+ /** HTTPS endpoint that receives the POST. Plain http:// is rejected with 422. */
1398
+ url: string;
1399
+ /**
1400
+ * Events to subscribe to. Two events have live producers: `arb.opportunity` (a cross-book arb
1401
+ * opened) and `race.odds_open` (the first bookmaker prices for a race just landed — fires once
1402
+ * per race, so you can fetch the card on push instead of schedule-polling). Since the
1403
+ * early-market lanes of 2026-09-01 it fires HOURS ahead, not minutes: measured over the 48h to
1404
+ * 2026-09-03 11:00 UTC, the first price landed a median 10.1h before the jump on AU
1405
+ * thoroughbreds, 5.3h on greyhounds, 6.8h on harness - typically overnight or early morning
1406
+ * for the day's card, from one or two early books; the full board fills inside about an hour
1407
+ * of the jump. `odds.change` is delivered solely by POST /v1/webhooks/{id}/test; `race.result`
1408
+ * and `market.suspend` are accepted but never fire.
1409
+ */
1410
+ events?: string[];
1411
+ /**
1412
+ * Only deliver arb.opportunity when the theoretical edge is at least this percentage. Omit for
1413
+ * every opportunity.
1414
+ */
1415
+ min_edge_pct?: number | null;
1416
+ /**
1417
+ * Only deliver events for these sports. Sport keys as returned by GET /v1/sports; racing
1418
+ * events (arbs and race.odds_open) arrive as racing_horse, racing_greyhound or racing_harness.
1419
+ * Omit for all sports.
1420
+ */
1421
+ sports?: string[] | null;
1422
+ }
1423
+ export interface WebhookDeliveryResponse {
1424
+ id: string;
1425
+ webhook_id: string;
1426
+ event_type: string;
1427
+ payload: unknown | null;
1428
+ /**
1429
+ * delivered (receiver answered 2xx) or failed (both attempts refused, see `error`). Rows
1430
+ * written before 2026-09-03 read `pending`.
1431
+ */
1432
+ status?: string;
1433
+ /** POST attempts made for this row, at most 2. */
1434
+ attempt_count?: number;
1435
+ status_code: number | null;
1436
+ error: string | null;
1437
+ delivered_at: string | null;
1438
+ }
1439
+ export interface WebhookResponse {
1440
+ id: string;
1441
+ url: string;
1442
+ events: string[];
1443
+ min_edge_pct?: number | null;
1444
+ sports?: string[] | null;
1445
+ is_active: boolean;
1446
+ created_at: string | null;
1447
+ last_delivery_at: string | null;
1448
+ failure_count: number;
1449
+ secret?: string | null;
1450
+ delivery_entitled?: boolean;
1451
+ delivery_paused_reason?: string | null;
1452
+ }
1453
+ //# sourceMappingURL=types.generated.d.ts.map