@0xinsider/mcp 2.0.1 → 2.1.3

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 (100) hide show
  1. package/README.md +27 -7
  2. package/dist/catalog.generated.d.ts +4 -0
  3. package/dist/catalog.generated.d.ts.map +1 -1
  4. package/dist/catalog.generated.js +5 -0
  5. package/dist/catalog.generated.js.map +1 -1
  6. package/dist/cli-lib.d.ts +1 -0
  7. package/dist/cli-lib.d.ts.map +1 -1
  8. package/dist/cli-lib.js +48 -8
  9. package/dist/cli-lib.js.map +1 -1
  10. package/dist/client.d.ts +36 -2
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +42 -4
  13. package/dist/client.js.map +1 -1
  14. package/dist/constants.d.ts +1 -1
  15. package/dist/constants.js +1 -1
  16. package/dist/product-cli/client.d.ts +3 -2
  17. package/dist/product-cli/client.d.ts.map +1 -1
  18. package/dist/product-cli/client.js.map +1 -1
  19. package/dist/sdk/client.d.ts +705 -131
  20. package/dist/sdk/client.d.ts.map +1 -1
  21. package/dist/sdk/client.js +719 -125
  22. package/dist/sdk/client.js.map +1 -1
  23. package/dist/sdk/errors.d.ts +42 -4
  24. package/dist/sdk/errors.d.ts.map +1 -1
  25. package/dist/sdk/errors.js +103 -2
  26. package/dist/sdk/errors.js.map +1 -1
  27. package/dist/sdk/index.d.ts +9 -8
  28. package/dist/sdk/index.d.ts.map +1 -1
  29. package/dist/sdk/index.js +6 -4
  30. package/dist/sdk/index.js.map +1 -1
  31. package/dist/sdk/pagination.d.ts +133 -15
  32. package/dist/sdk/pagination.d.ts.map +1 -1
  33. package/dist/sdk/pagination.js +132 -34
  34. package/dist/sdk/pagination.js.map +1 -1
  35. package/dist/sdk/retry.d.ts +68 -0
  36. package/dist/sdk/retry.d.ts.map +1 -0
  37. package/dist/sdk/retry.js +125 -0
  38. package/dist/sdk/retry.js.map +1 -0
  39. package/dist/sdk/schema.d.ts +1536 -430
  40. package/dist/sdk/schema.d.ts.map +1 -1
  41. package/dist/sdk/stream.d.ts +318 -8
  42. package/dist/sdk/stream.d.ts.map +1 -1
  43. package/dist/sdk/stream.js +503 -56
  44. package/dist/sdk/stream.js.map +1 -1
  45. package/dist/sdk/webhooks.d.ts +18 -3
  46. package/dist/sdk/webhooks.d.ts.map +1 -1
  47. package/dist/sdk/webhooks.js +13 -1
  48. package/dist/sdk/webhooks.js.map +1 -1
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +2 -1
  51. package/dist/server.js.map +1 -1
  52. package/dist/tools/explore-markets.d.ts.map +1 -1
  53. package/dist/tools/explore-markets.js +2 -5
  54. package/dist/tools/explore-markets.js.map +1 -1
  55. package/dist/tools/get-insider-radar.d.ts.map +1 -1
  56. package/dist/tools/get-insider-radar.js +3 -10
  57. package/dist/tools/get-insider-radar.js.map +1 -1
  58. package/dist/tools/get-large-positions.d.ts.map +1 -1
  59. package/dist/tools/get-large-positions.js +3 -10
  60. package/dist/tools/get-large-positions.js.map +1 -1
  61. package/dist/tools/get-leaderboard.d.ts.map +1 -1
  62. package/dist/tools/get-leaderboard.js +3 -10
  63. package/dist/tools/get-leaderboard.js.map +1 -1
  64. package/dist/tools/get-market-intel.d.ts.map +1 -1
  65. package/dist/tools/get-market-intel.js +3 -6
  66. package/dist/tools/get-market-intel.js.map +1 -1
  67. package/dist/tools/get-position-timeline.d.ts.map +1 -1
  68. package/dist/tools/get-position-timeline.js +3 -10
  69. package/dist/tools/get-position-timeline.js.map +1 -1
  70. package/dist/tools/get-positions.d.ts +2 -0
  71. package/dist/tools/get-positions.d.ts.map +1 -1
  72. package/dist/tools/get-positions.js +8 -11
  73. package/dist/tools/get-positions.js.map +1 -1
  74. package/dist/tools/get-trader-pnl.d.ts.map +1 -1
  75. package/dist/tools/get-trader-pnl.js +3 -6
  76. package/dist/tools/get-trader-pnl.js.map +1 -1
  77. package/dist/tools/get-trader.d.ts.map +1 -1
  78. package/dist/tools/get-trader.js +3 -6
  79. package/dist/tools/get-trader.js.map +1 -1
  80. package/dist/tools/get-trending-wallets.d.ts.map +1 -1
  81. package/dist/tools/get-trending-wallets.js +3 -10
  82. package/dist/tools/get-trending-wallets.js.map +1 -1
  83. package/dist/tools/get-whale-trades.d.ts.map +1 -1
  84. package/dist/tools/get-whale-trades.js +4 -11
  85. package/dist/tools/get-whale-trades.js.map +1 -1
  86. package/dist/tools/pick-of-the-day.d.ts.map +1 -1
  87. package/dist/tools/pick-of-the-day.js +5 -21
  88. package/dist/tools/pick-of-the-day.js.map +1 -1
  89. package/dist/tools/read-parity.d.ts +1 -0
  90. package/dist/tools/read-parity.d.ts.map +1 -1
  91. package/dist/tools/read-parity.js +69 -44
  92. package/dist/tools/read-parity.js.map +1 -1
  93. package/dist/tools/result.d.ts +31 -0
  94. package/dist/tools/result.d.ts.map +1 -0
  95. package/dist/tools/result.js +70 -0
  96. package/dist/tools/result.js.map +1 -0
  97. package/dist/tools/search-markets.d.ts.map +1 -1
  98. package/dist/tools/search-markets.js +16 -11
  99. package/dist/tools/search-markets.js.map +1 -1
  100. package/package.json +4 -4
@@ -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 that returns a
25
- * `200`. Kept in lockstep with `web/public/api/v1/openapi.json` by
26
- * `scripts/check-sdk-openapi-drift.mjs`. Do not add a row without a matching
27
- * spec operation, and do not remove a spec operation without removing its row.
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",
@@ -200,6 +212,12 @@ export const API_CLIENT_OPERATIONS = [
200
212
  operationId: "getPlatforms",
201
213
  auth: "none",
202
214
  },
