@0xinsider/mcp 2.1.0 → 2.4.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.
- package/README.md +49 -17
- package/dist/catalog.generated.d.ts +42 -6
- package/dist/catalog.generated.d.ts.map +1 -1
- package/dist/catalog.generated.js +51 -6
- package/dist/catalog.generated.js.map +1 -1
- package/dist/cli-lib.d.ts +8 -0
- package/dist/cli-lib.d.ts.map +1 -1
- package/dist/cli-lib.js +123 -17
- package/dist/cli-lib.js.map +1 -1
- package/dist/client.d.ts +35 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +42 -4
- package/dist/client.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/product-cli/client.d.ts +3 -2
- package/dist/product-cli/client.d.ts.map +1 -1
- package/dist/product-cli/client.js.map +1 -1
- package/dist/product-cli/index.d.ts.map +1 -1
- package/dist/product-cli/index.js +3 -1
- package/dist/product-cli/index.js.map +1 -1
- package/dist/prompts/index.js +2 -2
- package/dist/sdk/client.d.ts +861 -136
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +924 -127
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/errors.d.ts +48 -4
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/errors.js +123 -3
- package/dist/sdk/errors.js.map +1 -1
- package/dist/sdk/index.d.ts +40 -9
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +6 -4
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/pagination.d.ts +133 -15
- package/dist/sdk/pagination.d.ts.map +1 -1
- package/dist/sdk/pagination.js +132 -34
- package/dist/sdk/pagination.js.map +1 -1
- package/dist/sdk/retry.d.ts +68 -0
- package/dist/sdk/retry.d.ts.map +1 -0
- package/dist/sdk/retry.js +125 -0
- package/dist/sdk/retry.js.map +1 -0
- package/dist/sdk/schema.d.ts +2200 -688
- package/dist/sdk/schema.d.ts.map +1 -1
- package/dist/sdk/stream.d.ts +323 -8
- package/dist/sdk/stream.d.ts.map +1 -1
- package/dist/sdk/stream.js +582 -56
- package/dist/sdk/stream.js.map +1 -1
- package/dist/sdk/webhooks.d.ts +104 -12
- package/dist/sdk/webhooks.d.ts.map +1 -1
- package/dist/sdk/webhooks.js +22 -4
- package/dist/sdk/webhooks.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +13 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/explore-markets.d.ts.map +1 -1
- package/dist/tools/explore-markets.js +2 -5
- package/dist/tools/explore-markets.js.map +1 -1
- package/dist/tools/games.d.ts +5 -0
- package/dist/tools/games.d.ts.map +1 -0
- package/dist/tools/games.js +103 -0
- package/dist/tools/games.js.map +1 -0
- package/dist/tools/get-insider-radar.d.ts.map +1 -1
- package/dist/tools/get-insider-radar.js +11 -40
- package/dist/tools/get-insider-radar.js.map +1 -1
- package/dist/tools/get-large-positions.d.ts.map +1 -1
- package/dist/tools/get-large-positions.js +3 -10
- package/dist/tools/get-large-positions.js.map +1 -1
- package/dist/tools/get-leaderboard.d.ts.map +1 -1
- package/dist/tools/get-leaderboard.js +23 -12
- package/dist/tools/get-leaderboard.js.map +1 -1
- package/dist/tools/get-market-intel.d.ts.map +1 -1
- package/dist/tools/get-market-intel.js +3 -6
- package/dist/tools/get-market-intel.js.map +1 -1
- package/dist/tools/get-position-timeline.d.ts.map +1 -1
- package/dist/tools/get-position-timeline.js +3 -10
- package/dist/tools/get-position-timeline.js.map +1 -1
- package/dist/tools/get-positions.d.ts +2 -0
- package/dist/tools/get-positions.d.ts.map +1 -1
- package/dist/tools/get-positions.js +8 -11
- package/dist/tools/get-positions.js.map +1 -1
- package/dist/tools/get-suspicious-trades.d.ts +26 -0
- package/dist/tools/get-suspicious-trades.d.ts.map +1 -0
- package/dist/tools/get-suspicious-trades.js +91 -0
- package/dist/tools/get-suspicious-trades.js.map +1 -0
- package/dist/tools/get-trader-pnl.d.ts.map +1 -1
- package/dist/tools/get-trader-pnl.js +3 -6
- package/dist/tools/get-trader-pnl.js.map +1 -1
- package/dist/tools/get-trader.d.ts.map +1 -1
- package/dist/tools/get-trader.js +3 -6
- package/dist/tools/get-trader.js.map +1 -1
- package/dist/tools/get-trending-wallets.d.ts.map +1 -1
- package/dist/tools/get-trending-wallets.js +3 -10
- package/dist/tools/get-trending-wallets.js.map +1 -1
- package/dist/tools/get-whale-trades.d.ts +2 -0
- package/dist/tools/get-whale-trades.d.ts.map +1 -1
- package/dist/tools/get-whale-trades.js +21 -16
- package/dist/tools/get-whale-trades.js.map +1 -1
- package/dist/tools/pick-of-the-day.d.ts.map +1 -1
- package/dist/tools/pick-of-the-day.js +5 -21
- package/dist/tools/pick-of-the-day.js.map +1 -1
- package/dist/tools/read-parity.d.ts +8 -0
- package/dist/tools/read-parity.d.ts.map +1 -1
- package/dist/tools/read-parity.js +125 -77
- package/dist/tools/read-parity.js.map +1 -1
- package/dist/tools/result.d.ts +31 -0
- package/dist/tools/result.d.ts.map +1 -0
- package/dist/tools/result.js +70 -0
- package/dist/tools/result.js.map +1 -0
- package/dist/tools/search-markets.d.ts.map +1 -1
- package/dist/tools/search-markets.js +16 -11
- package/dist/tools/search-markets.js.map +1 -1
- package/package.json +4 -4
package/dist/sdk/client.js
CHANGED
|
@@ -19,12 +19,17 @@
|
|
|
19
19
|
* - Extra typed convenience methods for list/candle/webhook-event/stream
|
|
20
20
|
* surfaces and a typed `Grade` enum.
|
|
21
21
|
*/
|
|
22
|
-
import { errorFromResponse, OxinsiderApiError, RequestTimeoutError, } from "./errors.js";
|
|
22
|
+
import { errorFromResponse, OxinsiderApiError, InvalidResponseError, RequestTimeoutError, } from "./errors.js";
|
|
23
|
+
import { RETRY_AFTER_CEILING_MS, retryAfterSeconds, sleepUnlessAborted, } from "./retry.js";
|
|
23
24
|
/**
|
|
24
|
-
* The full V1 operation table. One row per OpenAPI operation
|
|
25
|
-
*
|
|
26
|
-
* `
|
|
27
|
-
*
|
|
25
|
+
* The full V1 operation table. One row per OpenAPI operation with a
|
|
26
|
+
* documented 2xx, which since #16137 includes the `201`-only
|
|
27
|
+
* `registerAgent`; `kind` says which method reads its body. Kept in lockstep
|
|
28
|
+
* with `web/public/api/v1/openapi.json` by
|
|
29
|
+
* `scripts/check-sdk-openapi-drift.mjs`, which also checks every row's kind
|
|
30
|
+
* against the spec. Do not add a row without a matching spec operation, and
|
|
31
|
+
* do not remove a spec operation without removing its row. A redirect-only
|
|
32
|
+
* operation belongs in `REDIRECT_OPERATIONS` below, not here.
|
|
28
33
|
*/
|
|
29
34
|
export const API_CLIENT_OPERATIONS = [
|
|
30
35
|
{ method: "GET", path: "/api/v1/me", operationId: "getAccountIdentity", auth: "bearer" },
|
|
@@ -76,6 +81,12 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
76
81
|
operationId: "getTraderPnl",
|
|
77
82
|
auth: "bearer",
|
|
78
83
|
},
|
|
84
|
+
{
|
|
85
|
+
method: "GET",
|
|
86
|
+
path: "/api/v1/trader/{address}/categories",
|
|
87
|
+
operationId: "getTraderCategoryRecords",
|
|
88
|
+
auth: "bearer",
|
|
89
|
+
},
|
|
79
90
|
// Pre-existing op-table drift (surfaced by test/drift.test.ts, #6915): these
|
|
80
91
|
// 200-returning openapi operations were never mirrored into the client table.
|
|
81
92
|
// Added so the table stays contract-complete against the checked-in spec. All
|
|
@@ -94,6 +105,7 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
94
105
|
path: "/api/v1/trader/{address}/context.md",
|
|
95
106
|
operationId: "getTraderContextMarkdown",
|
|
96
107
|
auth: "bearer",
|
|
108
|
+
kind: "text",
|
|
97
109
|
},
|
|
98
110
|
{
|
|
99
111
|
method: "POST",
|
|
@@ -116,6 +128,18 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
116
128
|
// Pre-existing gap, fixed here (#7209): the route has shipped in openapi.json,
|
|
117
129
|
// llms-full.txt, agents.md, and the discovery doc, but never in the SDK table --
|
|
118
130
|
// so `drift.test.ts` was RED on main and SDK users had no typed method for it.
|
|
131
|
+
{
|
|
132
|
+
method: "GET",
|
|
133
|
+
path: "/api/v1/sports/pre-game-sides",
|
|
134
|
+
operationId: "listPreGameSides",
|
|
135
|
+
auth: "bearer",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
method: "GET",
|
|
139
|
+
path: "/api/v1/sports/pre-game-side-observations",
|
|
140
|
+
operationId: "listPreGameSideObservations",
|
|
141
|
+
auth: "bearer",
|
|
142
|
+
},
|
|
119
143
|
{
|
|
120
144
|
method: "GET",
|
|
121
145
|
path: "/api/v1/sports-edge-signals",
|
|
@@ -128,6 +152,48 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
128
152
|
operationId: "listSportsEdgeObservations",
|
|
129
153
|
auth: "bearer",
|
|
130
154
|
},
|
|
155
|
+
{
|
|
156
|
+
method: "GET",
|
|
157
|
+
path: "/api/v1/games",
|
|
158
|
+
operationId: "listGames",
|
|
159
|
+
auth: "bearer",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
method: "GET",
|
|
163
|
+
path: "/api/v1/games/{event_slug}",
|
|
164
|
+
operationId: "getGame",
|
|
165
|
+
auth: "bearer",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
method: "GET",
|
|
169
|
+
path: "/api/v1/large-trades",
|
|
170
|
+
operationId: "listLargeTrades",
|
|
171
|
+
auth: "bearer",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
method: "GET",
|
|
175
|
+
path: "/api/v1/large-trades/history",
|
|
176
|
+
operationId: "listLargeTradeHistory",
|
|
177
|
+
auth: "bearer",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
method: "GET",
|
|
181
|
+
path: "/api/v1/large-trades/{id}/counterparties/executions",
|
|
182
|
+
operationId: "listLargeTradeCounterpartyExecutions",
|
|
183
|
+
auth: "bearer",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
method: "GET",
|
|
187
|
+
path: "/api/v1/large-trades/{id}/counterparties/executions/{execution_id}/makers",
|
|
188
|
+
operationId: "listLargeTradeCounterpartyMakers",
|
|
189
|
+
auth: "bearer",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
method: "GET",
|
|
193
|
+
path: "/api/v1/large-trades/{id}",
|
|
194
|
+
operationId: "getLargeTrade",
|
|
195
|
+
auth: "bearer",
|
|
196
|
+
},
|
|
131
197
|
{
|
|
132
198
|
method: "GET",
|
|
133
199
|
path: "/api/v1/whale-trades",
|
|
@@ -200,6 +266,12 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
200
266
|
operationId: "getPlatforms",
|
|
201
267
|
auth: "none",
|
|
202
268
|
},
|
|
269
|
+
{
|
|
270
|
+
method: "GET",
|
|
271
|
+
path: "/api/v1/market/{condition_id}/holders",
|
|
272
|
+
operationId: "getMarketHolders",
|
|
273
|
+
auth: "bearer",
|
|
274
|
+
},
|
|
203
275
|
{
|
|
204
276
|
method: "GET",
|
|
205
277
|
path: "/api/v1/market/{condition_id}/intel",
|
|
@@ -223,6 +295,7 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
223
295
|
path: "/api/v1/market/{condition_id}/context.md",
|
|
224
296
|
operationId: "getMarketContextMarkdown",
|
|
225
297
|
auth: "bearer",
|
|
298
|
+
kind: "text",
|
|
226
299
|
},
|
|
227
300
|
{
|
|
228
301
|
method: "GET",
|
|
@@ -230,6 +303,22 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
230
303
|
operationId: "getMarketCandles",
|
|
231
304
|
auth: "bearer",
|
|
232
305
|
},
|
|
306
|
+
{
|
|
307
|
+
method: "GET",
|
|
308
|
+
path: "/api/v1/suspicious-trades",
|
|
309
|
+
operationId: "listSuspiciousTrades",
|
|
310
|
+
auth: "bearer",
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
method: "GET",
|
|
314
|
+
path: "/api/v1/suspicious-trades/{id}",
|
|
315
|
+
operationId: "getSuspiciousTrade",
|
|
316
|
+
auth: "bearer",
|
|
317
|
+
},
|
|
318
|
+
// Deprecated aliases (#16301). Both paths stay live with no retirement
|
|
319
|
+
// date; responses carry Deprecation and a Link rel="successor-version".
|
|
320
|
+
// `/api/v1/insider-radar/{id}` also keeps answering `object: "radar_flag"`,
|
|
321
|
+
// so these entries are what an existing integration must keep exercising.
|
|
233
322
|
{
|
|
234
323
|
method: "GET",
|
|
235
324
|
path: "/api/v1/insider-radar",
|
|
@@ -253,6 +342,7 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
253
342
|
path: "/api/v1/stream",
|
|
254
343
|
operationId: "getStream",
|
|
255
344
|
auth: "bearer",
|
|
345
|
+
kind: "sse",
|
|
256
346
|
},
|
|
257
347
|
{
|
|
258
348
|
method: "GET",
|
|
@@ -296,6 +386,24 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
296
386
|
operationId: "rotateWebhookSecret",
|
|
297
387
|
auth: "bearer",
|
|
298
388
|
},
|
|
389
|
+
{
|
|
390
|
+
method: "POST",
|
|
391
|
+
path: "/api/v1/webhooks/{id}/rotate-secret/prepare",
|
|
392
|
+
operationId: "prepareWebhookSecret",
|
|
393
|
+
auth: "bearer",
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
method: "POST",
|
|
397
|
+
path: "/api/v1/webhooks/{id}/rotate-secret/activate",
|
|
398
|
+
operationId: "activateWebhookSecret",
|
|
399
|
+
auth: "bearer",
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
method: "POST",
|
|
403
|
+
path: "/api/v1/webhooks/{id}/rotate-secret/retire",
|
|
404
|
+
operationId: "retireWebhookSecret",
|
|
405
|
+
auth: "bearer",
|
|
406
|
+
},
|
|
299
407
|
{
|
|
300
408
|
method: "GET",
|
|
301
409
|
path: "/api/v1/webhooks/events",
|
|
@@ -325,12 +433,7 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
325
433
|
path: "/api/v1/mcp",
|
|
326
434
|
operationId: "createMcpJsonRpcResponse",
|
|
327
435
|
auth: "bearer",
|
|
328
|
-
|
|
329
|
-
{
|
|
330
|
-
method: "GET",
|
|
331
|
-
path: "/api/v1/mcp",
|
|
332
|
-
operationId: "openMcpEventStream",
|
|
333
|
-
auth: "bearer",
|
|
436
|
+
kind: "jsonrpc",
|
|
334
437
|
},
|
|
335
438
|
{
|
|
336
439
|
method: "GET",
|
|
@@ -384,9 +487,100 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
384
487
|
method: "GET",
|
|
385
488
|
path: "/api/v1/pick-of-the-day/ledger",
|
|
386
489
|
operationId: "getPickOfTheDayLedger",
|
|
490
|
+
// Keyless since #16459: the commitment ledger is published to be
|
|
491
|
+
// republished, so a client can read it with no key configured.
|
|
492
|
+
auth: "none",
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
method: "POST",
|
|
496
|
+
path: "/api/v1/agents/register",
|
|
497
|
+
operationId: "registerAgent",
|
|
498
|
+
// `security: []` in the document: minting a sandbox key is the one write
|
|
499
|
+
// that must work before a caller has any credential.
|
|
500
|
+
auth: "none",
|
|
501
|
+
},
|
|
502
|
+
];
|
|
503
|
+
/**
|
|
504
|
+
* Published operations whose success is a redirect, with the method that
|
|
505
|
+
* follows it (#16137). These have no 2xx body, so they are absent from
|
|
506
|
+
* `API_CLIENT_OPERATIONS` and from the generated `schema.ts`;
|
|
507
|
+
* `scripts/check-sdk-openapi-drift.mjs` requires every redirect-only spec
|
|
508
|
+
* operation to be declared here, so one cannot go missing in silence again.
|
|
509
|
+
*/
|
|
510
|
+
export const REDIRECT_OPERATIONS = [
|
|
511
|
+
{
|
|
512
|
+
method: "GET",
|
|
513
|
+
path: "/api/v1/trader/{address}/export/download",
|
|
514
|
+
operationId: "downloadTraderExport",
|
|
387
515
|
auth: "bearer",
|
|
516
|
+
status: 302,
|
|
517
|
+
handledBy: "getTraderExportDownloadUrl() resolves the Location; downloadTraderExport() then fetches the object with no Authorization header.",
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
method: "GET",
|
|
521
|
+
path: "/api/v1/openapi.json",
|
|
522
|
+
operationId: "redirectApiOpenapiSpec",
|
|
523
|
+
auth: "none",
|
|
524
|
+
status: 307,
|
|
525
|
+
handledBy: "No method: the document is public and static, so fetch the URL directly and let your runtime follow the redirect. This package ships the same contract as generated types in schema.ts.",
|
|
388
526
|
},
|
|
389
527
|
];
|
|
528
|
+
/**
|
|
529
|
+
* Published operations this client deliberately does not wrap, with the
|
|
530
|
+
* reason. The drift check requires every remaining spec operation to be
|
|
531
|
+
* accounted for here, so "unsupported" is always a stated decision.
|
|
532
|
+
*/
|
|
533
|
+
export const UNSUPPORTED_OPERATIONS = [
|
|
534
|
+
{
|
|
535
|
+
method: "GET",
|
|
536
|
+
path: "/api/v1/mcp",
|
|
537
|
+
operationId: "openMcpEventStream",
|
|
538
|
+
reason: "The MCP endpoint offers no server-to-client stream: GET answers 405 by design, so a client that reconnects to it would loop (#16354). Post JSON-RPC with mcp() instead.",
|
|
539
|
+
},
|
|
540
|
+
];
|
|
541
|
+
/**
|
|
542
|
+
* The writes the API replays under `Idempotency-Key`: a second request with
|
|
543
|
+
* the same key and the same body returns the first result instead of
|
|
544
|
+
* repeating the write. Source of truth: the operations declaring the
|
|
545
|
+
* `Idempotency-Key` header parameter in `web/public/api/v1/openapi.json`,
|
|
546
|
+
* pinned by `scripts/check-sdk-openapi-drift.mjs` (#16182). Only these are
|
|
547
|
+
* retried on a transport or 5xx failure, and only when a key is set; a key on
|
|
548
|
+
* any other operation is refused before the request, since the server would
|
|
549
|
+
* ignore it and a retry could repeat the side effect (`verifyWebhook` sends
|
|
550
|
+
* a challenge to your URL each time; `submitTraderExport` starts a job).
|
|
551
|
+
*/
|
|
552
|
+
export const IDEMPOTENT_WRITE_OPERATIONS = [
|
|
553
|
+
"createWebhook",
|
|
554
|
+
"updateWebhook",
|
|
555
|
+
"deleteWebhook",
|
|
556
|
+
"rotateWebhookSecret",
|
|
557
|
+
"prepareWebhookSecret",
|
|
558
|
+
"activateWebhookSecret",
|
|
559
|
+
"retireWebhookSecret",
|
|
560
|
+
"redeliverWebhookDelivery",
|
|
561
|
+
];
|
|
562
|
+
/**
|
|
563
|
+
* POST operations that read and never write (#16182): the batch lookups.
|
|
564
|
+
* `POST /api/v1/traders/batch` and `POST /api/v1/markets/intel/batch` resolve
|
|
565
|
+
* their inputs and store nothing (`backend/src/api_v1/handlers/batch.rs`), so
|
|
566
|
+
* a repeat cannot duplicate a side effect. Each attempt is one request
|
|
567
|
+
* against the account's quota and one reservation of batch item units, which
|
|
568
|
+
* is exactly what a retried GET costs, so they are retried like a GET. Every
|
|
569
|
+
* other POST, PATCH or DELETE is retried only as a keyed write above.
|
|
570
|
+
*/
|
|
571
|
+
export const READ_ONLY_POST_OPERATIONS = [
|
|
572
|
+
"batchGetTraders",
|
|
573
|
+
"batchGetMarketIntel",
|
|
574
|
+
];
|
|
575
|
+
const idempotentWrites = new Set(IDEMPOTENT_WRITE_OPERATIONS);
|
|
576
|
+
const readOnlyPosts = new Set(READ_ONLY_POST_OPERATIONS);
|
|
577
|
+
/** The retry class of an operation; see `RetryEligibility`. */
|
|
578
|
+
export function retryEligibility(operation) {
|
|
579
|
+
if (operation.method === "GET" || readOnlyPosts.has(operation.operationId)) {
|
|
580
|
+
return "read";
|
|
581
|
+
}
|
|
582
|
+
return idempotentWrites.has(operation.operationId) ? "keyed" : "never";
|
|
583
|
+
}
|
|
390
584
|
/**
|
|
391
585
|
* Trader skill grade. Source of truth:
|
|
392
586
|
* `components.schemas.Trader.properties.grade.enum` (S highest, F lowest).
|
|
@@ -394,17 +588,28 @@ export const API_CLIENT_OPERATIONS = [
|
|
|
394
588
|
export const GRADES = ["S", "A", "B", "C", "D", "F"];
|
|
395
589
|
/** Default production API base used when `baseUrl` is omitted. */
|
|
396
590
|
export const DEFAULT_BASE_URL = "https://api.0xinsider.com";
|
|
591
|
+
/**
|
|
592
|
+
* The sandbox server, the second `servers` entry of the OpenAPI document: no
|
|
593
|
+
* credential, no production data, every documented operation answered with
|
|
594
|
+
* its example or a deterministic sample, `?sandbox_status=<code>` for a
|
|
595
|
+
* documented error, and `X-Oxi-Sandbox: true` on every response. The two
|
|
596
|
+
* Markdown documents answer `200 text/markdown` there and the export download
|
|
597
|
+
* answers its `302` to a sample file the sandbox serves itself. `GET
|
|
598
|
+
* /api/v1/stream` is the one operation it does not simulate and answers 400,
|
|
599
|
+
* because an SSE stream is a live connection rather than a body.
|
|
600
|
+
*/
|
|
601
|
+
export const SANDBOX_BASE_URL = "https://0xinsider.com/sandbox";
|
|
602
|
+
/** A live secret key starts with this; the sandbox never needs one. */
|
|
603
|
+
const LIVE_KEY_PREFIX = "oxi_sk_live_";
|
|
397
604
|
/** Default per-request deadline; see `ApiClientOptions.timeoutMs`. */
|
|
398
605
|
export const DEFAULT_TIMEOUT_MS = 15_000;
|
|
399
606
|
/** Default retry budget; see `ApiClientOptions.maxRetries`. */
|
|
400
607
|
export const DEFAULT_MAX_RETRIES = 2;
|
|
401
608
|
/** Statuses a retry can fix: rate limited, or a gateway/availability failure. */
|
|
402
|
-
const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
|
|
609
|
+
const RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);
|
|
403
610
|
const RETRY_BASE_DELAY_MS = 500;
|
|
404
611
|
const RETRY_MAX_BACKOFF_MS = 8_000;
|
|
405
612
|
const RETRY_JITTER_MS = 250;
|
|
406
|
-
/** A server-requested wait longer than this is the caller's to schedule. */
|
|
407
|
-
const RETRY_AFTER_CEILING_MS = 60_000;
|
|
408
613
|
function assertRetryCount(value, source) {
|
|
409
614
|
if (!Number.isInteger(value) || value < 0) {
|
|
410
615
|
throw new Error(`${source} must be a non-negative integer, got ${String(value)}`);
|
|
@@ -413,11 +618,14 @@ function assertRetryCount(value, source) {
|
|
|
413
618
|
}
|
|
414
619
|
/**
|
|
415
620
|
* Milliseconds to wait before retry `attempt` (0-based), or `null` when the
|
|
416
|
-
* server asked for a wait a retry loop should not hold.
|
|
621
|
+
* server asked for a wait a retry loop should not hold. The ceiling check
|
|
622
|
+
* runs on the server's number before jitter is added, so the value handed to
|
|
623
|
+
* the timer is at most `RETRY_AFTER_CEILING_MS + RETRY_JITTER_MS`, far inside
|
|
624
|
+
* the timer range (`MAX_TIMER_DELAY_MS`, `retry.ts`).
|
|
417
625
|
*/
|
|
418
626
|
function retryDelayMs(attempt, retryAfter) {
|
|
419
627
|
if (retryAfter !== null) {
|
|
420
|
-
const requested =
|
|
628
|
+
const requested = retryAfter * 1000;
|
|
421
629
|
if (requested > RETRY_AFTER_CEILING_MS)
|
|
422
630
|
return null;
|
|
423
631
|
return requested + Math.random() * RETRY_JITTER_MS;
|
|
@@ -425,24 +633,6 @@ function retryDelayMs(attempt, retryAfter) {
|
|
|
425
633
|
const ceiling = Math.min(RETRY_MAX_BACKOFF_MS, RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
426
634
|
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
427
635
|
}
|
|
428
|
-
/** Resolve after `ms`, or reject with the signal's reason the moment it aborts. */
|
|
429
|
-
function sleepUnlessAborted(ms, signal) {
|
|
430
|
-
return new Promise((resolve, reject) => {
|
|
431
|
-
if (signal?.aborted) {
|
|
432
|
-
reject(signal.reason);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const onAbort = () => {
|
|
436
|
-
clearTimeout(timer);
|
|
437
|
-
reject(signal?.reason);
|
|
438
|
-
};
|
|
439
|
-
const timer = setTimeout(() => {
|
|
440
|
-
signal?.removeEventListener("abort", onAbort);
|
|
441
|
-
resolve();
|
|
442
|
-
}, ms);
|
|
443
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
636
|
function isLoopbackHost(hostname) {
|
|
447
637
|
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
448
638
|
return (host === "localhost" ||
|
|
@@ -469,6 +659,28 @@ export function assertTrustedBaseUrl(baseUrl) {
|
|
|
469
659
|
return url;
|
|
470
660
|
throw new Error(`Refusing to send the API key to ${url.origin}: baseUrl must use https: (http: is accepted only for a loopback host such as localhost or 127.0.0.1).`);
|
|
471
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* The server URL as this client stores it: origin plus path, trailing slashes
|
|
664
|
+
* trimmed, and a trailing `/api/v1` removed once. Every operation path starts
|
|
665
|
+
* with `/api/v1/`, so a base that already ends in it would double the prefix;
|
|
666
|
+
* before #16138 such a base worked only because the path was discarded, and
|
|
667
|
+
* this keeps it working.
|
|
668
|
+
*/
|
|
669
|
+
function normalizeBaseUrl(base) {
|
|
670
|
+
const trimmed = base.toString().replace(/\/+$/, "");
|
|
671
|
+
return trimmed.replace(/\/api\/v1$/, "");
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Join an `/api/v1/...` path onto the server URL, keeping the server's own
|
|
675
|
+
* path (`/sandbox`) exactly once. REST (`buildUrl`) and the SSE stream
|
|
676
|
+
* (`stream.ts`) both resolve through this, so the two cannot disagree about
|
|
677
|
+
* where a path-bearing base points (#16138: `new URL("/api/v1/...", base)`
|
|
678
|
+
* resolved against the origin and dropped `/sandbox`).
|
|
679
|
+
*/
|
|
680
|
+
export function resolveApiUrl(baseUrl, path) {
|
|
681
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
682
|
+
return new URL(`${baseUrl}${suffix}`);
|
|
683
|
+
}
|
|
472
684
|
/**
|
|
473
685
|
* One signal that aborts on the deadline OR when the caller's signal aborts.
|
|
474
686
|
* Uses native composition where available. The manual fallback removes both
|
|
@@ -512,12 +724,137 @@ function composeRequestSignal(timeoutMs, caller) {
|
|
|
512
724
|
}
|
|
513
725
|
return { signal: controller.signal, dispose };
|
|
514
726
|
}
|
|
727
|
+
/**
|
|
728
|
+
* `response` with `dispose` deferred to the end of its body: the stream's
|
|
729
|
+
* close, error or cancellation (#16686). Without a `dispose` (a runtime with
|
|
730
|
+
* `AbortSignal.any`, or no composition) the response is returned as is.
|
|
731
|
+
*/
|
|
732
|
+
function releaseWhenBodySettles(response, dispose) {
|
|
733
|
+
if (!dispose)
|
|
734
|
+
return response;
|
|
735
|
+
if (!response.body) {
|
|
736
|
+
dispose();
|
|
737
|
+
return response;
|
|
738
|
+
}
|
|
739
|
+
const reader = response.body.getReader();
|
|
740
|
+
const body = new ReadableStream({
|
|
741
|
+
async pull(controller) {
|
|
742
|
+
try {
|
|
743
|
+
const { done, value } = await reader.read();
|
|
744
|
+
if (done) {
|
|
745
|
+
dispose();
|
|
746
|
+
controller.close();
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
controller.enqueue(value);
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
dispose();
|
|
753
|
+
controller.error(error);
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
cancel(reason) {
|
|
757
|
+
dispose();
|
|
758
|
+
return reader.cancel(reason);
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
return new Response(body, {
|
|
762
|
+
status: response.status,
|
|
763
|
+
statusText: response.statusText,
|
|
764
|
+
headers: response.headers,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
/** JSON-RPC methods `POST /api/v1/mcp` answers without a credential. */
|
|
768
|
+
function isKeylessMcpMethod(method) {
|
|
769
|
+
return (method === "initialize" ||
|
|
770
|
+
method === "ping" ||
|
|
771
|
+
method === "tools/list" ||
|
|
772
|
+
method.startsWith("notifications/"));
|
|
773
|
+
}
|
|
515
774
|
function isTimeoutAbort(error) {
|
|
516
775
|
return (typeof error === "object" &&
|
|
517
776
|
error !== null &&
|
|
518
777
|
"name" in error &&
|
|
519
778
|
error.name === "TimeoutError");
|
|
520
779
|
}
|
|
780
|
+
/** Which method reads each kind of success body, named in the error that refuses the wrong one. */
|
|
781
|
+
const READER_FOR_KIND = {
|
|
782
|
+
envelope: "call() or list()",
|
|
783
|
+
text: "text()",
|
|
784
|
+
jsonrpc: "mcp()",
|
|
785
|
+
sse: "streamFeed(), streamFeedResilient() or consumeStreamCheckpointed()",
|
|
786
|
+
};
|
|
787
|
+
// --- MCP JSON-RPC (#16137) ---
|
|
788
|
+
/** The MCP methods `POST /api/v1/mcp` accepts, from the operation's request body. */
|
|
789
|
+
export const MCP_METHODS = [
|
|
790
|
+
"initialize",
|
|
791
|
+
"notifications/initialized",
|
|
792
|
+
"notifications/cancelled",
|
|
793
|
+
"ping",
|
|
794
|
+
"tools/list",
|
|
795
|
+
"tools/call",
|
|
796
|
+
];
|
|
797
|
+
function isMcpJsonRpcResponse(body) {
|
|
798
|
+
if (typeof body !== "object" || body === null)
|
|
799
|
+
return false;
|
|
800
|
+
const candidate = body;
|
|
801
|
+
return (candidate.jsonrpc === "2.0" &&
|
|
802
|
+
(typeof candidate.id === "string" ||
|
|
803
|
+
typeof candidate.id === "number" ||
|
|
804
|
+
candidate.id === null));
|
|
805
|
+
}
|
|
806
|
+
// --- Export download (#16137) ---
|
|
807
|
+
/** The `downloadTraderExport` row of `REDIRECT_OPERATIONS`, as a request target. */
|
|
808
|
+
const exportDownloadOperation = {
|
|
809
|
+
method: REDIRECT_OPERATIONS[0].method,
|
|
810
|
+
path: REDIRECT_OPERATIONS[0].path,
|
|
811
|
+
operationId: REDIRECT_OPERATIONS[0].operationId,
|
|
812
|
+
auth: REDIRECT_OPERATIONS[0].auth,
|
|
813
|
+
};
|
|
814
|
+
/**
|
|
815
|
+
* Read a presigned URL's own expiry, and refuse a destination that would
|
|
816
|
+
* downgrade the transport. The backend already validates the redirect
|
|
817
|
+
* against its expected R2 origin; this is the client-side half, so a
|
|
818
|
+
* redirect can never move a download onto plain `http:`.
|
|
819
|
+
*/
|
|
820
|
+
function presignedTarget(location) {
|
|
821
|
+
let url;
|
|
822
|
+
try {
|
|
823
|
+
url = new URL(location);
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
throw new InvalidResponseError(302, `The export download redirect is not an absolute URL: ${location}`, null);
|
|
827
|
+
}
|
|
828
|
+
if (url.protocol !== "https:" && !isLoopbackHost(url.hostname)) {
|
|
829
|
+
throw new InvalidResponseError(302, `Refusing to follow the export download redirect to ${url.origin}: it must use https: (http: is accepted only for a loopback host).`, null);
|
|
830
|
+
}
|
|
831
|
+
const signedAt = url.searchParams.get("X-Amz-Date");
|
|
832
|
+
const lifetime = url.searchParams.get("X-Amz-Expires");
|
|
833
|
+
const expiresAt = presignExpiry(signedAt, lifetime);
|
|
834
|
+
return { url: url.toString(), ...(expiresAt ? { expiresAt } : {}) };
|
|
835
|
+
}
|
|
836
|
+
/** `20260922T101500Z` plus `3600` seconds, as an ISO instant; `null` if either is unreadable. */
|
|
837
|
+
function presignExpiry(signedAt, lifetimeSeconds) {
|
|
838
|
+
if (!signedAt || !lifetimeSeconds)
|
|
839
|
+
return null;
|
|
840
|
+
const parsed = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(signedAt);
|
|
841
|
+
const seconds = Number(lifetimeSeconds);
|
|
842
|
+
if (!parsed || !Number.isFinite(seconds))
|
|
843
|
+
return null;
|
|
844
|
+
const [, year, month, day, hour, minute, second] = parsed;
|
|
845
|
+
const signed = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
|
|
846
|
+
return new Date(signed + seconds * 1000).toISOString();
|
|
847
|
+
}
|
|
848
|
+
/** The `filename="..."` of a `Content-Disposition`, or `null`. */
|
|
849
|
+
function filenameFromDisposition(disposition) {
|
|
850
|
+
if (!disposition)
|
|
851
|
+
return null;
|
|
852
|
+
const quoted = /filename\*?=(?:UTF-8'')?"([^"]+)"/i.exec(disposition);
|
|
853
|
+
if (quoted)
|
|
854
|
+
return decodeURIComponent(quoted[1]);
|
|
855
|
+
const bare = /filename\*?=(?:UTF-8'')?([^;]+)/i.exec(disposition);
|
|
856
|
+
return bare ? decodeURIComponent(bare[1].trim()) : null;
|
|
857
|
+
}
|
|
521
858
|
/** `GET /api/v1/leaderboard` -> parameters -> strategy (`web/public/api/v1/openapi.json`). */
|
|
522
859
|
export const LEADERBOARD_STRATEGIES = [
|
|
523
860
|
"accumulator",
|
|
@@ -531,16 +868,48 @@ export const LEADERBOARD_STRATEGIES = [
|
|
|
531
868
|
"speculator",
|
|
532
869
|
"swing_trader",
|
|
533
870
|
];
|
|
871
|
+
/**
|
|
872
|
+
* Body-level `expand` values of `POST /api/v1/traders/batch`. The type is the
|
|
873
|
+
* operation's own body (`OperationBody["batchGetTraders"]["expand"]`); the
|
|
874
|
+
* const is the same list as a runtime value for a caller that iterates it.
|
|
875
|
+
*/
|
|
876
|
+
export const BATCH_TRADER_EXPANSIONS = [
|
|
877
|
+
"strategy",
|
|
878
|
+
"categories",
|
|
879
|
+
"quant_metrics",
|
|
880
|
+
"trust",
|
|
881
|
+
];
|
|
534
882
|
const operationsById = new Map(API_CLIENT_OPERATIONS.map((operation) => [operation.operationId, operation]));
|
|
535
883
|
export class OxinsiderApiClient {
|
|
536
884
|
baseUrl;
|
|
537
885
|
apiKey;
|
|
886
|
+
sandbox;
|
|
538
887
|
fetchImpl;
|
|
539
888
|
timeoutMs;
|
|
540
889
|
maxRetries;
|
|
890
|
+
/**
|
|
891
|
+
* A client for the sandbox server (#16138): `SANDBOX_BASE_URL`, no
|
|
892
|
+
* credential needed, example data only, never production data. The same
|
|
893
|
+
* as `new OxinsiderApiClient({ ...options, sandbox: true })`; pass a
|
|
894
|
+
* sandbox key (`oxi_sk_test_*`) as `apiKey` to have it checked.
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* const sandbox = OxinsiderApiClient.sandbox();
|
|
898
|
+
* const board = await sandbox.listLeaderboard({ limit: 5 }); // no key
|
|
899
|
+
* board.meta?.sandbox; // true
|
|
900
|
+
*/
|
|
901
|
+
static sandbox(options = {}) {
|
|
902
|
+
return new OxinsiderApiClient({ ...options, sandbox: true });
|
|
903
|
+
}
|
|
541
904
|
constructor(options = {}) {
|
|
542
|
-
|
|
543
|
-
|
|
905
|
+
this.sandbox = options.sandbox === true;
|
|
906
|
+
const base = assertTrustedBaseUrl(options.baseUrl ?? (this.sandbox ? SANDBOX_BASE_URL : DEFAULT_BASE_URL));
|
|
907
|
+
this.baseUrl = normalizeBaseUrl(base);
|
|
908
|
+
if (this.sandbox && options.apiKey?.startsWith(LIVE_KEY_PREFIX)) {
|
|
909
|
+
// The sandbox needs no credential and ignores a live key, so the only
|
|
910
|
+
// effect of sending one is a secret on the wire for nothing.
|
|
911
|
+
throw new Error("Refusing to send a live API key (oxi_sk_live_*) to the sandbox: omit apiKey, or pass a sandbox key (oxi_sk_test_*) from POST /api/v1/agents/register.");
|
|
912
|
+
}
|
|
544
913
|
this.apiKey = options.apiKey;
|
|
545
914
|
this.timeoutMs =
|
|
546
915
|
options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
|
|
@@ -552,29 +921,236 @@ export class OxinsiderApiClient {
|
|
|
552
921
|
// Bind so a passed-through `globalThis.fetch` keeps its receiver.
|
|
553
922
|
this.fetchImpl = fetchImpl.bind(globalThis);
|
|
554
923
|
}
|
|
924
|
+
// The implementation signature is what both overloads narrow; `unknown`
|
|
925
|
+
// here is the widest return both can refine, not a shape a caller sees.
|
|
926
|
+
async call(operationId, options = {}) {
|
|
927
|
+
const operation = this.resolveOperation(operationId, "envelope");
|
|
928
|
+
return this.request(operation, options, async (response) => {
|
|
929
|
+
if (response.status === 304) {
|
|
930
|
+
return notModifiedResponse(response);
|
|
931
|
+
}
|
|
932
|
+
const envelope = await parseJson(response);
|
|
933
|
+
if (isApiEnvelope(envelope)) {
|
|
934
|
+
const etag = response.headers.get("etag");
|
|
935
|
+
const sandbox = response.headers.get("x-oxi-sandbox") === "true";
|
|
936
|
+
const queryIgnored = response.headers.get("x-query-ignored");
|
|
937
|
+
const effectiveQuery = response.headers.get("x-effective-query");
|
|
938
|
+
// Only ADD to a meta the server actually sent. The contract makes
|
|
939
|
+
// `meta` required on every envelope, so synthesizing one from `?? {}`
|
|
940
|
+
// would hand a caller a `ResponseMeta` missing its required fields --
|
|
941
|
+
// which is what the hand-written type hid before #14278. `status` is
|
|
942
|
+
// always lifted (#16137): `201` and `202` are load-bearing on
|
|
943
|
+
// `registerAgent` and `submitTraderExport`, and a caller cannot read
|
|
944
|
+
// them off a body that looks identical to a 200.
|
|
945
|
+
if (envelope.meta) {
|
|
946
|
+
envelope.meta = {
|
|
947
|
+
...envelope.meta,
|
|
948
|
+
status: response.status,
|
|
949
|
+
...(etag ? { etag } : {}),
|
|
950
|
+
...(sandbox ? { sandbox: true } : {}),
|
|
951
|
+
...(queryIgnored ? { queryIgnored } : {}),
|
|
952
|
+
...(effectiveQuery ? { effectiveQuery } : {}),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
return envelope;
|
|
956
|
+
}
|
|
957
|
+
throw new OxinsiderApiError(response.status, envelope);
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
async text(operationId, options = {}) {
|
|
961
|
+
const operation = this.resolveOperation(operationId, "text");
|
|
962
|
+
return this.request(operation, options, async (response) => response.text(), { accept: "text/markdown, text/plain;q=0.9, */*;q=0.1" });
|
|
963
|
+
}
|
|
555
964
|
/**
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
965
|
+
* Post one MCP JSON-RPC 2.0 message to `POST /api/v1/mcp` (#16137).
|
|
966
|
+
*
|
|
967
|
+
* The endpoint answers a JSON-RPC envelope, not the `{ object, data, meta }`
|
|
968
|
+
* envelope every other operation uses, so `call()` refuses it. A request
|
|
969
|
+
* (one carrying `id`) comes back as `status: 200` with `response` set; a
|
|
970
|
+
* supported notification (no `id`) comes back as `status: 202` with
|
|
971
|
+
* `response: null`, which is the empty body the server sends. A JSON-RPC
|
|
972
|
+
* `error` member is a protocol-level failure and is RETURNED, not thrown:
|
|
973
|
+
* only a non-2xx HTTP status throws, because an unknown tool is an answer
|
|
974
|
+
* and a revoked credential is not.
|
|
975
|
+
*
|
|
976
|
+
* The request is never retried: `tools/call` can have an effect, and the
|
|
977
|
+
* endpoint honours no `Idempotency-Key`.
|
|
978
|
+
*
|
|
979
|
+
* @example
|
|
980
|
+
* const listed = await client.mcp({ method: "tools/list", id: 1 });
|
|
981
|
+
* listed.response?.result;
|
|
559
982
|
*/
|
|
560
|
-
async
|
|
983
|
+
async mcp(request, options = {}) {
|
|
984
|
+
// `initialize`, `ping`, `tools/list` and the notifications need no
|
|
985
|
+
// credential (`backend/src/mcp/mod.rs`); only `tools/call` does, so the
|
|
986
|
+
// local credential check is per method (#16686). A configured key is
|
|
987
|
+
// still sent on every method.
|
|
988
|
+
const operation = this.resolveOperation("createMcpJsonRpcResponse", "jsonrpc", !isKeylessMcpMethod(request.method));
|
|
989
|
+
const { sessionId, protocolVersion, ...transportOptions } = options;
|
|
990
|
+
const headers = { ...options.headers };
|
|
991
|
+
if (sessionId !== undefined) {
|
|
992
|
+
headers["mcp-session-id"] = sessionId;
|
|
993
|
+
}
|
|
994
|
+
if (protocolVersion !== undefined) {
|
|
995
|
+
headers["mcp-protocol-version"] = protocolVersion;
|
|
996
|
+
}
|
|
997
|
+
// Set after the spread: an explicit `jsonrpc: undefined` would otherwise
|
|
998
|
+
// drop the field and the server would answer 400 (#16686).
|
|
999
|
+
const body = { ...request, jsonrpc: "2.0" };
|
|
1000
|
+
return this.request(operation, { ...transportOptions, headers, body, maxRetries: 0 }, async (response) => {
|
|
1001
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
1002
|
+
const result = {
|
|
1003
|
+
status: response.status === 202 ? 202 : 200,
|
|
1004
|
+
response: null,
|
|
1005
|
+
...(sessionId ? { sessionId } : {}),
|
|
1006
|
+
};
|
|
1007
|
+
if (response.status === 202)
|
|
1008
|
+
return result;
|
|
1009
|
+
const parsed = await parseJson(response);
|
|
1010
|
+
if (!isMcpJsonRpcResponse(parsed)) {
|
|
1011
|
+
throw new InvalidResponseError(response.status, "POST /api/v1/mcp returned a body that is not a JSON-RPC 2.0 response: expected an object with jsonrpc \"2.0\" and an id.", parsed);
|
|
1012
|
+
}
|
|
1013
|
+
result.response = parsed;
|
|
1014
|
+
return result;
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Resolve the presigned object URL behind `GET /api/v1/trader/{address}/export/download`
|
|
1019
|
+
* without downloading anything (#16137).
|
|
1020
|
+
*
|
|
1021
|
+
* The route answers `302` with a `Location`, which `call()` treats as an
|
|
1022
|
+
* error. This reads the redirect target and stops there, so a caller can
|
|
1023
|
+
* hand the URL to a download manager or a browser. The returned URL is a
|
|
1024
|
+
* bearer credential in itself: anyone holding it can read the file until it
|
|
1025
|
+
* expires, so do not log it.
|
|
1026
|
+
*
|
|
1027
|
+
* Browser caveat: `fetch` with `redirect: "manual"` yields an opaque
|
|
1028
|
+
* redirect whose `Location` no script can read. The method says so rather
|
|
1029
|
+
* than guessing a URL; run the download from a server runtime.
|
|
1030
|
+
*/
|
|
1031
|
+
async getTraderExportDownloadUrl(address, jobId, options = {}) {
|
|
1032
|
+
const operation = exportDownloadOperation;
|
|
1033
|
+
if (!this.apiKey && !this.sandbox) {
|
|
1034
|
+
throw new Error("downloadTraderExport requires an API key (oxi_sk_*)");
|
|
1035
|
+
}
|
|
1036
|
+
return this.request(operation, { ...options, path: { address }, query: { job_id: jobId } }, async (response) => {
|
|
1037
|
+
if (response.type === "opaqueredirect" || response.status === 0) {
|
|
1038
|
+
throw new InvalidResponseError(302, "This runtime hides redirect targets: fetch with redirect: \"manual\" returned an opaque redirect, so the presigned download URL cannot be read. Run the export download from a server runtime (Node 18+, Deno, Bun, a Worker).", null);
|
|
1039
|
+
}
|
|
1040
|
+
const location = response.headers.get("location");
|
|
1041
|
+
if (!location) {
|
|
1042
|
+
throw new InvalidResponseError(response.status, "GET /api/v1/trader/{address}/export/download answered a redirect with no Location header.", null);
|
|
1043
|
+
}
|
|
1044
|
+
return presignedTarget(location);
|
|
1045
|
+
}, {
|
|
1046
|
+
// The 302 IS the success here, so it must not enter the error path.
|
|
1047
|
+
isSuccess: (response) => response.status === 302 || response.type === "opaqueredirect",
|
|
1048
|
+
redirect: "manual",
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Download a finished export (#16137): resolve the `302`, then fetch the
|
|
1053
|
+
* object store directly.
|
|
1054
|
+
*
|
|
1055
|
+
* The second request carries NO headers at all, so the live API key never
|
|
1056
|
+
* reaches the object store; the presigned URL is its own credential. The
|
|
1057
|
+
* body is not buffered -- read `response.body` as a stream and write it
|
|
1058
|
+
* where it belongs.
|
|
1059
|
+
*
|
|
1060
|
+
* There is no default deadline on the object fetch, the way `streamFeed`
|
|
1061
|
+
* has none: a multi-gigabyte export would fail the 15-second REST default
|
|
1062
|
+
* halfway through. Pass `signal` to cancel it, or `downloadTimeoutMs` for
|
|
1063
|
+
* a deadline of your own. `timeoutMs` still bounds the redirect request.
|
|
1064
|
+
*
|
|
1065
|
+
* @example
|
|
1066
|
+
* const { response, filename } = await client.downloadTraderExport(address, jobId);
|
|
1067
|
+
* await pipeline(Readable.fromWeb(response.body), createWriteStream(filename ?? "export.json"));
|
|
1068
|
+
*/
|
|
1069
|
+
async downloadTraderExport(address, jobId, options = {}) {
|
|
1070
|
+
const target = await this.getTraderExportDownloadUrl(address, jobId, options);
|
|
1071
|
+
const downloadSignal = composeRequestSignal(options.downloadTimeoutMs ?? null, options.signal);
|
|
1072
|
+
let response;
|
|
1073
|
+
try {
|
|
1074
|
+
// No headers, and no credentials: the presigned URL authorizes itself,
|
|
1075
|
+
// and forwarding the bearer would hand the live API key to the object
|
|
1076
|
+
// store on every download.
|
|
1077
|
+
response = await this.fetchImpl(target.url, {
|
|
1078
|
+
method: "GET",
|
|
1079
|
+
signal: downloadSignal.signal,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
catch (error) {
|
|
1083
|
+
downloadSignal.dispose?.();
|
|
1084
|
+
throw error;
|
|
1085
|
+
}
|
|
1086
|
+
if (!response.ok) {
|
|
1087
|
+
downloadSignal.dispose?.();
|
|
1088
|
+
throw new InvalidResponseError(response.status, `The presigned export URL answered ${response.status}. A presigned link is valid for about an hour; request a fresh one with getTraderExportDownloadUrl.`, null);
|
|
1089
|
+
}
|
|
1090
|
+
// The caller reads the body after this returns, so the composed signal
|
|
1091
|
+
// (caller abort plus `downloadTimeoutMs`) stays wired until the body ends
|
|
1092
|
+
// or is cancelled, not only until the headers arrive (#16686).
|
|
1093
|
+
response = releaseWhenBodySettles(response, downloadSignal.dispose);
|
|
1094
|
+
const contentLength = response.headers.get("content-length");
|
|
1095
|
+
return {
|
|
1096
|
+
...target,
|
|
1097
|
+
response,
|
|
1098
|
+
contentLength: contentLength === null ? null : Number.parseInt(contentLength, 10),
|
|
1099
|
+
contentType: response.headers.get("content-type"),
|
|
1100
|
+
filename: filenameFromDisposition(response.headers.get("content-disposition")),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Look up an operation and refuse it when the caller reached for the wrong
|
|
1105
|
+
* reader (#16137): `call()` on a Markdown route used to fail as a bad 200.
|
|
1106
|
+
*/
|
|
1107
|
+
resolveOperation(operationId, expected, requireCredential = true) {
|
|
561
1108
|
const operation = operationsById.get(operationId);
|
|
562
1109
|
if (!operation) {
|
|
563
1110
|
throw new Error(`Unknown 0xinsider API operation: ${operationId}`);
|
|
564
1111
|
}
|
|
565
|
-
|
|
1112
|
+
const kind = operation.kind ?? "envelope";
|
|
1113
|
+
if (kind !== expected) {
|
|
1114
|
+
throw new Error(`${operationId} answers a "${kind}" body, not "${expected}": use ${READER_FOR_KIND[kind]}.`);
|
|
1115
|
+
}
|
|
1116
|
+
// The sandbox answers every operation without a credential; production
|
|
1117
|
+
// does not, and the local check saves a round trip that can only be 401.
|
|
1118
|
+
if (requireCredential && operation.auth === "bearer" && !this.apiKey && !this.sandbox) {
|
|
566
1119
|
throw new Error(`${operationId} requires an API key (oxi_sk_*)`);
|
|
567
1120
|
}
|
|
1121
|
+
return operation;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* One request pipeline for every transport: URL and headers, the retry
|
|
1125
|
+
* policy, one deadline shared by every attempt, the typed error hierarchy,
|
|
1126
|
+
* and cancellation held live through body consumption. `consume` runs
|
|
1127
|
+
* inside that window, so a slow body is still covered by the deadline and
|
|
1128
|
+
* by the caller's `signal`.
|
|
1129
|
+
*/
|
|
1130
|
+
async request(operation, options, consume, transport = {}) {
|
|
1131
|
+
const operationId = operation.operationId;
|
|
568
1132
|
const timeoutMs = options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs;
|
|
569
1133
|
const maxRetries = options.maxRetries === undefined
|
|
570
1134
|
? this.maxRetries
|
|
571
1135
|
: assertRetryCount(options.maxRetries, "maxRetries");
|
|
572
1136
|
const url = this.buildUrl(operation, options);
|
|
573
|
-
const headers = this.buildHeaders(operation, options);
|
|
1137
|
+
const headers = this.buildHeaders(operation, options, transport.accept);
|
|
574
1138
|
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
1139
|
+
const isSuccess = transport.isSuccess ??
|
|
1140
|
+
((response) => response.ok || response.status === 304);
|
|
1141
|
+
// A key the server does not read is a false promise of safety: the
|
|
1142
|
+
// request would be retried as though replayable while the route repeats
|
|
1143
|
+
// its effect (#16182). Refuse it here, before anything is sent.
|
|
1144
|
+
const eligibility = retryEligibility(operation);
|
|
1145
|
+
const keyed = headers.has("idempotency-key");
|
|
1146
|
+
if (keyed && eligibility !== "keyed") {
|
|
1147
|
+
throw new Error(`${operationId} does not honour Idempotency-Key; the API replays only ${IDEMPOTENT_WRITE_OPERATIONS.join(", ")}. Remove idempotencyKey: a retry of this request could repeat its side effect.`);
|
|
1148
|
+
}
|
|
575
1149
|
// Replaying a write without a key could repeat it (a second webhook
|
|
576
|
-
// endpoint, say), so only a
|
|
577
|
-
|
|
1150
|
+
// endpoint, say), so only a read or a keyed write is retried (#14281).
|
|
1151
|
+
// `headers` and `body` are built once, above, so every attempt carries
|
|
1152
|
+
// the same key and the same bytes.
|
|
1153
|
+
const retryable = eligibility === "read" || (eligibility === "keyed" && keyed);
|
|
578
1154
|
// One deadline for every attempt: a retry that cannot finish before it is
|
|
579
1155
|
// not started, so the caller sees the real failure, not a timeout.
|
|
580
1156
|
const deadlineAt = timeoutMs === null ? null : Date.now() + timeoutMs;
|
|
@@ -598,6 +1174,7 @@ export class OxinsiderApiClient {
|
|
|
598
1174
|
headers,
|
|
599
1175
|
body,
|
|
600
1176
|
signal: requestSignal.signal,
|
|
1177
|
+
...(transport.redirect ? { redirect: transport.redirect } : {}),
|
|
601
1178
|
});
|
|
602
1179
|
}
|
|
603
1180
|
catch (error) {
|
|
@@ -610,7 +1187,7 @@ export class OxinsiderApiClient {
|
|
|
610
1187
|
await sleepUnlessAborted(delay, requestSignal.signal);
|
|
611
1188
|
continue;
|
|
612
1189
|
}
|
|
613
|
-
if (response
|
|
1190
|
+
if (isSuccess(response))
|
|
614
1191
|
break;
|
|
615
1192
|
const errorBody = await parseJson(response);
|
|
616
1193
|
const retryAfter = retryAfterSeconds(response);
|
|
@@ -622,22 +1199,7 @@ export class OxinsiderApiClient {
|
|
|
622
1199
|
}
|
|
623
1200
|
await sleepUnlessAborted(delay, requestSignal.signal);
|
|
624
1201
|
}
|
|
625
|
-
|
|
626
|
-
return notModifiedResponse(response);
|
|
627
|
-
}
|
|
628
|
-
const envelope = await parseJson(response);
|
|
629
|
-
if (isApiEnvelope(envelope)) {
|
|
630
|
-
const etag = response.headers.get("etag");
|
|
631
|
-
// Only ADD to a meta the server actually sent. The contract makes
|
|
632
|
-
// `meta` required on every envelope, so synthesizing one from `?? {}`
|
|
633
|
-
// would hand a caller a `ResponseMeta` missing its required fields --
|
|
634
|
-
// which is what the hand-written type hid before #14278.
|
|
635
|
-
if (etag && envelope.meta) {
|
|
636
|
-
envelope.meta = { ...envelope.meta, etag };
|
|
637
|
-
}
|
|
638
|
-
return envelope;
|
|
639
|
-
}
|
|
640
|
-
throw new OxinsiderApiError(response.status, envelope);
|
|
1202
|
+
return await consume(response);
|
|
641
1203
|
}
|
|
642
1204
|
catch (error) {
|
|
643
1205
|
if (timeoutMs !== null &&
|
|
@@ -655,66 +1217,151 @@ export class OxinsiderApiClient {
|
|
|
655
1217
|
requestSignal.dispose?.();
|
|
656
1218
|
}
|
|
657
1219
|
}
|
|
658
|
-
/**
|
|
659
|
-
* Execute a Stripe-style list operation and return the list envelope.
|
|
660
|
-
* Use `paginate()` (in `pagination.ts`) to auto-follow `next_cursor`.
|
|
661
|
-
*/
|
|
662
1220
|
async list(operationId, options = {}) {
|
|
663
1221
|
const result = await this.call(operationId, options);
|
|
664
1222
|
if (isNotModified(result)) {
|
|
665
1223
|
throw new OxinsiderApiError(304, "list() received a 304 not_modified; use call() if you send If-None-Match on a list endpoint");
|
|
666
1224
|
}
|
|
667
1225
|
if (!isListEnvelope(result)) {
|
|
668
|
-
throw new
|
|
1226
|
+
throw new InvalidResponseError(200, `Operation ${operationId} did not return a list envelope: expected object "list", an array data, a boolean has_more and a string or absent next_cursor`, result);
|
|
669
1227
|
}
|
|
670
1228
|
return result;
|
|
671
1229
|
}
|
|
672
1230
|
// --- Typed convenience methods (key surfaces) ---
|
|
1231
|
+
//
|
|
1232
|
+
// Each takes the route's path parameters positionally, its documented
|
|
1233
|
+
// query as `params` (list reads) or `options.query` (single reads), and
|
|
1234
|
+
// the shared transport options: `signal`, `timeoutMs`, `maxRetries`,
|
|
1235
|
+
// `headers` (`If-None-Match` for a conditional read) and `idempotencyKey`
|
|
1236
|
+
// on a keyed write. The result is the operation's own envelope; no method
|
|
1237
|
+
// takes a type argument any more (#16136).
|
|
673
1238
|
getTrader(address, options = {}) {
|
|
674
1239
|
return this.call("getTrader", { ...options, path: { address } });
|
|
675
1240
|
}
|
|
676
1241
|
getTraderPnl(address, options = {}) {
|
|
677
1242
|
return this.call("getTraderPnl", { ...options, path: { address } });
|
|
678
1243
|
}
|
|
679
|
-
|
|
680
|
-
|
|
1244
|
+
/**
|
|
1245
|
+
* Fetch one wallet's win record per canonical category.
|
|
1246
|
+
*
|
|
1247
|
+
* Counts every settled market at any position size, so it can differ from
|
|
1248
|
+
* `category_strengths` on `getTrader`, which reads the floored calibration
|
|
1249
|
+
* sample. A category under `min_decided_for_win_rate` keeps its `wins` and
|
|
1250
|
+
* `decided` with `win_rate: null` and `status: "not_enough_data"`; a category
|
|
1251
|
+
* the wallet has no settled market in is absent, which means no record rather
|
|
1252
|
+
* than a 0% record. The `Esports` record carries `games`: the wallet's record
|
|
1253
|
+
* per esports title (`LoL`, `CS2`, `Dota 2`, ...) under the same rule, named
|
|
1254
|
+
* as the holder chips name them in `category_win_rate_game`. Pass
|
|
1255
|
+
* `params.category` to filter to one bucket; a filter that reaches Esports
|
|
1256
|
+
* returns its games too.
|
|
1257
|
+
*/
|
|
1258
|
+
getTraderCategoryRecords(address, params = {}, options = {}) {
|
|
1259
|
+
return this.call("getTraderCategoryRecords", {
|
|
681
1260
|
...options,
|
|
682
|
-
|
|
1261
|
+
path: { address },
|
|
1262
|
+
query: params,
|
|
683
1263
|
});
|
|
684
1264
|
}
|
|
685
|
-
|
|
686
|
-
|
|
1265
|
+
/**
|
|
1266
|
+
* Resolve 1 to 25 traders in one request (`POST /api/v1/traders/batch`).
|
|
1267
|
+
*
|
|
1268
|
+
* `traders` is sent as the request body's `traders` array, the field the
|
|
1269
|
+
* route requires: wallet addresses, usernames, `trd_` ids or integer trader
|
|
1270
|
+
* ids, resolved in input order. Until #16135 this method sent the array as
|
|
1271
|
+
* `identifiers`, a field the route does not declare, so every call was
|
|
1272
|
+
* refused for a missing `traders`. `options.expand` is sent as the body's
|
|
1273
|
+
* `expand` array and applies to every item.
|
|
1274
|
+
*
|
|
1275
|
+
* The response `data` keeps request order and one row per input, duplicates
|
|
1276
|
+
* included; a row is `status: "ok"` with `data`, or `status: "error"` with
|
|
1277
|
+
* the item's own `error` (an unknown identity is a per-item `not_found`, not
|
|
1278
|
+
* a request failure). `meta` is the batch's `BatchResponseMeta`:
|
|
1279
|
+
* `request_cost` and `rate_limit` follow the batch item quota, not the
|
|
1280
|
+
* per-request one.
|
|
1281
|
+
*/
|
|
1282
|
+
batchGetTraders(traders, options = {}) {
|
|
1283
|
+
const { expand, ...request } = options;
|
|
1284
|
+
return this.call("batchGetTraders", {
|
|
1285
|
+
...request,
|
|
1286
|
+
body: expand === undefined ? { traders } : { traders, expand },
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Resolve up to 25 markets' intel in one request
|
|
1291
|
+
* (`POST /api/v1/markets/intel/batch`); `meta` is the batch's own.
|
|
1292
|
+
*/
|
|
1293
|
+
batchGetMarketIntel(body, options = {}) {
|
|
1294
|
+
return this.call("batchGetMarketIntel", { ...options, body });
|
|
1295
|
+
}
|
|
1296
|
+
listLeaderboard(params = {}, options = {}) {
|
|
1297
|
+
return this.list("listLeaderboard", { ...options, query: params });
|
|
687
1298
|
}
|
|
688
|
-
listTrendingWallets(params = {}) {
|
|
689
|
-
return this.list("listTrendingWallets", { query: params });
|
|
1299
|
+
listTrendingWallets(params = {}, options = {}) {
|
|
1300
|
+
return this.list("listTrendingWallets", { ...options, query: params });
|
|
690
1301
|
}
|
|
691
|
-
|
|
692
|
-
return this.list("
|
|
1302
|
+
listLargeTrades(params = {}, options = {}) {
|
|
1303
|
+
return this.list("listLargeTrades", { ...options, query: params });
|
|
693
1304
|
}
|
|
694
|
-
|
|
695
|
-
return this.list("
|
|
1305
|
+
listLargeTradeHistory(params = {}, options = {}) {
|
|
1306
|
+
return this.list("listLargeTradeHistory", { ...options, query: params });
|
|
696
1307
|
}
|
|
697
|
-
|
|
698
|
-
|
|
1308
|
+
/** @deprecated Use {@link listLargeTrades} (#16304); calls the deprecated `/api/v1/whale-trades`. */
|
|
1309
|
+
listWhaleTrades(params = {}, options = {}) {
|
|
1310
|
+
return this.list("listWhaleTrades", { ...options, query: params });
|
|
699
1311
|
}
|
|
700
|
-
|
|
701
|
-
|
|
1312
|
+
/** @deprecated Use {@link listLargeTradeHistory} (#16304); calls the deprecated `/api/v1/whale-trades/history`. */
|
|
1313
|
+
listWhaleTradeHistory(params = {}, options = {}) {
|
|
1314
|
+
return this.list("listWhaleTradeHistory", { ...options, query: params });
|
|
702
1315
|
}
|
|
703
|
-
|
|
704
|
-
return this.list("
|
|
1316
|
+
listPositions(params = {}, options = {}) {
|
|
1317
|
+
return this.list("listPositions", { ...options, query: params });
|
|
1318
|
+
}
|
|
1319
|
+
listLargePositions(params = {}, options = {}) {
|
|
1320
|
+
return this.list("listLargePositions", { ...options, query: params });
|
|
1321
|
+
}
|
|
1322
|
+
listSmartMoneyFlows(params = {}, options = {}) {
|
|
1323
|
+
return this.list("listSmartMoneyFlows", { ...options, query: params });
|
|
705
1324
|
}
|
|
706
1325
|
/** Canonical alias of {@link listSmartMoneyFlows} (epic #6912). */
|
|
707
|
-
listSharpMoneyFlows(params = {}) {
|
|
708
|
-
return this.list("listSharpMoneyFlows", { query: params });
|
|
1326
|
+
listSharpMoneyFlows(params = {}, options = {}) {
|
|
1327
|
+
return this.list("listSharpMoneyFlows", { ...options, query: params });
|
|
709
1328
|
}
|
|
710
1329
|
/**
|
|
711
|
-
* List
|
|
712
|
-
*
|
|
1330
|
+
* List covered games: both sides with their provider ids and live scores, the
|
|
1331
|
+
* UTC kickoff, the provider's own status, and every linked Polymarket market
|
|
1332
|
+
* with its condition id and outcome token ids. Ordered by kickoff, then by
|
|
1333
|
+
* `event_slug`, with unscheduled games last. An unknown `sport` or `status`
|
|
1334
|
+
* returns an empty page rather than an error, and the response's `coverage`
|
|
1335
|
+
* names what this deployment serves.
|
|
713
1336
|
*/
|
|
714
|
-
|
|
715
|
-
return this.list("
|
|
716
|
-
|
|
717
|
-
|
|
1337
|
+
listGames(params = {}, options = {}) {
|
|
1338
|
+
return this.list("listGames", { ...options, query: params });
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Read one game by its `event_slug`, the identity the `live_sports_updated`
|
|
1342
|
+
* webhook pulse carries. A slug outside the published coverage returns 404.
|
|
1343
|
+
*/
|
|
1344
|
+
getGame(eventSlug, options = {}) {
|
|
1345
|
+
return this.call("getGame", { ...options, path: { event_slug: eventSlug } });
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* List upcoming games ranked by the side profitable wallets hold, with
|
|
1349
|
+
* additive, shadow-only category evidence. `category_skill` never changes
|
|
1350
|
+
* membership, ordering, or sizing. Rows carry `side`, `ranked_at`,
|
|
1351
|
+
* `backing_score` and `side_share`; the older `piled_side`,
|
|
1352
|
+
* `signal_created_at`, `conviction_score` and `smart_score` keys carry the
|
|
1353
|
+
* same values and stay on the wire.
|
|
1354
|
+
*/
|
|
1355
|
+
listPreGameSides(params = {}, options = {}) {
|
|
1356
|
+
return this.list("listPreGameSides", { ...options, query: params });
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* @deprecated Use {@link listPreGameSides} (#16310). Kept live; it calls the
|
|
1360
|
+
* deprecated `/api/v1/sports-edge-signals` path, which answers with
|
|
1361
|
+
* `Deprecation` and successor `Link` headers and the same body.
|
|
1362
|
+
*/
|
|
1363
|
+
listSportsEdgeSignals(params = {}, options = {}) {
|
|
1364
|
+
return this.list("listSportsEdgeSignals", { ...options, query: params });
|
|
718
1365
|
}
|
|
719
1366
|
/**
|
|
720
1367
|
* Read an explicitly observation-only sports cohort and its accountable
|
|
@@ -725,8 +1372,23 @@ export class OxinsiderApiClient {
|
|
|
725
1372
|
* additive wider-holder projection, not an arrival-history or independent
|
|
726
1373
|
* denominator view.
|
|
727
1374
|
*/
|
|
1375
|
+
async listPreGameSideObservations(params, options = {}) {
|
|
1376
|
+
const response = await this.list("listPreGameSideObservations", {
|
|
1377
|
+
...options,
|
|
1378
|
+
query: params,
|
|
1379
|
+
});
|
|
1380
|
+
return sportsEdgeObservationsResponse(response);
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* @deprecated Use {@link listPreGameSideObservations} (#16310). Kept live; it
|
|
1384
|
+
* calls the deprecated `/api/v1/sports-edge-observations` path, which answers
|
|
1385
|
+
* with `Deprecation` and successor `Link` headers and the same body.
|
|
1386
|
+
*/
|
|
728
1387
|
async listSportsEdgeObservations(params, options = {}) {
|
|
729
|
-
const response = await this.list("listSportsEdgeObservations", {
|
|
1388
|
+
const response = await this.list("listSportsEdgeObservations", {
|
|
1389
|
+
...options,
|
|
1390
|
+
query: params,
|
|
1391
|
+
});
|
|
730
1392
|
return sportsEdgeObservationsResponse(response);
|
|
731
1393
|
}
|
|
732
1394
|
/**
|
|
@@ -738,24 +1400,40 @@ export class OxinsiderApiClient {
|
|
|
738
1400
|
* projection cutoff inside `next_cursor` is excluded while its stable page
|
|
739
1401
|
* position remains covered.
|
|
740
1402
|
*/
|
|
741
|
-
async
|
|
742
|
-
const response = await this.call("
|
|
1403
|
+
async listPreGameSideObservationsConditional(params, options = {}) {
|
|
1404
|
+
const response = await this.call("listPreGameSideObservations", {
|
|
1405
|
+
...options,
|
|
1406
|
+
query: params,
|
|
1407
|
+
});
|
|
743
1408
|
if (isNotModified(response)) {
|
|
744
1409
|
return response;
|
|
745
1410
|
}
|
|
746
|
-
if (!isListEnvelope(response)) {
|
|
747
|
-
throw new OxinsiderApiError(200, "listSportsEdgeObservations did not return a list envelope");
|
|
748
|
-
}
|
|
749
1411
|
return sportsEdgeObservationsResponse(response);
|
|
750
1412
|
}
|
|
751
|
-
|
|
752
|
-
|
|
1413
|
+
/**
|
|
1414
|
+
* @deprecated Use {@link listPreGameSideObservationsConditional} (#16310).
|
|
1415
|
+
* Kept live on the deprecated `/api/v1/sports-edge-observations` path.
|
|
1416
|
+
*/
|
|
1417
|
+
async listSportsEdgeObservationsConditional(params, options = {}) {
|
|
1418
|
+
const response = await this.call("listSportsEdgeObservations", {
|
|
753
1419
|
...options,
|
|
754
|
-
query:
|
|
1420
|
+
query: params,
|
|
755
1421
|
});
|
|
1422
|
+
if (isNotModified(response)) {
|
|
1423
|
+
return response;
|
|
1424
|
+
}
|
|
1425
|
+
return sportsEdgeObservationsResponse(response);
|
|
1426
|
+
}
|
|
1427
|
+
searchMarkets(q, options = {}) {
|
|
1428
|
+
const { query, ...request } = options;
|
|
1429
|
+
return this.list("searchMarkets", { ...request, query: { ...query, q } });
|
|
1430
|
+
}
|
|
1431
|
+
searchContent(q, options = {}) {
|
|
1432
|
+
const { query, ...request } = options;
|
|
1433
|
+
return this.list("searchContent", { ...request, query: { ...query, q } });
|
|
756
1434
|
}
|
|
757
|
-
exploreMarkets(params = {}) {
|
|
758
|
-
return this.list("exploreMarkets", { query: params });
|
|
1435
|
+
exploreMarkets(params = {}, options = {}) {
|
|
1436
|
+
return this.list("exploreMarkets", { ...options, query: params });
|
|
759
1437
|
}
|
|
760
1438
|
getMarketIntel(conditionId, options = {}) {
|
|
761
1439
|
return this.call("getMarketIntel", {
|
|
@@ -769,37 +1447,98 @@ export class OxinsiderApiClient {
|
|
|
769
1447
|
path: { condition_id: conditionId },
|
|
770
1448
|
});
|
|
771
1449
|
}
|
|
1450
|
+
/**
|
|
1451
|
+
* One page of a market's graded (S/A/B) holder roster from a complete
|
|
1452
|
+
* provider holder scan: the list a Pick of the Day shows, for any market.
|
|
1453
|
+
* `params.outcome` (`yes` | `no` | `all`), `params.min_grade` (`S` | `A` |
|
|
1454
|
+
* `B`), `params.limit` and `params.cursor` (`mh_` prefix). The page carries
|
|
1455
|
+
* the `market`, `scan` and roster `totals` beside `data`; `total` is the
|
|
1456
|
+
* count matching the filters across every page.
|
|
1457
|
+
*/
|
|
1458
|
+
getMarketHolders(conditionId, params = {}, options = {}) {
|
|
1459
|
+
return this.list("getMarketHolders", {
|
|
1460
|
+
...options,
|
|
1461
|
+
path: { condition_id: conditionId },
|
|
1462
|
+
query: params,
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
772
1465
|
/**
|
|
773
1466
|
* Fetch bucketed OHLC candles for a market's outcome tokens.
|
|
774
|
-
* Resolution defaults to the server default; pass `
|
|
775
|
-
* `"1d"` or `"1w"` to override.
|
|
1467
|
+
* Resolution defaults to the server default; pass `params.resolution` as
|
|
1468
|
+
* `"1d"` or `"1w"` to override. `params.from` is exclusive and `params.to`
|
|
1469
|
+
* is inclusive; when both are present, `from` must be less than or equal to
|
|
1470
|
+
* `to`.
|
|
776
1471
|
*/
|
|
777
|
-
getMarketCandles(conditionId, options = {}) {
|
|
1472
|
+
getMarketCandles(conditionId, params = {}, options = {}) {
|
|
778
1473
|
return this.call("getMarketCandles", {
|
|
779
1474
|
...options,
|
|
780
1475
|
path: { condition_id: conditionId },
|
|
1476
|
+
query: params,
|
|
781
1477
|
});
|
|
782
1478
|
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Fetch one suspicious trade by raw `whale_alerts.id` or the `rf_`-prefixed
|
|
1481
|
+
* id list responses emit. The envelope's `object` is `"suspicious_trade"`.
|
|
1482
|
+
*/
|
|
1483
|
+
getSuspiciousTrade(id, options = {}) {
|
|
1484
|
+
return this.call("getSuspiciousTrade", { ...options, path: { id } });
|
|
1485
|
+
}
|
|
1486
|
+
/** List stored trades whose recorded suspicion score meets the flag threshold. */
|
|
1487
|
+
listSuspiciousTrades(params = {}, options = {}) {
|
|
1488
|
+
return this.list("listSuspiciousTrades", { ...options, query: params });
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* @deprecated Use `getSuspiciousTrade()`; renamed in #16301. This method
|
|
1492
|
+
* keeps calling the deprecated `GET /api/v1/insider-radar/{id}`, which stays
|
|
1493
|
+
* live with no retirement date and still answers `object: "radar_flag"`, so
|
|
1494
|
+
* an integration branching on that envelope keeps working.
|
|
1495
|
+
*/
|
|
783
1496
|
getInsiderRadarFlag(id, options = {}) {
|
|
784
1497
|
return this.call("getInsiderRadarFlag", { ...options, path: { id } });
|
|
785
1498
|
}
|
|
786
|
-
|
|
787
|
-
|
|
1499
|
+
/**
|
|
1500
|
+
* @deprecated Use `listSuspiciousTrades()`; renamed in #16301. This method
|
|
1501
|
+
* keeps calling the deprecated `GET /api/v1/insider-radar`, which stays live
|
|
1502
|
+
* with no retirement date and answers with `Deprecation` plus a `Link
|
|
1503
|
+
* rel="successor-version"` header.
|
|
1504
|
+
*/
|
|
1505
|
+
listInsiderRadar(params = {}, options = {}) {
|
|
1506
|
+
return this.list("listInsiderRadar", { ...options, query: params });
|
|
1507
|
+
}
|
|
1508
|
+
getLargeTrade(id, options = {}) {
|
|
1509
|
+
return this.call("getLargeTrade", { ...options, path: { id } });
|
|
1510
|
+
}
|
|
1511
|
+
/** @deprecated Use {@link getLargeTrade} (#16304); calls the deprecated `/api/v1/whale-trades/{id}`. */
|
|
1512
|
+
getWhaleTrade(id, options = {}) {
|
|
1513
|
+
return this.call("getWhaleTrade", { ...options, path: { id } });
|
|
788
1514
|
}
|
|
789
1515
|
// --- Webhooks ---
|
|
790
1516
|
listWebhooks(options = {}) {
|
|
791
|
-
return this.
|
|
1517
|
+
return this.list("listWebhooks", options);
|
|
1518
|
+
}
|
|
1519
|
+
getWebhook(id, options = {}) {
|
|
1520
|
+
return this.call("getWebhook", { ...options, path: { id } });
|
|
1521
|
+
}
|
|
1522
|
+
createWebhook(body, options = {}) {
|
|
1523
|
+
return this.call("createWebhook", { ...options, body });
|
|
1524
|
+
}
|
|
1525
|
+
updateWebhook(id, body, options = {}) {
|
|
1526
|
+
return this.call("updateWebhook", { ...options, path: { id }, body });
|
|
1527
|
+
}
|
|
1528
|
+
deleteWebhook(id, options = {}) {
|
|
1529
|
+
return this.call("deleteWebhook", { ...options, path: { id } });
|
|
792
1530
|
}
|
|
793
1531
|
/**
|
|
794
1532
|
* Fetch the self-describing webhook event catalog (each entry's `id`,
|
|
795
1533
|
* description, payload shape, and active/dormant status).
|
|
796
1534
|
*/
|
|
797
1535
|
listWebhookEvents(options = {}) {
|
|
798
|
-
return this.
|
|
1536
|
+
return this.list("listWebhookEvents", options);
|
|
799
1537
|
}
|
|
800
1538
|
/** List delivery attempts for one webhook endpoint (Stripe-style list). */
|
|
801
|
-
listWebhookDeliveries(webhookId, params = {}) {
|
|
1539
|
+
listWebhookDeliveries(webhookId, params = {}, options = {}) {
|
|
802
1540
|
return this.list("listWebhookDeliveries", {
|
|
1541
|
+
...options,
|
|
803
1542
|
path: { id: webhookId },
|
|
804
1543
|
query: params,
|
|
805
1544
|
});
|
|
@@ -825,9 +1564,44 @@ export class OxinsiderApiClient {
|
|
|
825
1564
|
getApiDiscovery(options = {}) {
|
|
826
1565
|
return this.call("getApiDiscovery", options);
|
|
827
1566
|
}
|
|
1567
|
+
getPlatforms(options = {}) {
|
|
1568
|
+
return this.call("getPlatforms", options);
|
|
1569
|
+
}
|
|
828
1570
|
getAccountIdentity(options = {}) {
|
|
829
1571
|
return this.call("getAccountIdentity", options);
|
|
830
1572
|
}
|
|
1573
|
+
/**
|
|
1574
|
+
* One wallet's context as Markdown, ready to paste into a model prompt
|
|
1575
|
+
* (`GET /api/v1/trader/{address}/context.md`). The JSON form of the same
|
|
1576
|
+
* read is `getTraderContext`.
|
|
1577
|
+
*/
|
|
1578
|
+
getTraderContextMarkdown(address, options = {}) {
|
|
1579
|
+
return this.text("getTraderContextMarkdown", { ...options, path: { address } });
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* One market's context as Markdown
|
|
1583
|
+
* (`GET /api/v1/market/{condition_id}/context.md`). The JSON form is
|
|
1584
|
+
* `getMarketIntel`.
|
|
1585
|
+
*/
|
|
1586
|
+
getMarketContextMarkdown(conditionId, options = {}) {
|
|
1587
|
+
return this.text("getMarketContextMarkdown", {
|
|
1588
|
+
...options,
|
|
1589
|
+
path: { condition_id: conditionId },
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Mint a sandbox key (`POST /api/v1/agents/register`), the one write that
|
|
1594
|
+
* needs no credential. Answers `201`, which is why the SDK had no method
|
|
1595
|
+
* for it until #16137: the drift gate counted only operations with a
|
|
1596
|
+
* documented `200`. `meta.status` is `201`; `data.api_key` is the
|
|
1597
|
+
* `oxi_sk_test_*` key to pass to `OxinsiderApiClient.sandbox()`.
|
|
1598
|
+
*
|
|
1599
|
+
* Nothing is stored: the key cannot be listed or revoked and does not
|
|
1600
|
+
* expire. Register again for another one.
|
|
1601
|
+
*/
|
|
1602
|
+
registerAgent(options = {}) {
|
|
1603
|
+
return this.call("registerAgent", options);
|
|
1604
|
+
}
|
|
831
1605
|
getUsage(options = {}) {
|
|
832
1606
|
return this.call("getUsage", options);
|
|
833
1607
|
}
|
|
@@ -862,12 +1636,16 @@ export class OxinsiderApiClient {
|
|
|
862
1636
|
getApiKey() {
|
|
863
1637
|
return this.apiKey;
|
|
864
1638
|
}
|
|
1639
|
+
/** Whether this client was built in sandbox mode (#16138). */
|
|
1640
|
+
isSandbox() {
|
|
1641
|
+
return this.sandbox;
|
|
1642
|
+
}
|
|
865
1643
|
/** The resolved fetch implementation (used by the SSE stream consumer). */
|
|
866
1644
|
getFetch() {
|
|
867
1645
|
return this.fetchImpl;
|
|
868
1646
|
}
|
|
869
1647
|
buildUrl(operation, options) {
|
|
870
|
-
const url =
|
|
1648
|
+
const url = resolveApiUrl(this.baseUrl, interpolatePath(operation.path, options.path ?? {}));
|
|
871
1649
|
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
872
1650
|
if (value === null || value === undefined) {
|
|
873
1651
|
continue;
|
|
@@ -883,9 +1661,9 @@ export class OxinsiderApiClient {
|
|
|
883
1661
|
}
|
|
884
1662
|
return url.toString();
|
|
885
1663
|
}
|
|
886
|
-
buildHeaders(operation, options) {
|
|
1664
|
+
buildHeaders(operation, options, accept = "application/json") {
|
|
887
1665
|
const headers = new Headers(options.headers);
|
|
888
|
-
headers.set("accept",
|
|
1666
|
+
headers.set("accept", accept);
|
|
889
1667
|
if (options.body !== undefined && !headers.has("content-type")) {
|
|
890
1668
|
headers.set("content-type", "application/json");
|
|
891
1669
|
}
|
|
@@ -895,9 +1673,18 @@ export class OxinsiderApiClient {
|
|
|
895
1673
|
if (options.idempotencyKey !== undefined) {
|
|
896
1674
|
headers.set("idempotency-key", options.idempotencyKey);
|
|
897
1675
|
}
|
|
1676
|
+
if (options.strictQuery) {
|
|
1677
|
+
headers.set("x-query-validation", "strict");
|
|
1678
|
+
}
|
|
898
1679
|
return headers;
|
|
899
1680
|
}
|
|
900
1681
|
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Narrow a `call()` result to the typed 304. Takes any envelope-shaped
|
|
1684
|
+
* value, so it works on an `OperationResult<K>` (whose `meta` is the
|
|
1685
|
+
* operation's own type) as well as the loose `ApiClientResponse<T>`
|
|
1686
|
+
* (#16136).
|
|
1687
|
+
*/
|
|
901
1688
|
export function isApiNotModifiedResponse(response) {
|
|
902
1689
|
return response.object === "not_modified";
|
|
903
1690
|
}
|
|
@@ -925,13 +1712,6 @@ async function parseJson(response) {
|
|
|
925
1712
|
return text;
|
|
926
1713
|
}
|
|
927
1714
|
}
|
|
928
|
-
function retryAfterSeconds(response) {
|
|
929
|
-
const header = response.headers.get("retry-after");
|
|
930
|
-
if (!header)
|
|
931
|
-
return null;
|
|
932
|
-
const seconds = Number(header);
|
|
933
|
-
return Number.isFinite(seconds) ? seconds : null;
|
|
934
|
-
}
|
|
935
1715
|
function notModifiedResponse(response) {
|
|
936
1716
|
const meta = { status: 304 };
|
|
937
1717
|
const etag = response.headers.get("etag");
|
|
@@ -942,6 +1722,14 @@ function notModifiedResponse(response) {
|
|
|
942
1722
|
if (requestId) {
|
|
943
1723
|
meta.request_id = requestId;
|
|
944
1724
|
}
|
|
1725
|
+
const queryIgnored = response.headers.get("x-query-ignored");
|
|
1726
|
+
if (queryIgnored) {
|
|
1727
|
+
meta.queryIgnored = queryIgnored;
|
|
1728
|
+
}
|
|
1729
|
+
const effectiveQuery = response.headers.get("x-effective-query");
|
|
1730
|
+
if (effectiveQuery) {
|
|
1731
|
+
meta.effectiveQuery = effectiveQuery;
|
|
1732
|
+
}
|
|
945
1733
|
return { object: "not_modified", data: null, meta };
|
|
946
1734
|
}
|
|
947
1735
|
function isApiEnvelope(body) {
|
|
@@ -950,14 +1738,23 @@ function isApiEnvelope(body) {
|
|
|
950
1738
|
"object" in body &&
|
|
951
1739
|
"data" in body);
|
|
952
1740
|
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Whether `body` is a list envelope whose control fields the walker can act
|
|
1743
|
+
* on: `object: "list"`, an array `data`, a BOOLEAN `has_more`, and a
|
|
1744
|
+
* `next_cursor` that is a string, `null` or absent (#16246: the check used to
|
|
1745
|
+
* accept any `has_more` that was merely present, so a malformed value could
|
|
1746
|
+
* be coerced into "no more pages" or "more pages" by truthiness).
|
|
1747
|
+
*/
|
|
953
1748
|
export function isListEnvelope(body) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1749
|
+
if (typeof body !== "object" || body === null)
|
|
1750
|
+
return false;
|
|
1751
|
+
const candidate = body;
|
|
1752
|
+
return (candidate.object === "list" &&
|
|
1753
|
+
Array.isArray(candidate.data) &&
|
|
1754
|
+
typeof candidate.has_more === "boolean" &&
|
|
1755
|
+
(candidate.next_cursor === undefined ||
|
|
1756
|
+
candidate.next_cursor === null ||
|
|
1757
|
+
typeof candidate.next_cursor === "string"));
|
|
961
1758
|
}
|
|
962
1759
|
function sportsEdgeObservationsResponse(response) {
|
|
963
1760
|
const candidate = response;
|