Skip to content

Commit dc4e22b

Browse files
Mlaz-codeclaude
andcommitted
feat(sdk): add odds.closing + keys CRUD
Two coverage gaps in the TS SDK against the live REST surface: - odds.closing(eventId, { sportsbook? }) → GET /odds/closing Returns ClosingSnapshot with per-book ClosingOdd arrays. - keys namespace → full CRUD on /account/keys list() — GET /account/keys create(name) — POST /account/keys revoke(keyId) — DELETE /account/keys/{keyId} rotate(keyId) — POST /account/keys/{keyId}/rotate New types (ClosingOdd, ClosingBooks, ClosingSnapshot, APIKey, CreatedAPIKey, RotatedAPIKey, RevokedAPIKey, SubscriptionTier) all exported. Snake_case wire fields preserved. Bumped 0.2.3 → 0.2.4. typecheck clean, 68/68 tests (+8 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a9e8f77 commit dc4e22b

3 files changed

Lines changed: 405 additions & 2 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sharp-api/client",
3-
"version": "0.2.3",
3+
"version": "0.2.4",
44
"description": "Official TypeScript/JavaScript client for the SharpAPI real-time sports betting odds API",
55
"type": "module",
66
"main": "./dist/index.cjs",

src/index.ts

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,101 @@ export interface Event {
330330
status: 'upcoming' | 'live' | 'ended'
331331
}
332332

333+
/** Subscription tier identifier */
334+
export type SubscriptionTier = 'free' | 'hobby' | 'pro' | 'sharp' | 'enterprise'
335+
336+
/** Single closing-line odd entry returned by `GET /odds/closing`.
337+
*
338+
* Field names match the wire format (snake_case). `line` only appears on
339+
* spread/total markets; `player_name` and `stat_category` only appear on
340+
* player-prop markets. */
341+
export interface ClosingOdd {
342+
sportsbook: string
343+
market_type: string
344+
selection: string
345+
selection_type:
346+
| 'home'
347+
| 'away'
348+
| 'over'
349+
| 'under'
350+
| 'draw'
351+
| 'home_draw'
352+
| 'away_draw'
353+
| 'home_away'
354+
| (string & {})
355+
odds_american: number
356+
odds_decimal: number
357+
/** Spread / total line — present only on point-spread and totals markets. */
358+
line?: number
359+
/** Player-prop only. */
360+
player_name?: string
361+
/** Player-prop only. */
362+
stat_category?: string
363+
}
364+
365+
/** Books-keyed map of closing odds — one entry per sportsbook. */
366+
export type ClosingBooks = Record<string, ClosingOdd[]>
367+
368+
/** Closing-line snapshot for a single event.
369+
*
370+
* Returned in the `data` field of `GET /odds/closing?event_id=...`. When
371+
* no closing snapshot has been captured yet, `books` is an empty object
372+
* and `sport`/`league`/team fields may be absent. */
373+
export interface ClosingSnapshot {
374+
event_id: string
375+
sport?: string
376+
league?: string
377+
home_team?: string
378+
away_team?: string
379+
event_start_time?: string
380+
/** Server-side capture timestamp (ISO 8601). */
381+
captured_at?: string
382+
books: ClosingBooks
383+
}
384+
385+
/** API key record returned by `GET /account/keys` (and the create/rotate
386+
* endpoints, which add a one-time-only `key` field on the new key). */
387+
export interface APIKey {
388+
id: string
389+
/** Last-8-char-suffix mask of the key id (e.g. `"...FrolK3uD"`). */
390+
id_masked: string
391+
name: string | null
392+
tier: SubscriptionTier | (string & {})
393+
is_active: boolean
394+
created_at: string
395+
updated_at: string
396+
}
397+
398+
/** Response from `POST /account/keys` — includes the plaintext `key`
399+
* value, which is shown only once. */
400+
export interface CreatedAPIKey {
401+
id: string
402+
/** The full plaintext API key (e.g. `sk_live_xxx`). Only returned on
403+
* creation — the server cannot show it again. */
404+
key: string
405+
name: string | null
406+
tier: SubscriptionTier | (string & {})
407+
}
408+
409+
/** Response from `POST /account/keys/{id}/rotate`. The new key value is
410+
* shown only once; the old key may either be revoked immediately or
411+
* expire after a grace period. */
412+
export interface RotatedAPIKey {
413+
new_key: CreatedAPIKey
414+
old_key: {
415+
id: string
416+
revoked: boolean
417+
expires_at: string | null
418+
}
419+
}
420+
421+
/** Response from `DELETE /account/keys/{id}`. */
422+
export interface RevokedAPIKey {
423+
deleted: boolean
424+
key_id: string
425+
message: string
426+
}
427+
333428
/** Account/key info */
334429
export interface AccountInfo {
335430
key: {
@@ -406,6 +501,13 @@ export interface MiddlesParams {
406501
offset?: number
407502
}
408503

504+
export interface ClosingParams {
505+
/** Restrict the response to a specific sportsbook (or list — CSV on the
506+
* wire). When omitted, every book that has a closing snapshot for the
507+
* event is returned. */
508+
sportsbook?: string | string[]
509+
}
510+
409511
export interface StreamParams {
410512
sportsbook?: string | string[]
411513
add_sportsbook?: string | string[]
@@ -465,7 +567,7 @@ class HttpClient {
465567
}
466568

467569
private async request<T>(
468-
method: 'GET' | 'POST',
570+
method: 'GET' | 'POST' | 'DELETE',
469571
path: string,
470572
body?: unknown,
471573
params?: Record<string, unknown>,
@@ -541,6 +643,10 @@ class HttpClient {
541643
return this.request<T>('POST', path, body, params)
542644
}
543645

646+
async delete<T>(path: string, params?: Record<string, unknown>): Promise<T> {
647+
return this.request<T>('DELETE', path, undefined, params)
648+
}
649+
544650
getStreamUrl(path: string, params?: Record<string, unknown>): string {
545651
const url = this.buildUrl(path, { ...params, api_key: this.apiKey })
546652
return url
@@ -1024,6 +1130,37 @@ class OddsResource {
10241130
async batch(eventIds: string[]): Promise<APIResponse<NormalizedOdds[]>> {
10251131
return this.http.post('/api/v1/odds/batch', { event_ids: eventIds })
10261132
}
1133+
1134+
/**
1135+
* Get the closing-line snapshot for a single event.
1136+
*
1137+
* Returns the per-book odds captured at event kickoff (the moment the
1138+
* sharp market locked). Used for line-shopping post-mortems and CLV
1139+
* (closing-line value) calculations.
1140+
*
1141+
* When no closing snapshot has been captured yet, `data.books` is an
1142+
* empty object.
1143+
*
1144+
* @param eventId — canonical event id (e.g. `"evt_..."`).
1145+
* @param options — optional `sportsbook` filter (CSV on the wire).
1146+
*
1147+
* @example
1148+
* ```typescript
1149+
* const { data } = await api.odds.closing('evt_123')
1150+
* for (const [book, odds] of Object.entries(data.books)) {
1151+
* console.log(book, odds.length, 'markets')
1152+
* }
1153+
* ```
1154+
*/
1155+
async closing(
1156+
eventId: string,
1157+
options?: ClosingParams,
1158+
): Promise<APIResponse<ClosingSnapshot>> {
1159+
return this.http.get('/api/v1/odds/closing', {
1160+
event_id: eventId,
1161+
...(options ?? {}),
1162+
} as Record<string, unknown>)
1163+
}
10271164
}
10281165

10291166
class ArbitrageResource {
@@ -1094,6 +1231,43 @@ class AccountResource {
10941231
}
10951232
}
10961233

1234+
/** CRUD for the caller's API keys, backed by `/api/v1/account/keys`.
1235+
*
1236+
* Returned key records use snake_case field names to match the wire
1237+
* format ({@link APIKey}). The plaintext key value is only ever returned
1238+
* by {@link KeysResource.create} and {@link KeysResource.rotate} — the
1239+
* server cannot show it again. */
1240+
class KeysResource {
1241+
constructor(private http: HttpClient) {}
1242+
1243+
/** List every API key on the caller's account. */
1244+
async list(): Promise<
1245+
APIResponse<APIKey[]> & { meta?: { count?: number; max_keys?: number } }
1246+
> {
1247+
return this.http.get('/api/v1/account/keys')
1248+
}
1249+
1250+
/** Create a new API key. The plaintext `key` value in the response is
1251+
* shown only once — the caller must store it securely. */
1252+
async create(name: string): Promise<APIResponse<CreatedAPIKey>> {
1253+
return this.http.post('/api/v1/account/keys', { name })
1254+
}
1255+
1256+
/** Revoke an API key by id. The caller cannot revoke the key they are
1257+
* currently authenticating with. */
1258+
async revoke(keyId: string): Promise<APIResponse<RevokedAPIKey>> {
1259+
return this.http.delete(`/api/v1/account/keys/${encodeURIComponent(keyId)}`)
1260+
}
1261+
1262+
/** Rotate an API key — atomically issues a replacement and revokes the
1263+
* original. The new plaintext `key` is shown only once. */
1264+
async rotate(keyId: string): Promise<APIResponse<RotatedAPIKey>> {
1265+
return this.http.post(
1266+
`/api/v1/account/keys/${encodeURIComponent(keyId)}/rotate`,
1267+
)
1268+
}
1269+
}
1270+
10971271
class StreamResource {
10981272
private apiKey: string
10991273

@@ -1230,6 +1404,8 @@ export class SharpAPI {
12301404
readonly middles: MiddlesResource
12311405
/** Account endpoints */
12321406
readonly account: AccountResource
1407+
/** API key management — `/api/v1/account/keys` CRUD. */
1408+
readonly keys: KeysResource
12331409
/** Streaming endpoints (requires WebSocket add-on) */
12341410
readonly stream: StreamResource
12351411

@@ -1245,6 +1421,7 @@ export class SharpAPI {
12451421
this.ev = new EVResource(this.http)
12461422
this.middles = new MiddlesResource(this.http)
12471423
this.account = new AccountResource(this.http)
1424+
this.keys = new KeysResource(this.http)
12481425
this.stream = new StreamResource(this.http)
12491426
}
12501427
}

0 commit comments

Comments
 (0)