215
+ {
216
+ method: "GET",
217
+ path: "/api/v1/market/{condition_id}/holders",
218
+ operationId: "getMarketHolders",
219
+ auth: "bearer",
220
+ },
203
221
  {
204
222
  method: "GET",
205
223
  path: "/api/v1/market/{condition_id}/intel",
@@ -223,6 +241,7 @@ export const API_CLIENT_OPERATIONS = [
223
241
  path: "/api/v1/market/{condition_id}/context.md",
224
242
  operationId: "getMarketContextMarkdown",
225
243
  auth: "bearer",
244
+ kind: "text",
226
245
  },
227
246
  {
228
247
  method: "GET",
@@ -253,6 +272,7 @@ export const API_CLIENT_OPERATIONS = [
253
272
  path: "/api/v1/stream",
254
273
  operationId: "getStream",
255
274
  auth: "bearer",
275
+ kind: "sse",
256
276
  },
257
277
  {
258
278
  method: "GET",
@@ -296,6 +316,24 @@ export const API_CLIENT_OPERATIONS = [
296
316
  operationId: "rotateWebhookSecret",
297
317
  auth: "bearer",
298
318
  },
319
+ {
320
+ method: "POST",
321
+ path: "/api/v1/webhooks/{id}/rotate-secret/prepare",
322
+ operationId: "prepareWebhookSecret",
323
+ auth: "bearer",
324
+ },
325
+ {
326
+ method: "POST",
327
+ path: "/api/v1/webhooks/{id}/rotate-secret/activate",
328
+ operationId: "activateWebhookSecret",
329
+ auth: "bearer",
330
+ },
331
+ {
332
+ method: "POST",
333
+ path: "/api/v1/webhooks/{id}/rotate-secret/retire",
334
+ operationId: "retireWebhookSecret",
335
+ auth: "bearer",
336
+ },
299
337
  {
300
338
  method: "GET",
301
339
  path: "/api/v1/webhooks/events",
@@ -325,12 +363,7 @@ export const API_CLIENT_OPERATIONS = [
325
363
  path: "/api/v1/mcp",
326
364
  operationId: "createMcpJsonRpcResponse",
327
365
  auth: "bearer",
328
- },
329
- {
330
- method: "GET",
331
- path: "/api/v1/mcp",
332
- operationId: "openMcpEventStream",
333
- auth: "bearer",
366
+ kind: "jsonrpc",
334
367
  },
335
368
  {
336
369
  method: "GET",
@@ -380,7 +413,104 @@ export const API_CLIENT_OPERATIONS = [
380
413
  operationId: "getPickOfTheDayArchive",
381
414
  auth: "bearer",
382
415
  },
416
+ {
417
+ method: "GET",
418
+ path: "/api/v1/pick-of-the-day/ledger",
419
+ operationId: "getPickOfTheDayLedger",
420
+ // Keyless since #16459: the commitment ledger is published to be
421
+ // republished, so a client can read it with no key configured.
422
+ auth: "none",
423
+ },
424
+ {
425
+ method: "POST",
426
+ path: "/api/v1/agents/register",
427
+ operationId: "registerAgent",
428
+ // `security: []` in the document: minting a sandbox key is the one write
429
+ // that must work before a caller has any credential.
430
+ auth: "none",
431
+ },
432
+ ];
433
+ /**
434
+ * Published operations whose success is a redirect, with the method that
435
+ * follows it (#16137). These have no 2xx body, so they are absent from
436
+ * `API_CLIENT_OPERATIONS` and from the generated `schema.ts`;
437
+ * `scripts/check-sdk-openapi-drift.mjs` requires every redirect-only spec
438
+ * operation to be declared here, so one cannot go missing in silence again.
439
+ */
440
+ export const REDIRECT_OPERATIONS = [
441
+ {
442
+ method: "GET",
443
+ path: "/api/v1/trader/{address}/export/download",
444
+ operationId: "downloadTraderExport",
445
+ auth: "bearer",
446
+ status: 302,
447
+ handledBy: "getTraderExportDownloadUrl() resolves the Location; downloadTraderExport() then fetches the object with no Authorization header.",
448
+ },
449
+ {
450
+ method: "GET",
451
+ path: "/api/v1/openapi.json",
452
+ operationId: "redirectApiOpenapiSpec",
453
+ auth: "none",
454
+ status: 307,
455
+ 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.",
456
+ },
457
+ ];
458
+ /**
459
+ * Published operations this client deliberately does not wrap, with the
460
+ * reason. The drift check requires every remaining spec operation to be
461
+ * accounted for here, so "unsupported" is always a stated decision.
462
+ */
463
+ export const UNSUPPORTED_OPERATIONS = [
464
+ {
465
+ method: "GET",
466
+ path: "/api/v1/mcp",
467
+ operationId: "openMcpEventStream",
468
+ 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.",
469
+ },
470
+ ];
471
+ /**
472
+ * The writes the API replays under `Idempotency-Key`: a second request with
473
+ * the same key and the same body returns the first result instead of
474
+ * repeating the write. Source of truth: the operations declaring the
475
+ * `Idempotency-Key` header parameter in `web/public/api/v1/openapi.json`,
476
+ * pinned by `scripts/check-sdk-openapi-drift.mjs` (#16182). Only these are
477
+ * retried on a transport or 5xx failure, and only when a key is set; a key on
478
+ * any other operation is refused before the request, since the server would
479
+ * ignore it and a retry could repeat the side effect (`verifyWebhook` sends
480
+ * a challenge to your URL each time; `submitTraderExport` starts a job).
481
+ */
482
+ export const IDEMPOTENT_WRITE_OPERATIONS = [
483
+ "createWebhook",
484
+ "updateWebhook",
485
+ "deleteWebhook",
486
+ "rotateWebhookSecret",
487
+ "prepareWebhookSecret",
488
+ "activateWebhookSecret",
489
+ "retireWebhookSecret",
490
+ "redeliverWebhookDelivery",
383
491
  ];
492
+ /**
493
+ * POST operations that read and never write (#16182): the batch lookups.
494
+ * `POST /api/v1/traders/batch` and `POST /api/v1/markets/intel/batch` resolve
495
+ * their inputs and store nothing (`backend/src/api_v1/handlers/batch.rs`), so
496
+ * a repeat cannot duplicate a side effect. Each attempt is one request
497
+ * against the account's quota and one reservation of batch item units, which
498
+ * is exactly what a retried GET costs, so they are retried like a GET. Every
499
+ * other POST, PATCH or DELETE is retried only as a keyed write above.
500
+ */
501
+ export const READ_ONLY_POST_OPERATIONS = [
502
+ "batchGetTraders",
503
+ "batchGetMarketIntel",
504
+ ];
505
+ const idempotentWrites = new Set(IDEMPOTENT_WRITE_OPERATIONS);
506
+ const readOnlyPosts = new Set(READ_ONLY_POST_OPERATIONS);
507
+ /** The retry class of an operation; see `RetryEligibility`. */
508
+ export function retryEligibility(operation) {
509
+ if (operation.method === "GET" || readOnlyPosts.has(operation.operationId)) {
510
+ return "read";
511
+ }
512
+ return idempotentWrites.has(operation.operationId) ? "keyed" : "never";
513
+ }
384
514
  /**
385
515
  * Trader skill grade. Source of truth:
386
516
  * `components.schemas.Trader.properties.grade.enum` (S highest, F lowest).
@@ -388,17 +518,25 @@ export const API_CLIENT_OPERATIONS = [
388
518
  export const GRADES = ["S", "A", "B", "C", "D", "F"];
389
519
  /** Default production API base used when `baseUrl` is omitted. */
390
520
  export const DEFAULT_BASE_URL = "https://api.0xinsider.com";
521
+ /**
522
+ * The sandbox server, the second `servers` entry of the OpenAPI document: no
523
+ * credential, no production data, every documented operation answered with
524
+ * its example or a deterministic sample, `?sandbox_status=<code>` for a
525
+ * documented error, and `X-Oxi-Sandbox: true` on every response. Streams and
526
+ * file downloads are not simulated there and answer 400.
527
+ */
528
+ export const SANDBOX_BASE_URL = "https://0xinsider.com/sandbox";
529
+ /** A live secret key starts with this; the sandbox never needs one. */
530
+ const LIVE_KEY_PREFIX = "oxi_sk_live_";
391
531
  /** Default per-request deadline; see `ApiClientOptions.timeoutMs`. */
392
532
  export const DEFAULT_TIMEOUT_MS = 15_000;
393
533
  /** Default retry budget; see `ApiClientOptions.maxRetries`. */
394
534
  export const DEFAULT_MAX_RETRIES = 2;
395
535
  /** Statuses a retry can fix: rate limited, or a gateway/availability failure. */
396
- const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
536
+ const RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);
397
537
  const RETRY_BASE_DELAY_MS = 500;
398
538
  const RETRY_MAX_BACKOFF_MS = 8_000;
399
539
  const RETRY_JITTER_MS = 250;
400
- /** A server-requested wait longer than this is the caller's to schedule. */
401
- const RETRY_AFTER_CEILING_MS = 60_000;
402
540
  function assertRetryCount(value, source) {
403
541
  if (!Number.isInteger(value) || value < 0) {
404
542
  throw new Error(`${source} must be a non-negative integer, got ${String(value)}`);
@@ -407,11 +545,14 @@ function assertRetryCount(value, source) {
407
545
  }
408
546
  /**
409
547
  * Milliseconds to wait before retry `attempt` (0-based), or `null` when the
410
- * server asked for a wait a retry loop should not hold.
548
+ * server asked for a wait a retry loop should not hold. The ceiling check
549
+ * runs on the server's number before jitter is added, so the value handed to
550
+ * the timer is at most `RETRY_AFTER_CEILING_MS + RETRY_JITTER_MS`, far inside
551
+ * the timer range (`MAX_TIMER_DELAY_MS`, `retry.ts`).
411
552
  */
412
553
  function retryDelayMs(attempt, retryAfter) {
413
554
  if (retryAfter !== null) {
414
- const requested = Math.max(0, retryAfter * 1000);
555
+ const requested = retryAfter * 1000;
415
556
  if (requested > RETRY_AFTER_CEILING_MS)
416
557
  return null;
417
558
  return requested + Math.random() * RETRY_JITTER_MS;
@@ -419,24 +560,6 @@ function retryDelayMs(attempt, retryAfter) {
419
560
  const ceiling = Math.min(RETRY_MAX_BACKOFF_MS, RETRY_BASE_DELAY_MS * 2 ** attempt);
420
561
  return ceiling / 2 + Math.random() * (ceiling / 2);
421
562
  }
422
- /** Resolve after `ms`, or reject with the signal's reason the moment it aborts. */
423
- function sleepUnlessAborted(ms, signal) {
424
- return new Promise((resolve, reject) => {
425
- if (signal?.aborted) {
426
- reject(signal.reason);
427
- return;
428
- }
429
- const onAbort = () => {
430
- clearTimeout(timer);
431
- reject(signal?.reason);
432
- };
433
- const timer = setTimeout(() => {
434
- signal?.removeEventListener("abort", onAbort);
435
- resolve();
436
- }, ms);
437
- signal?.addEventListener("abort", onAbort, { once: true });
438
- });
439
- }
440
563
  function isLoopbackHost(hostname) {
441
564
  const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
442
565
  return (host === "localhost" ||
@@ -463,6 +586,28 @@ export function assertTrustedBaseUrl(baseUrl) {
463
586
  return url;
464
587
  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).`);
465
588
  }
589
+ /**
590
+ * The server URL as this client stores it: origin plus path, trailing slashes
591
+ * trimmed, and a trailing `/api/v1` removed once. Every operation path starts
592
+ * with `/api/v1/`, so a base that already ends in it would double the prefix;
593
+ * before #16138 such a base worked only because the path was discarded, and
594
+ * this keeps it working.
595
+ */
596
+ function normalizeBaseUrl(base) {
597
+ const trimmed = base.toString().replace(/\/+$/, "");
598
+ return trimmed.replace(/\/api\/v1$/, "");
599
+ }
600
+ /**
601
+ * Join an `/api/v1/...` path onto the server URL, keeping the server's own
602
+ * path (`/sandbox`) exactly once. REST (`buildUrl`) and the SSE stream
603
+ * (`stream.ts`) both resolve through this, so the two cannot disagree about
604
+ * where a path-bearing base points (#16138: `new URL("/api/v1/...", base)`
605
+ * resolved against the origin and dropped `/sandbox`).
606
+ */
607
+ export function resolveApiUrl(baseUrl, path) {
608
+ const suffix = path.startsWith("/") ? path : `/${path}`;
609
+ return new URL(`${baseUrl}${suffix}`);
610
+ }
466
611
  /**
467
612
  * One signal that aborts on the deadline OR when the caller's signal aborts.
468
613
  * Uses native composition where available. The manual fallback removes both
@@ -512,6 +657,84 @@ function isTimeoutAbort(error) {
512
657
  "name" in error &&
513
658
  error.name === "TimeoutError");
514
659
  }
660
+ /** Which method reads each kind of success body, named in the error that refuses the wrong one. */
661
+ const READER_FOR_KIND = {
662
+ envelope: "call() or list()",
663
+ text: "text()",
664
+ jsonrpc: "mcp()",
665
+ sse: "streamFeed(), streamFeedResilient() or consumeStreamCheckpointed()",
666
+ };
667
+ // --- MCP JSON-RPC (#16137) ---
668
+ /** The MCP methods `POST /api/v1/mcp` accepts, from the operation's request body. */
669
+ export const MCP_METHODS = [
670
+ "initialize",
671
+ "notifications/initialized",
672
+ "notifications/cancelled",
673
+ "ping",
674
+ "tools/list",
675
+ "tools/call",
676
+ ];
677
+ function isMcpJsonRpcResponse(body) {
678
+ if (typeof body !== "object" || body === null)
679
+ return false;
680
+ const candidate = body;
681
+ return (candidate.jsonrpc === "2.0" &&
682
+ (typeof candidate.id === "string" ||
683
+ typeof candidate.id === "number" ||
684
+ candidate.id === null));
685
+ }
686
+ // --- Export download (#16137) ---
687
+ /** The `downloadTraderExport` row of `REDIRECT_OPERATIONS`, as a request target. */
688
+ const exportDownloadOperation = {
689
+ method: REDIRECT_OPERATIONS[0].method,
690
+ path: REDIRECT_OPERATIONS[0].path,
691
+ operationId: REDIRECT_OPERATIONS[0].operationId,
692
+ auth: REDIRECT_OPERATIONS[0].auth,
693
+ };
694
+ /**
695
+ * Read a presigned URL's own expiry, and refuse a destination that would
696
+ * downgrade the transport. The backend already validates the redirect
697
+ * against its expected R2 origin; this is the client-side half, so a
698
+ * redirect can never move a download onto plain `http:`.
699
+ */
700
+ function presignedTarget(location) {
701
+ let url;
702
+ try {
703
+ url = new URL(location);
704
+ }
705
+ catch {
706
+ throw new InvalidResponseError(302, `The export download redirect is not an absolute URL: ${location}`, null);
707
+ }
708
+ if (url.protocol !== "https:" && !isLoopbackHost(url.hostname)) {
709
+ 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);
710
+ }
711
+ const signedAt = url.searchParams.get("X-Amz-Date");
712
+ const lifetime = url.searchParams.get("X-Amz-Expires");
713
+ const expiresAt = presignExpiry(signedAt, lifetime);
714
+ return { url: url.toString(), ...(expiresAt ? { expiresAt } : {}) };
715
+ }
716
+ /** `20260922T101500Z` plus `3600` seconds, as an ISO instant; `null` if either is unreadable. */
717
+ function presignExpiry(signedAt, lifetimeSeconds) {
718
+ if (!signedAt || !lifetimeSeconds)
719
+ return null;
720
+ const parsed = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(signedAt);
721
+ const seconds = Number(lifetimeSeconds);
722
+ if (!parsed || !Number.isFinite(seconds))
723
+ return null;
724
+ const [, year, month, day, hour, minute, second] = parsed;
725
+ const signed = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
726
+ return new Date(signed + seconds * 1000).toISOString();
727
+ }
728
+ /** The `filename="..."` of a `Content-Disposition`, or `null`. */
729
+ function filenameFromDisposition(disposition) {
730
+ if (!disposition)
731
+ return null;
732
+ const quoted = /filename\*?=(?:UTF-8'')?"([^"]+)"/i.exec(disposition);
733
+ if (quoted)
734
+ return decodeURIComponent(quoted[1]);
735
+ const bare = /filename\*?=(?:UTF-8'')?([^;]+)/i.exec(disposition);
736
+ return bare ? decodeURIComponent(bare[1].trim()) : null;
737
+ }
515
738
  /** `GET /api/v1/leaderboard` -> parameters -> strategy (`web/public/api/v1/openapi.json`). */
516
739
  export const LEADERBOARD_STRATEGIES = [
517
740
  "accumulator",
@@ -525,16 +748,48 @@ export const LEADERBOARD_STRATEGIES = [
525
748
  "speculator",
526
749
  "swing_trader",
527
750
  ];
751
+ /**
752
+ * Body-level `expand` values of `POST /api/v1/traders/batch`. The type is the
753
+ * operation's own body (`OperationBody["batchGetTraders"]["expand"]`); the
754
+ * const is the same list as a runtime value for a caller that iterates it.
755
+ */
756
+ export const BATCH_TRADER_EXPANSIONS = [
757
+ "strategy",
758
+ "categories",
759
+ "quant_metrics",
760
+ "trust",
761
+ ];
528
762
  const operationsById = new Map(API_CLIENT_OPERATIONS.map((operation) => [operation.operationId, operation]));
529
763
  export class OxinsiderApiClient {
530
764
  baseUrl;
531
765
  apiKey;
766
+ sandbox;
532
767
  fetchImpl;
533
768
  timeoutMs;
534
769
  maxRetries;
770
+ /**
771
+ * A client for the sandbox server (#16138): `SANDBOX_BASE_URL`, no
772
+ * credential needed, example data only, never production data. The same
773
+ * as `new OxinsiderApiClient({ ...options, sandbox: true })`; pass a
774
+ * sandbox key (`oxi_sk_test_*`) as `apiKey` to have it checked.
775
+ *
776
+ * @example
777
+ * const sandbox = OxinsiderApiClient.sandbox();
778
+ * const board = await sandbox.listLeaderboard({ limit: 5 }); // no key
779
+ * board.meta?.sandbox; // true
780
+ */
781
+ static sandbox(options = {}) {
782
+ return new OxinsiderApiClient({ ...options, sandbox: true });
783
+ }
535
784
  constructor(options = {}) {
536
- const base = assertTrustedBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
537
- this.baseUrl = base.toString().replace(/\/+$/, "");
785
+ this.sandbox = options.sandbox === true;
786
+ const base = assertTrustedBaseUrl(options.baseUrl ?? (this.sandbox ? SANDBOX_BASE_URL : DEFAULT_BASE_URL));
787
+ this.baseUrl = normalizeBaseUrl(base);
788
+ if (this.sandbox && options.apiKey?.startsWith(LIVE_KEY_PREFIX)) {
789
+ // The sandbox needs no credential and ignores a live key, so the only
790
+ // effect of sending one is a secret on the wire for nothing.
791
+ 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.");
792
+ }
538
793
  this.apiKey = options.apiKey;
539
794
  this.timeoutMs =
540
795
  options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
@@ -546,29 +801,224 @@ export class OxinsiderApiClient {
546
801
  // Bind so a passed-through `globalThis.fetch` keeps its receiver.
547
802
  this.fetchImpl = fetchImpl.bind(globalThis);
548
803
  }
804
+ // The implementation signature is what both overloads narrow; `unknown`
805
+ // here is the widest return both can refine, not a shape a caller sees.
806
+ async call(operationId, options = {}) {
807
+ const operation = this.resolveOperation(operationId, "envelope");
808
+ return this.request(operation, options, async (response) => {
809
+ if (response.status === 304) {
810
+ return notModifiedResponse(response);
811
+ }
812
+ const envelope = await parseJson(response);
813
+ if (isApiEnvelope(envelope)) {
814
+ const etag = response.headers.get("etag");
815
+ const sandbox = response.headers.get("x-oxi-sandbox") === "true";
816
+ const queryIgnored = response.headers.get("x-query-ignored");
817
+ const effectiveQuery = response.headers.get("x-effective-query");
818
+ // Only ADD to a meta the server actually sent. The contract makes
819
+ // `meta` required on every envelope, so synthesizing one from `?? {}`
820
+ // would hand a caller a `ResponseMeta` missing its required fields --
821
+ // which is what the hand-written type hid before #14278. `status` is
822
+ // always lifted (#16137): `201` and `202` are load-bearing on
823
+ // `registerAgent` and `submitTraderExport`, and a caller cannot read
824
+ // them off a body that looks identical to a 200.
825
+ if (envelope.meta) {
826
+ envelope.meta = {
827
+ ...envelope.meta,
828
+ status: response.status,
829
+ ...(etag ? { etag } : {}),
830
+ ...(sandbox ? { sandbox: true } : {}),
831
+ ...(queryIgnored ? { queryIgnored } : {}),
832
+ ...(effectiveQuery ? { effectiveQuery } : {}),
833
+ };
834
+ }
835
+ return envelope;
836
+ }
837
+ throw new OxinsiderApiError(response.status, envelope);
838
+ });
839
+ }
840
+ async text(operationId, options = {}) {
841
+ const operation = this.resolveOperation(operationId, "text");
842
+ return this.request(operation, options, async (response) => response.text(), { accept: "text/markdown, text/plain;q=0.9, */*;q=0.1" });
843
+ }
549
844
  /**
550
- * Execute any operation by id. Throws the matching `OxinsiderApiError`
551
- * subclass on a non-2xx response, returns a typed `not_modified` result for a
552
- * 304, and otherwise returns the parsed `{ object, data, meta }` envelope.
845
+ * Post one MCP JSON-RPC 2.0 message to `POST /api/v1/mcp` (#16137).
846
+ *
847
+ * The endpoint answers a JSON-RPC envelope, not the `{ object, data, meta }`
848
+ * envelope every other operation uses, so `call()` refuses it. A request
849
+ * (one carrying `id`) comes back as `status: 200` with `response` set; a
850
+ * supported notification (no `id`) comes back as `status: 202` with
851
+ * `response: null`, which is the empty body the server sends. A JSON-RPC
852
+ * `error` member is a protocol-level failure and is RETURNED, not thrown:
853
+ * only a non-2xx HTTP status throws, because an unknown tool is an answer
854
+ * and a revoked credential is not.
855
+ *
856
+ * The request is never retried: `tools/call` can have an effect, and the
857
+ * endpoint honours no `Idempotency-Key`.
858
+ *
859
+ * @example
860
+ * const listed = await client.mcp({ method: "tools/list", id: 1 });
861
+ * listed.response?.result;
553
862
  */
554
- async call(operationId, options = {}) {
863
+ async mcp(request, options = {}) {
864
+ const operation = this.resolveOperation("createMcpJsonRpcResponse", "jsonrpc");
865
+ const { sessionId, protocolVersion, ...transportOptions } = options;
866
+ const headers = { ...options.headers };
867
+ if (sessionId !== undefined) {
868
+ headers["mcp-session-id"] = sessionId;
869
+ }
870
+ if (protocolVersion !== undefined) {
871
+ headers["mcp-protocol-version"] = protocolVersion;
872
+ }
873
+ const body = { jsonrpc: "2.0", ...request };
874
+ return this.request(operation, { ...transportOptions, headers, body, maxRetries: 0 }, async (response) => {
875
+ const sessionId = response.headers.get("mcp-session-id");
876
+ const result = {
877
+ status: response.status === 202 ? 202 : 200,
878
+ response: null,
879
+ ...(sessionId ? { sessionId } : {}),
880
+ };
881
+ if (response.status === 202)
882
+ return result;
883
+ const parsed = await parseJson(response);
884
+ if (!isMcpJsonRpcResponse(parsed)) {
885
+ 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);
886
+ }
887
+ result.response = parsed;
888
+ return result;
889
+ });
890
+ }
891
+ /**
892
+ * Resolve the presigned object URL behind `GET /api/v1/trader/{address}/export/download`
893
+ * without downloading anything (#16137).
894
+ *
895
+ * The route answers `302` with a `Location`, which `call()` treats as an
896
+ * error. This reads the redirect target and stops there, so a caller can
897
+ * hand the URL to a download manager or a browser. The returned URL is a
898
+ * bearer credential in itself: anyone holding it can read the file until it
899
+ * expires, so do not log it.
900
+ *
901
+ * Browser caveat: `fetch` with `redirect: "manual"` yields an opaque
902
+ * redirect whose `Location` no script can read. The method says so rather
903
+ * than guessing a URL; run the download from a server runtime.
904
+ */
905
+ async getTraderExportDownloadUrl(address, jobId, options = {}) {
906
+ const operation = exportDownloadOperation;
907
+ if (!this.apiKey && !this.sandbox) {
908
+ throw new Error("downloadTraderExport requires an API key (oxi_sk_*)");
909
+ }
910
+ return this.request(operation, { ...options, path: { address }, query: { job_id: jobId } }, async (response) => {
911
+ if (response.type === "opaqueredirect" || response.status === 0) {
912
+ 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);
913
+ }
914
+ const location = response.headers.get("location");
915
+ if (!location) {
916
+ throw new InvalidResponseError(response.status, "GET /api/v1/trader/{address}/export/download answered a redirect with no Location header.", null);
917
+ }
918
+ return presignedTarget(location);
919
+ }, {
920
+ // The 302 IS the success here, so it must not enter the error path.
921
+ isSuccess: (response) => response.status === 302 || response.type === "opaqueredirect",
922
+ redirect: "manual",
923
+ });
924
+ }
925
+ /**
926
+ * Download a finished export (#16137): resolve the `302`, then fetch the
927
+ * object store directly.
928
+ *
929
+ * The second request carries NO headers at all, so the live API key never
930
+ * reaches the object store; the presigned URL is its own credential. The
931
+ * body is not buffered -- read `response.body` as a stream and write it
932
+ * where it belongs.
933
+ *
934
+ * There is no default deadline on the object fetch, the way `streamFeed`
935
+ * has none: a multi-gigabyte export would fail the 15-second REST default
936
+ * halfway through. Pass `signal` to cancel it, or `downloadTimeoutMs` for
937
+ * a deadline of your own. `timeoutMs` still bounds the redirect request.
938
+ *
939
+ * @example
940
+ * const { response, filename } = await client.downloadTraderExport(address, jobId);
941
+ * await pipeline(Readable.fromWeb(response.body), createWriteStream(filename ?? "export.json"));
942
+ */
943
+ async downloadTraderExport(address, jobId, options = {}) {
944
+ const target = await this.getTraderExportDownloadUrl(address, jobId, options);
945
+ const downloadSignal = composeRequestSignal(options.downloadTimeoutMs ?? null, options.signal);
946
+ let response;
947
+ try {
948
+ // No headers, and no credentials: the presigned URL authorizes itself,
949
+ // and forwarding the bearer would hand the live API key to the object
950
+ // store on every download.
951
+ response = await this.fetchImpl(target.url, {
952
+ method: "GET",
953
+ signal: downloadSignal.signal,
954
+ });
955
+ }
956
+ finally {
957
+ downloadSignal.dispose?.();
958
+ }
959
+ if (!response.ok) {
960
+ 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);
961
+ }
962
+ const contentLength = response.headers.get("content-length");
963
+ return {
964
+ ...target,
965
+ response,
966
+ contentLength: contentLength === null ? null : Number.parseInt(contentLength, 10),
967
+ contentType: response.headers.get("content-type"),
968
+ filename: filenameFromDisposition(response.headers.get("content-disposition")),
969
+ };
970
+ }
971
+ /**
972
+ * Look up an operation and refuse it when the caller reached for the wrong
973
+ * reader (#16137): `call()` on a Markdown route used to fail as a bad 200.
974
+ */
975
+ resolveOperation(operationId, expected) {
555
976
  const operation = operationsById.get(operationId);
556
977
  if (!operation) {
557
978
  throw new Error(`Unknown 0xinsider API operation: ${operationId}`);
558
979
  }
559
- if (operation.auth === "bearer" && !this.apiKey) {
980
+ const kind = operation.kind ?? "envelope";
981
+ if (kind !== expected) {
982
+ throw new Error(`${operationId} answers a "${kind}" body, not "${expected}": use ${READER_FOR_KIND[kind]}.`);
983
+ }
984
+ // The sandbox answers every operation without a credential; production
985
+ // does not, and the local check saves a round trip that can only be 401.
986
+ if (operation.auth === "bearer" && !this.apiKey && !this.sandbox) {
560
987
  throw new Error(`${operationId} requires an API key (oxi_sk_*)`);
561
988
  }
989
+ return operation;
990
+ }
991
+ /**
992
+ * One request pipeline for every transport: URL and headers, the retry
993
+ * policy, one deadline shared by every attempt, the typed error hierarchy,
994
+ * and cancellation held live through body consumption. `consume` runs
995
+ * inside that window, so a slow body is still covered by the deadline and
996
+ * by the caller's `signal`.
997
+ */
998
+ async request(operation, options, consume, transport = {}) {
999
+ const operationId = operation.operationId;
562
1000
  const timeoutMs = options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs;
563
1001
  const maxRetries = options.maxRetries === undefined
564
1002
  ? this.maxRetries
565
1003
  : assertRetryCount(options.maxRetries, "maxRetries");
566
1004
  const url = this.buildUrl(operation, options);
567
- const headers = this.buildHeaders(operation, options);
1005
+ const headers = this.buildHeaders(operation, options, transport.accept);
568
1006
  const body = options.body === undefined ? undefined : JSON.stringify(options.body);
1007
+ const isSuccess = transport.isSuccess ??
1008
+ ((response) => response.ok || response.status === 304);
1009
+ // A key the server does not read is a false promise of safety: the
1010
+ // request would be retried as though replayable while the route repeats
1011
+ // its effect (#16182). Refuse it here, before anything is sent.
1012
+ const eligibility = retryEligibility(operation);
1013
+ const keyed = headers.has("idempotency-key");
1014
+ if (keyed && eligibility !== "keyed") {
1015
+ 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.`);
1016
+ }
569
1017
  // Replaying a write without a key could repeat it (a second webhook
570
- // endpoint, say), so only a GET or a keyed write is retried (#14281).
571
- const retryable = operation.method === "GET" || headers.has("idempotency-key");
1018
+ // endpoint, say), so only a read or a keyed write is retried (#14281).
1019
+ // `headers` and `body` are built once, above, so every attempt carries
1020
+ // the same key and the same bytes.
1021
+ const retryable = eligibility === "read" || (eligibility === "keyed" && keyed);
572
1022
  // One deadline for every attempt: a retry that cannot finish before it is
573
1023
  // not started, so the caller sees the real failure, not a timeout.
574
1024
  const deadlineAt = timeoutMs === null ? null : Date.now() + timeoutMs;
@@ -592,6 +1042,7 @@ export class OxinsiderApiClient {
592
1042
  headers,
593
1043
  body,
594
1044
  signal: requestSignal.signal,
1045
+ ...(transport.redirect ? { redirect: transport.redirect } : {}),
595
1046
  });
596
1047
  }
597
1048
  catch (error) {
@@ -604,7 +1055,7 @@ export class OxinsiderApiClient {
604
1055
  await sleepUnlessAborted(delay, requestSignal.signal);
605
1056
  continue;
606
1057
  }
607
- if (response.ok || response.status === 304)
1058
+ if (isSuccess(response))
608
1059
  break;
609
1060
  const errorBody = await parseJson(response);
610
1061
  const retryAfter = retryAfterSeconds(response);
@@ -616,22 +1067,7 @@ export class OxinsiderApiClient {
616
1067
  }
617
1068
  await sleepUnlessAborted(delay, requestSignal.signal);
618
1069
  }
619
- if (response.status === 304) {
620
- return notModifiedResponse(response);
621
- }
622
- const envelope = await parseJson(response);
623
- if (isApiEnvelope(envelope)) {
624
- const etag = response.headers.get("etag");
625
- // Only ADD to a meta the server actually sent. The contract makes
626
- // `meta` required on every envelope, so synthesizing one from `?? {}`
627
- // would hand a caller a `ResponseMeta` missing its required fields --
628
- // which is what the hand-written type hid before #14278.
629
- if (etag && envelope.meta) {
630
- envelope.meta = { ...envelope.meta, etag };
631
- }
632
- return envelope;
633
- }
634
- throw new OxinsiderApiError(response.status, envelope);
1070
+ return await consume(response);
635
1071
  }
636
1072
  catch (error) {
637
1073
  if (timeoutMs !== null &&
@@ -649,66 +1085,113 @@ export class OxinsiderApiClient {
649
1085
  requestSignal.dispose?.();
650
1086
  }
651
1087
  }
652
- /**
653
- * Execute a Stripe-style list operation and return the list envelope.
654
- * Use `paginate()` (in `pagination.ts`) to auto-follow `next_cursor`.
655
- */
656
1088
  async list(operationId, options = {}) {
657
1089
  const result = await this.call(operationId, options);
658
1090
  if (isNotModified(result)) {
659
1091
  throw new OxinsiderApiError(304, "list() received a 304 not_modified; use call() if you send If-None-Match on a list endpoint");
660
1092
  }
661
1093
  if (!isListEnvelope(result)) {
662
- throw new OxinsiderApiError(200, `Operation ${operationId} did not return a list envelope`);
1094
+ 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);
663
1095
  }
664
1096
  return result;
665
1097
  }
666
1098
  // --- Typed convenience methods (key surfaces) ---
1099
+ //
1100
+ // Each takes the route's path parameters positionally, its documented
1101
+ // query as `params` (list reads) or `options.query` (single reads), and
1102
+ // the shared transport options: `signal`, `timeoutMs`, `maxRetries`,
1103
+ // `headers` (`If-None-Match` for a conditional read) and `idempotencyKey`
1104
+ // on a keyed write. The result is the operation's own envelope; no method
1105
+ // takes a type argument any more (#16136).
667
1106
  getTrader(address, options = {}) {
668
1107
  return this.call("getTrader", { ...options, path: { address } });
669
1108
  }
670
1109
  getTraderPnl(address, options = {}) {
671
1110
  return this.call("getTraderPnl", { ...options, path: { address } });
672
1111
  }
673
- batchGetTraders(identifiers, options = {}) {
674
- return this.call("batchGetTraders", {
1112
+ /**
1113
+ * Fetch one wallet's win record per canonical category.
1114
+ *
1115
+ * Counts every settled market at any position size, so it can differ from
1116
+ * `category_strengths` on `getTrader`, which reads the floored calibration
1117
+ * sample. A category under `min_decided_for_win_rate` keeps its `wins` and
1118
+ * `decided` with `win_rate: null` and `status: "not_enough_data"`; a category
1119
+ * the wallet has no settled market in is absent, which means no record rather
1120
+ * than a 0% record. The `Esports` record carries `games`: the wallet's record
1121
+ * per esports title (`LoL`, `CS2`, `Dota 2`, ...) under the same rule, named
1122
+ * as the holder chips name them in `category_win_rate_game`. Pass
1123
+ * `params.category` to filter to one bucket; a filter that reaches Esports
1124
+ * returns its games too.
1125
+ */
1126
+ getTraderCategoryRecords(address, params = {}, options = {}) {
1127
+ return this.call("getTraderCategoryRecords", {
675
1128
  ...options,
676
- body: { identifiers },
1129
+ path: { address },
1130
+ query: params,
677
1131
  });
678
1132
  }
679
- listLeaderboard(params = {}) {
680
- return this.list("listLeaderboard", { query: params });
1133
+ /**
1134
+ * Resolve 1 to 25 traders in one request (`POST /api/v1/traders/batch`).
1135
+ *
1136
+ * `traders` is sent as the request body's `traders` array, the field the
1137
+ * route requires: wallet addresses, usernames, `trd_` ids or integer trader
1138
+ * ids, resolved in input order. Until #16135 this method sent the array as
1139
+ * `identifiers`, a field the route does not declare, so every call was
1140
+ * refused for a missing `traders`. `options.expand` is sent as the body's
1141
+ * `expand` array and applies to every item.
1142
+ *
1143
+ * The response `data` keeps request order and one row per input, duplicates
1144
+ * included; a row is `status: "ok"` with `data`, or `status: "error"` with
1145
+ * the item's own `error` (an unknown identity is a per-item `not_found`, not
1146
+ * a request failure). `meta` is the batch's `BatchResponseMeta`:
1147
+ * `request_cost` and `rate_limit` follow the batch item quota, not the
1148
+ * per-request one.
1149
+ */
1150
+ batchGetTraders(traders, options = {}) {
1151
+ const { expand, ...request } = options;
1152
+ return this.call("batchGetTraders", {
1153
+ ...request,
1154
+ body: expand === undefined ? { traders } : { traders, expand },
1155
+ });
1156
+ }
1157
+ /**
1158
+ * Resolve up to 25 markets' intel in one request
1159
+ * (`POST /api/v1/markets/intel/batch`); `meta` is the batch's own.
1160
+ */
1161
+ batchGetMarketIntel(body, options = {}) {
1162
+ return this.call("batchGetMarketIntel", { ...options, body });
1163
+ }
1164
+ listLeaderboard(params = {}, options = {}) {
1165
+ return this.list("listLeaderboard", { ...options, query: params });
681
1166
  }
682
- listTrendingWallets(params = {}) {
683
- return this.list("listTrendingWallets", { query: params });
1167
+ listTrendingWallets(params = {}, options = {}) {
1168
+ return this.list("listTrendingWallets", { ...options, query: params });
684
1169
  }
685
- listWhaleTrades(params = {}) {
686
- return this.list("listWhaleTrades", { query: params });
1170
+ listWhaleTrades(params = {}, options = {}) {
1171
+ return this.list("listWhaleTrades", { ...options, query: params });
687
1172
  }
688
- listWhaleTradeHistory(params = {}) {
689
- return this.list("listWhaleTradeHistory", { query: params });
1173
+ listWhaleTradeHistory(params = {}, options = {}) {
1174
+ return this.list("listWhaleTradeHistory", { ...options, query: params });
690
1175
  }
691
- listPositions(params = {}) {
692
- return this.list("listPositions", { query: params });
1176
+ listPositions(params = {}, options = {}) {
1177
+ return this.list("listPositions", { ...options, query: params });
693
1178
  }
694
- listLargePositions(params = {}) {
695
- return this.list("listLargePositions", { query: params });
1179
+ listLargePositions(params = {}, options = {}) {
1180
+ return this.list("listLargePositions", { ...options, query: params });
696
1181
  }
697
- listSmartMoneyFlows(params = {}) {
698
- return this.list("listSmartMoneyFlows", { query: params });
1182
+ listSmartMoneyFlows(params = {}, options = {}) {
1183
+ return this.list("listSmartMoneyFlows", { ...options, query: params });
699
1184
  }
700
1185
  /** Canonical alias of {@link listSmartMoneyFlows} (epic #6912). */
701
- listSharpMoneyFlows(params = {}) {
702
- return this.list("listSharpMoneyFlows", { query: params });
1186
+ listSharpMoneyFlows(params = {}, options = {}) {
1187
+ return this.list("listSharpMoneyFlows", { ...options, query: params });
703
1188
  }
704
1189
  /**
705
1190
  * List the funded-primary sports signals with additive, shadow-only category
706
1191
  * evidence. `category_skill` never changes membership, ordering, or sizing.
707
1192
  */
708
- listSportsEdgeSignals(params = {}) {
709
- return this.list("listSportsEdgeSignals", {
710
- query: params,
711
- });
1193
+ listSportsEdgeSignals(params = {}, options = {}) {
1194
+ return this.list("listSportsEdgeSignals", { ...options, query: params });
712
1195
  }
713
1196
  /**
714
1197
  * Read an explicitly observation-only sports cohort and its accountable
@@ -720,7 +1203,10 @@ export class OxinsiderApiClient {
720
1203
  * denominator view.
721
1204
  */
722
1205
  async listSportsEdgeObservations(params, options = {}) {
723
- const response = await this.list("listSportsEdgeObservations", { ...options, query: params });
1206
+ const response = await this.list("listSportsEdgeObservations", {
1207
+ ...options,
1208
+ query: params,
1209
+ });
724
1210
  return sportsEdgeObservationsResponse(response);
725
1211
  }
726
1212
  /**
@@ -733,23 +1219,25 @@ export class OxinsiderApiClient {
733
1219
  * position remains covered.
734
1220
  */
735
1221
  async listSportsEdgeObservationsConditional(params, options = {}) {
736
- const response = await this.call("listSportsEdgeObservations", { ...options, query: params });
1222
+ const response = await this.call("listSportsEdgeObservations", {
1223
+ ...options,
1224
+ query: params,
1225
+ });
737
1226
  if (isNotModified(response)) {
738
1227
  return response;
739
1228
  }
740
- if (!isListEnvelope(response)) {
741
- throw new OxinsiderApiError(200, "listSportsEdgeObservations did not return a list envelope");
742
- }
743
1229
  return sportsEdgeObservationsResponse(response);
744
1230
  }
745
1231
  searchMarkets(q, options = {}) {
746
- return this.call("searchMarkets", {
747
- ...options,
748
- query: { q, ...options.query },
749
- });
1232
+ const { query, ...request } = options;
1233
+ return this.list("searchMarkets", { ...request, query: { ...query, q } });
1234
+ }
1235
+ searchContent(q, options = {}) {
1236
+ const { query, ...request } = options;
1237
+ return this.list("searchContent", { ...request, query: { ...query, q } });
750
1238
  }
751
- exploreMarkets(params = {}) {
752
- return this.list("exploreMarkets", { query: params });
1239
+ exploreMarkets(params = {}, options = {}) {
1240
+ return this.list("exploreMarkets", { ...options, query: params });
753
1241
  }
754
1242
  getMarketIntel(conditionId, options = {}) {
755
1243
  return this.call("getMarketIntel", {
@@ -763,37 +1251,71 @@ export class OxinsiderApiClient {
763
1251
  path: { condition_id: conditionId },
764
1252
  });
765
1253
  }
1254
+ /**
1255
+ * One page of a market's graded (S/A/B) holder roster from a complete
1256
+ * provider holder scan: the list a Pick of the Day shows, for any market.
1257
+ * `params.outcome` (`yes` | `no` | `all`), `params.min_grade` (`S` | `A` |
1258
+ * `B`), `params.limit` and `params.cursor` (`mh_` prefix). The page carries
1259
+ * the `market`, `scan` and roster `totals` beside `data`; `total` is the
1260
+ * count matching the filters across every page.
1261
+ */
1262
+ getMarketHolders(conditionId, params = {}, options = {}) {
1263
+ return this.list("getMarketHolders", {
1264
+ ...options,
1265
+ path: { condition_id: conditionId },
1266
+ query: params,
1267
+ });
1268
+ }
766
1269
  /**
767
1270
  * Fetch bucketed OHLC candles for a market's outcome tokens.
768
- * Resolution defaults to the server default; pass `query.resolution` as
769
- * `"1d"` or `"1w"` to override.
1271
+ * Resolution defaults to the server default; pass `params.resolution` as
1272
+ * `"1d"` or `"1w"` to override. `params.from` is exclusive and `params.to`
1273
+ * is inclusive; when both are present, `from` must be less than or equal to
1274
+ * `to`.
770
1275
  */
771
- getMarketCandles(conditionId, options = {}) {
1276
+ getMarketCandles(conditionId, params = {}, options = {}) {
772
1277
  return this.call("getMarketCandles", {
773
1278
  ...options,
774
1279
  path: { condition_id: conditionId },
1280
+ query: params,
775
1281
  });
776
1282
  }
777
1283
  getInsiderRadarFlag(id, options = {}) {
778
1284
  return this.call("getInsiderRadarFlag", { ...options, path: { id } });
779
1285
  }
780
- listInsiderRadar(params = {}) {
781
- return this.list("listInsiderRadar", { query: params });
1286
+ listInsiderRadar(params = {}, options = {}) {
1287
+ return this.list("listInsiderRadar", { ...options, query: params });
1288
+ }
1289
+ getWhaleTrade(id, options = {}) {
1290
+ return this.call("getWhaleTrade", { ...options, path: { id } });
782
1291
  }
783
1292
  // --- Webhooks ---
784
1293
  listWebhooks(options = {}) {
785
- return this.call("listWebhooks", options);
1294
+ return this.list("listWebhooks", options);
1295
+ }
1296
+ getWebhook(id, options = {}) {
1297
+ return this.call("getWebhook", { ...options, path: { id } });
1298
+ }
1299
+ createWebhook(body, options = {}) {
1300
+ return this.call("createWebhook", { ...options, body });
1301
+ }
1302
+ updateWebhook(id, body, options = {}) {
1303
+ return this.call("updateWebhook", { ...options, path: { id }, body });
1304
+ }
1305
+ deleteWebhook(id, options = {}) {
1306
+ return this.call("deleteWebhook", { ...options, path: { id } });
786
1307
  }
787
1308
  /**
788
1309
  * Fetch the self-describing webhook event catalog (each entry's `id`,
789
1310
  * description, payload shape, and active/dormant status).
790
1311
  */
791
1312
  listWebhookEvents(options = {}) {
792
- return this.call("listWebhookEvents", options);
1313
+ return this.list("listWebhookEvents", options);
793
1314
  }
794
1315
  /** List delivery attempts for one webhook endpoint (Stripe-style list). */
795
- listWebhookDeliveries(webhookId, params = {}) {
1316
+ listWebhookDeliveries(webhookId, params = {}, options = {}) {
796
1317
  return this.list("listWebhookDeliveries", {
1318
+ ...options,
797
1319
  path: { id: webhookId },
798
1320
  query: params,
799
1321
  });
@@ -819,9 +1341,44 @@ export class OxinsiderApiClient {
819
1341
  getApiDiscovery(options = {}) {
820
1342
  return this.call("getApiDiscovery", options);
821
1343
  }
1344
+ getPlatforms(options = {}) {
1345
+ return this.call("getPlatforms", options);
1346
+ }
822
1347
  getAccountIdentity(options = {}) {
823
1348
  return this.call("getAccountIdentity", options);
824
1349
  }
1350
+ /**
1351
+ * One wallet's context as Markdown, ready to paste into a model prompt
1352
+ * (`GET /api/v1/trader/{address}/context.md`). The JSON form of the same
1353
+ * read is `getTraderContext`.
1354
+ */
1355
+ getTraderContextMarkdown(address, options = {}) {
1356
+ return this.text("getTraderContextMarkdown", { ...options, path: { address } });
1357
+ }
1358
+ /**
1359
+ * One market's context as Markdown
1360
+ * (`GET /api/v1/market/{condition_id}/context.md`). The JSON form is
1361
+ * `getMarketIntel`.
1362
+ */
1363
+ getMarketContextMarkdown(conditionId, options = {}) {
1364
+ return this.text("getMarketContextMarkdown", {
1365
+ ...options,
1366
+ path: { condition_id: conditionId },
1367
+ });
1368
+ }
1369
+ /**
1370
+ * Mint a sandbox key (`POST /api/v1/agents/register`), the one write that
1371
+ * needs no credential. Answers `201`, which is why the SDK had no method
1372
+ * for it until #16137: the drift gate counted only operations with a
1373
+ * documented `200`. `meta.status` is `201`; `data.api_key` is the
1374
+ * `oxi_sk_test_*` key to pass to `OxinsiderApiClient.sandbox()`.
1375
+ *
1376
+ * Nothing is stored: the key cannot be listed or revoked and does not
1377
+ * expire. Register again for another one.
1378
+ */
1379
+ registerAgent(options = {}) {
1380
+ return this.call("registerAgent", options);
1381
+ }
825
1382
  getUsage(options = {}) {
826
1383
  return this.call("getUsage", options);
827
1384
  }
@@ -833,6 +1390,20 @@ export class OxinsiderApiClient {
833
1390
  getPickOfTheDayArchive(options = {}) {
834
1391
  return this.call("getPickOfTheDayArchive", options);
835
1392
  }
1393
+ /**
1394
+ * Fetch the Pick of the Day commitment ledger (single-object envelope).
1395
+ *
1396
+ * Every entry is `sealed` (a live pick: the hash, no side and no price),
1397
+ * `opened` (a settled pick: the nonce and the exact hashed payload) or
1398
+ * `uncommitted` (no commitment; once settled, its unhashed side and price
1399
+ * under `payload`). Verify an opened entry by
1400
+ * appending the hex-decoded `commitment_nonce` to the `payload` bytes as
1401
+ * received and hashing with sha256; do not reserialize the payload, since it
1402
+ * is served byte for byte as it was hashed.
1403
+ */
1404
+ getPickOfTheDayLedger(options = {}) {
1405
+ return this.call("getPickOfTheDayLedger", options);
1406
+ }
836
1407
  // --- Internals ---
837
1408
  /** Resolve the base URL (used by the SSE stream consumer). */
838
1409
  getBaseUrl() {
@@ -842,12 +1413,16 @@ export class OxinsiderApiClient {
842
1413
  getApiKey() {
843
1414
  return this.apiKey;
844
1415
  }
1416
+ /** Whether this client was built in sandbox mode (#16138). */
1417
+ isSandbox() {
1418
+ return this.sandbox;
1419
+ }
845
1420
  /** The resolved fetch implementation (used by the SSE stream consumer). */
846
1421
  getFetch() {
847
1422
  return this.fetchImpl;
848
1423
  }
849
1424
  buildUrl(operation, options) {
850
- const url = new URL(interpolatePath(operation.path, options.path ?? {}), `${this.baseUrl}/`);
1425
+ const url = resolveApiUrl(this.baseUrl, interpolatePath(operation.path, options.path ?? {}));
851
1426
  for (const [key, value] of Object.entries(options.query ?? {})) {
852
1427
  if (value === null || value === undefined) {
853
1428
  continue;
@@ -863,9 +1438,9 @@ export class OxinsiderApiClient {
863
1438
  }
864
1439
  return url.toString();
865
1440
  }
866
- buildHeaders(operation, options) {
1441
+ buildHeaders(operation, options, accept = "application/json") {
867
1442
  const headers = new Headers(options.headers);
868
- headers.set("accept", "application/json");
1443
+ headers.set("accept", accept);
869
1444
  if (options.body !== undefined && !headers.has("content-type")) {
870
1445
  headers.set("content-type", "application/json");
871
1446
  }
@@ -875,9 +1450,18 @@ export class OxinsiderApiClient {
875
1450
  if (options.idempotencyKey !== undefined) {
876
1451
  headers.set("idempotency-key", options.idempotencyKey);
877
1452
  }
1453
+ if (options.strictQuery) {
1454
+ headers.set("x-query-validation", "strict");
1455
+ }
878
1456
  return headers;
879
1457
  }
880
1458
  }
1459
+ /**
1460
+ * Narrow a `call()` result to the typed 304. Takes any envelope-shaped
1461
+ * value, so it works on an `OperationResult<K>` (whose `meta` is the
1462
+ * operation's own type) as well as the loose `ApiClientResponse<T>`
1463
+ * (#16136).
1464
+ */
881
1465
  export function isApiNotModifiedResponse(response) {
882
1466
  return response.object === "not_modified";
883
1467
  }
@@ -905,13 +1489,6 @@ async function parseJson(response) {
905
1489
  return text;
906
1490
  }
907
1491
  }
908
- function retryAfterSeconds(response) {
909
- const header = response.headers.get("retry-after");
910
- if (!header)
911
- return null;
912
- const seconds = Number(header);
913
- return Number.isFinite(seconds) ? seconds : null;
914
- }
915
1492
  function notModifiedResponse(response) {
916
1493
  const meta = { status: 304 };
917
1494
  const etag = response.headers.get("etag");
@@ -922,6 +1499,14 @@ function notModifiedResponse(response) {
922
1499
  if (requestId) {
923
1500
  meta.request_id = requestId;
924
1501
  }
1502
+ const queryIgnored = response.headers.get("x-query-ignored");
1503
+ if (queryIgnored) {
1504
+ meta.queryIgnored = queryIgnored;
1505
+ }
1506
+ const effectiveQuery = response.headers.get("x-effective-query");
1507
+ if (effectiveQuery) {
1508
+ meta.effectiveQuery = effectiveQuery;
1509
+ }
925
1510
  return { object: "not_modified", data: null, meta };
926
1511
  }
927
1512
  function isApiEnvelope(body) {
@@ -930,14 +1515,23 @@ function isApiEnvelope(body) {
930
1515
  "object" in body &&
931
1516
  "data" in body);
932
1517
  }
1518
+ /**
1519
+ * Whether `body` is a list envelope whose control fields the walker can act
1520
+ * on: `object: "list"`, an array `data`, a BOOLEAN `has_more`, and a
1521
+ * `next_cursor` that is a string, `null` or absent (#16246: the check used to
1522
+ * accept any `has_more` that was merely present, so a malformed value could
1523
+ * be coerced into "no more pages" or "more pages" by truthiness).
1524
+ */
933
1525
  export function isListEnvelope(body) {
934
- return (typeof body === "object" &&
935
- body !== null &&
936
- "object" in body &&
937
- body.object === "list" &&
938
- "data" in body &&
939
- Array.isArray(body.data) &&
940
- "has_more" in body);
1526
+ if (typeof body !== "object" || body === null)
1527
+ return false;
1528
+ const candidate = body;
1529
+ return (candidate.object === "list" &&
1530
+ Array.isArray(candidate.data) &&
1531
+ typeof candidate.has_more === "boolean" &&
1532
+ (candidate.next_cursor === undefined ||
1533
+ candidate.next_cursor === null ||
1534
+ typeof candidate.next_cursor === "string"));
941
1535
  }
942
1536
  function sportsEdgeObservationsResponse(response) {
943
1537
  const candidate = response;