@0xinsider/mcp 2.1.0 → 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 (92) hide show
  1. package/README.md +25 -6
  2. package/dist/cli-lib.d.ts +1 -0
  3. package/dist/cli-lib.d.ts.map +1 -1
  4. package/dist/cli-lib.js +48 -8
  5. package/dist/cli-lib.js.map +1 -1
  6. package/dist/client.d.ts +33 -2
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +42 -4
  9. package/dist/client.js.map +1 -1
  10. package/dist/constants.d.ts +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/product-cli/client.d.ts +3 -2
  13. package/dist/product-cli/client.d.ts.map +1 -1
  14. package/dist/product-cli/client.js.map +1 -1
  15. package/dist/sdk/client.d.ts +690 -133
  16. package/dist/sdk/client.d.ts.map +1 -1
  17. package/dist/sdk/client.js +699 -125
  18. package/dist/sdk/client.js.map +1 -1
  19. package/dist/sdk/errors.d.ts +42 -4
  20. package/dist/sdk/errors.d.ts.map +1 -1
  21. package/dist/sdk/errors.js +103 -2
  22. package/dist/sdk/errors.js.map +1 -1
  23. package/dist/sdk/index.d.ts +9 -8
  24. package/dist/sdk/index.d.ts.map +1 -1
  25. package/dist/sdk/index.js +6 -4
  26. package/dist/sdk/index.js.map +1 -1
  27. package/dist/sdk/pagination.d.ts +133 -15
  28. package/dist/sdk/pagination.d.ts.map +1 -1
  29. package/dist/sdk/pagination.js +132 -34
  30. package/dist/sdk/pagination.js.map +1 -1
  31. package/dist/sdk/retry.d.ts +68 -0
  32. package/dist/sdk/retry.d.ts.map +1 -0
  33. package/dist/sdk/retry.js +125 -0
  34. package/dist/sdk/retry.js.map +1 -0
  35. package/dist/sdk/schema.d.ts +1381 -439
  36. package/dist/sdk/schema.d.ts.map +1 -1
  37. package/dist/sdk/stream.d.ts +318 -8
  38. package/dist/sdk/stream.d.ts.map +1 -1
  39. package/dist/sdk/stream.js +503 -56
  40. package/dist/sdk/stream.js.map +1 -1
  41. package/dist/sdk/webhooks.d.ts +18 -3
  42. package/dist/sdk/webhooks.d.ts.map +1 -1
  43. package/dist/sdk/webhooks.js +13 -1
  44. package/dist/sdk/webhooks.js.map +1 -1
  45. package/dist/tools/explore-markets.d.ts.map +1 -1
  46. package/dist/tools/explore-markets.js +2 -5
  47. package/dist/tools/explore-markets.js.map +1 -1
  48. package/dist/tools/get-insider-radar.d.ts.map +1 -1
  49. package/dist/tools/get-insider-radar.js +3 -10
  50. package/dist/tools/get-insider-radar.js.map +1 -1
  51. package/dist/tools/get-large-positions.d.ts.map +1 -1
  52. package/dist/tools/get-large-positions.js +3 -10
  53. package/dist/tools/get-large-positions.js.map +1 -1
  54. package/dist/tools/get-leaderboard.d.ts.map +1 -1
  55. package/dist/tools/get-leaderboard.js +3 -10
  56. package/dist/tools/get-leaderboard.js.map +1 -1
  57. package/dist/tools/get-market-intel.d.ts.map +1 -1
  58. package/dist/tools/get-market-intel.js +3 -6
  59. package/dist/tools/get-market-intel.js.map +1 -1
  60. package/dist/tools/get-position-timeline.d.ts.map +1 -1
  61. package/dist/tools/get-position-timeline.js +3 -10
  62. package/dist/tools/get-position-timeline.js.map +1 -1
  63. package/dist/tools/get-positions.d.ts +2 -0
  64. package/dist/tools/get-positions.d.ts.map +1 -1
  65. package/dist/tools/get-positions.js +8 -11
  66. package/dist/tools/get-positions.js.map +1 -1
  67. package/dist/tools/get-trader-pnl.d.ts.map +1 -1
  68. package/dist/tools/get-trader-pnl.js +3 -6
  69. package/dist/tools/get-trader-pnl.js.map +1 -1
  70. package/dist/tools/get-trader.d.ts.map +1 -1
  71. package/dist/tools/get-trader.js +3 -6
  72. package/dist/tools/get-trader.js.map +1 -1
  73. package/dist/tools/get-trending-wallets.d.ts.map +1 -1
  74. package/dist/tools/get-trending-wallets.js +3 -10
  75. package/dist/tools/get-trending-wallets.js.map +1 -1
  76. package/dist/tools/get-whale-trades.d.ts.map +1 -1
  77. package/dist/tools/get-whale-trades.js +4 -11
  78. package/dist/tools/get-whale-trades.js.map +1 -1
  79. package/dist/tools/pick-of-the-day.d.ts.map +1 -1
  80. package/dist/tools/pick-of-the-day.js +5 -21
  81. package/dist/tools/pick-of-the-day.js.map +1 -1
  82. package/dist/tools/read-parity.d.ts.map +1 -1
  83. package/dist/tools/read-parity.js +36 -44
  84. package/dist/tools/read-parity.js.map +1 -1
  85. package/dist/tools/result.d.ts +31 -0
  86. package/dist/tools/result.d.ts.map +1 -0
  87. package/dist/tools/result.js +70 -0
  88. package/dist/tools/result.js.map +1 -0
  89. package/dist/tools/search-markets.d.ts.map +1 -1
  90. package/dist/tools/search-markets.js +16 -11
  91. package/dist/tools/search-markets.js.map +1 -1
  92. 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",
@@ -384,9 +417,100 @@ export const API_CLIENT_OPERATIONS = [
384
417
  method: "GET",
385
418
  path: "/api/v1/pick-of-the-day/ledger",
386
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",
387
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.",
388
456
  },
389
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",
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
+ }
390
514
  /**
391
515
  * Trader skill grade. Source of truth:
392
516
  * `components.schemas.Trader.properties.grade.enum` (S highest, F lowest).
@@ -394,17 +518,25 @@ export const API_CLIENT_OPERATIONS = [
394
518
  export const GRADES = ["S", "A", "B", "C", "D", "F"];
395
519
  /** Default production API base used when `baseUrl` is omitted. */
396
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_";
397
531
  /** Default per-request deadline; see `ApiClientOptions.timeoutMs`. */
398
532
  export const DEFAULT_TIMEOUT_MS = 15_000;
399
533
  /** Default retry budget; see `ApiClientOptions.maxRetries`. */
400
534
  export const DEFAULT_MAX_RETRIES = 2;
401
535
  /** Statuses a retry can fix: rate limited, or a gateway/availability failure. */
402
- const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
536
+ const RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);
403
537
  const RETRY_BASE_DELAY_MS = 500;
404
538
  const RETRY_MAX_BACKOFF_MS = 8_000;
405
539
  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
540
  function assertRetryCount(value, source) {
409
541
  if (!Number.isInteger(value) || value < 0) {
410
542
  throw new Error(`${source} must be a non-negative integer, got ${String(value)}`);
@@ -413,11 +545,14 @@ function assertRetryCount(value, source) {
413
545
  }
414
546
  /**
415
547
  * Milliseconds to wait before retry `attempt` (0-based), or `null` when the
416
- * 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`).
417
552
  */
418
553
  function retryDelayMs(attempt, retryAfter) {
419
554
  if (retryAfter !== null) {
420
- const requested = Math.max(0, retryAfter * 1000);
555
+ const requested = retryAfter * 1000;
421
556
  if (requested > RETRY_AFTER_CEILING_MS)
422
557
  return null;
423
558
  return requested + Math.random() * RETRY_JITTER_MS;
@@ -425,24 +560,6 @@ function retryDelayMs(attempt, retryAfter) {
425
560
  const ceiling = Math.min(RETRY_MAX_BACKOFF_MS, RETRY_BASE_DELAY_MS * 2 ** attempt);
426
561
  return ceiling / 2 + Math.random() * (ceiling / 2);
427
562
  }
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
563
  function isLoopbackHost(hostname) {
447
564
  const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
448
565
  return (host === "localhost" ||
@@ -469,6 +586,28 @@ export function assertTrustedBaseUrl(baseUrl) {
469
586
  return url;
470
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).`);
471
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
+ }
472
611
  /**
473
612
  * One signal that aborts on the deadline OR when the caller's signal aborts.
474
613
  * Uses native composition where available. The manual fallback removes both
@@ -518,6 +657,84 @@ function isTimeoutAbort(error) {
518
657
  "name" in error &&
519
658
  error.name === "TimeoutError");
520
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
+ }
521
738
  /** `GET /api/v1/leaderboard` -> parameters -> strategy (`web/public/api/v1/openapi.json`). */
522
739
  export const LEADERBOARD_STRATEGIES = [
523
740
  "accumulator",
@@ -531,16 +748,48 @@ export const LEADERBOARD_STRATEGIES = [
531
748
  "speculator",
532
749
  "swing_trader",
533
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
+ ];
534
762
  const operationsById = new Map(API_CLIENT_OPERATIONS.map((operation) => [operation.operationId, operation]));
535
763
  export class OxinsiderApiClient {
536
764
  baseUrl;
537
765
  apiKey;
766
+ sandbox;
538
767
  fetchImpl;
539
768
  timeoutMs;
540
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
+ }
541
784
  constructor(options = {}) {
542
- const base = assertTrustedBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
543
- 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
+ }
544
793
  this.apiKey = options.apiKey;
545
794
  this.timeoutMs =
546
795
  options.timeoutMs === undefined ? DEFAULT_TIMEOUT_MS : options.timeoutMs;
@@ -552,29 +801,224 @@ export class OxinsiderApiClient {
552
801
  // Bind so a passed-through `globalThis.fetch` keeps its receiver.
553
802
  this.fetchImpl = fetchImpl.bind(globalThis);
554
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
+ }
555
844
  /**
556
- * Execute any operation by id. Throws the matching `OxinsiderApiError`
557
- * subclass on a non-2xx response, returns a typed `not_modified` result for a
558
- * 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;
559
862
  */
560
- 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) {
561
976
  const operation = operationsById.get(operationId);
562
977
  if (!operation) {
563
978
  throw new Error(`Unknown 0xinsider API operation: ${operationId}`);
564
979
  }
565
- 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) {
566
987
  throw new Error(`${operationId} requires an API key (oxi_sk_*)`);
567
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;
568
1000
  const timeoutMs = options.timeoutMs === undefined ? this.timeoutMs : options.timeoutMs;
569
1001
  const maxRetries = options.maxRetries === undefined
570
1002
  ? this.maxRetries
571
1003
  : assertRetryCount(options.maxRetries, "maxRetries");
572
1004
  const url = this.buildUrl(operation, options);
573
- const headers = this.buildHeaders(operation, options);
1005
+ const headers = this.buildHeaders(operation, options, transport.accept);
574
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
+ }
575
1017
  // Replaying a write without a key could repeat it (a second webhook
576
- // endpoint, say), so only a GET or a keyed write is retried (#14281).
577
- 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);
578
1022
  // One deadline for every attempt: a retry that cannot finish before it is
579
1023
  // not started, so the caller sees the real failure, not a timeout.
580
1024
  const deadlineAt = timeoutMs === null ? null : Date.now() + timeoutMs;
@@ -598,6 +1042,7 @@ export class OxinsiderApiClient {
598
1042
  headers,
599
1043
  body,
600
1044
  signal: requestSignal.signal,
1045
+ ...(transport.redirect ? { redirect: transport.redirect } : {}),
601
1046
  });
602
1047
  }
603
1048
  catch (error) {
@@ -610,7 +1055,7 @@ export class OxinsiderApiClient {
610
1055
  await sleepUnlessAborted(delay, requestSignal.signal);
611
1056
  continue;
612
1057
  }
613
- if (response.ok || response.status === 304)
1058
+ if (isSuccess(response))
614
1059
  break;
615
1060
  const errorBody = await parseJson(response);
616
1061
  const retryAfter = retryAfterSeconds(response);
@@ -622,22 +1067,7 @@ export class OxinsiderApiClient {
622
1067
  }
623
1068
  await sleepUnlessAborted(delay, requestSignal.signal);
624
1069
  }
625
- if (response.status === 304) {
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);
1070
+ return await consume(response);
641
1071
  }
642
1072
  catch (error) {
643
1073
  if (timeoutMs !== null &&
@@ -655,66 +1085,113 @@ export class OxinsiderApiClient {
655
1085
  requestSignal.dispose?.();
656
1086
  }
657
1087
  }
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
1088
  async list(operationId, options = {}) {
663
1089
  const result = await this.call(operationId, options);
664
1090
  if (isNotModified(result)) {
665
1091
  throw new OxinsiderApiError(304, "list() received a 304 not_modified; use call() if you send If-None-Match on a list endpoint");
666
1092
  }
667
1093
  if (!isListEnvelope(result)) {
668
- 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);
669
1095
  }
670
1096
  return result;
671
1097
  }
672
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).
673
1106
  getTrader(address, options = {}) {
674
1107
  return this.call("getTrader", { ...options, path: { address } });
675
1108
  }
676
1109
  getTraderPnl(address, options = {}) {
677
1110
  return this.call("getTraderPnl", { ...options, path: { address } });
678
1111
  }
679
- batchGetTraders(identifiers, options = {}) {
680
- 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", {
681
1128
  ...options,
682
- body: { identifiers },
1129
+ path: { address },
1130
+ query: params,
683
1131
  });
684
1132
  }
685
- listLeaderboard(params = {}) {
686
- 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
+ });
687
1156
  }
688
- listTrendingWallets(params = {}) {
689
- return this.list("listTrendingWallets", { query: params });
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 });
1166
+ }
1167
+ listTrendingWallets(params = {}, options = {}) {
1168
+ return this.list("listTrendingWallets", { ...options, query: params });
690
1169
  }
691
- listWhaleTrades(params = {}) {
692
- return this.list("listWhaleTrades", { query: params });
1170
+ listWhaleTrades(params = {}, options = {}) {
1171
+ return this.list("listWhaleTrades", { ...options, query: params });
693
1172
  }
694
- listWhaleTradeHistory(params = {}) {
695
- return this.list("listWhaleTradeHistory", { query: params });
1173
+ listWhaleTradeHistory(params = {}, options = {}) {
1174
+ return this.list("listWhaleTradeHistory", { ...options, query: params });
696
1175
  }
697
- listPositions(params = {}) {
698
- return this.list("listPositions", { query: params });
1176
+ listPositions(params = {}, options = {}) {
1177
+ return this.list("listPositions", { ...options, query: params });
699
1178
  }
700
- listLargePositions(params = {}) {
701
- return this.list("listLargePositions", { query: params });
1179
+ listLargePositions(params = {}, options = {}) {
1180
+ return this.list("listLargePositions", { ...options, query: params });
702
1181
  }
703
- listSmartMoneyFlows(params = {}) {
704
- return this.list("listSmartMoneyFlows", { query: params });
1182
+ listSmartMoneyFlows(params = {}, options = {}) {
1183
+ return this.list("listSmartMoneyFlows", { ...options, query: params });
705
1184
  }
706
1185
  /** Canonical alias of {@link listSmartMoneyFlows} (epic #6912). */
707
- listSharpMoneyFlows(params = {}) {
708
- return this.list("listSharpMoneyFlows", { query: params });
1186
+ listSharpMoneyFlows(params = {}, options = {}) {
1187
+ return this.list("listSharpMoneyFlows", { ...options, query: params });
709
1188
  }
710
1189
  /**
711
1190
  * List the funded-primary sports signals with additive, shadow-only category
712
1191
  * evidence. `category_skill` never changes membership, ordering, or sizing.
713
1192
  */
714
- listSportsEdgeSignals(params = {}) {
715
- return this.list("listSportsEdgeSignals", {
716
- query: params,
717
- });
1193
+ listSportsEdgeSignals(params = {}, options = {}) {
1194
+ return this.list("listSportsEdgeSignals", { ...options, query: params });
718
1195
  }
719
1196
  /**
720
1197
  * Read an explicitly observation-only sports cohort and its accountable
@@ -726,7 +1203,10 @@ export class OxinsiderApiClient {
726
1203
  * denominator view.
727
1204
  */
728
1205
  async listSportsEdgeObservations(params, options = {}) {
729
- const response = await this.list("listSportsEdgeObservations", { ...options, query: params });
1206
+ const response = await this.list("listSportsEdgeObservations", {
1207
+ ...options,
1208
+ query: params,
1209
+ });
730
1210
  return sportsEdgeObservationsResponse(response);
731
1211
  }
732
1212
  /**
@@ -739,23 +1219,25 @@ export class OxinsiderApiClient {
739
1219
  * position remains covered.
740
1220
  */
741
1221
  async listSportsEdgeObservationsConditional(params, options = {}) {
742
- const response = await this.call("listSportsEdgeObservations", { ...options, query: params });
1222
+ const response = await this.call("listSportsEdgeObservations", {
1223
+ ...options,
1224
+ query: params,
1225
+ });
743
1226
  if (isNotModified(response)) {
744
1227
  return response;
745
1228
  }
746
- if (!isListEnvelope(response)) {
747
- throw new OxinsiderApiError(200, "listSportsEdgeObservations did not return a list envelope");
748
- }
749
1229
  return sportsEdgeObservationsResponse(response);
750
1230
  }
751
1231
  searchMarkets(q, options = {}) {
752
- return this.call("searchMarkets", {
753
- ...options,
754
- query: { q, ...options.query },
755
- });
1232
+ const { query, ...request } = options;
1233
+ return this.list("searchMarkets", { ...request, query: { ...query, q } });
756
1234
  }
757
- exploreMarkets(params = {}) {
758
- return this.list("exploreMarkets", { query: params });
1235
+ searchContent(q, options = {}) {
1236
+ const { query, ...request } = options;
1237
+ return this.list("searchContent", { ...request, query: { ...query, q } });
1238
+ }
1239
+ exploreMarkets(params = {}, options = {}) {
1240
+ return this.list("exploreMarkets", { ...options, query: params });
759
1241
  }
760
1242
  getMarketIntel(conditionId, options = {}) {
761
1243
  return this.call("getMarketIntel", {
@@ -769,37 +1251,71 @@ export class OxinsiderApiClient {
769
1251
  path: { condition_id: conditionId },
770
1252
  });
771
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
+ }
772
1269
  /**
773
1270
  * Fetch bucketed OHLC candles for a market's outcome tokens.
774
- * Resolution defaults to the server default; pass `query.resolution` as
775
- * `"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`.
776
1275
  */
777
- getMarketCandles(conditionId, options = {}) {
1276
+ getMarketCandles(conditionId, params = {}, options = {}) {
778
1277
  return this.call("getMarketCandles", {
779
1278
  ...options,
780
1279
  path: { condition_id: conditionId },
1280
+ query: params,
781
1281
  });
782
1282
  }
783
1283
  getInsiderRadarFlag(id, options = {}) {
784
1284
  return this.call("getInsiderRadarFlag", { ...options, path: { id } });
785
1285
  }
786
- listInsiderRadar(params = {}) {
787
- 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 } });
788
1291
  }
789
1292
  // --- Webhooks ---
790
1293
  listWebhooks(options = {}) {
791
- 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 } });
792
1307
  }
793
1308
  /**
794
1309
  * Fetch the self-describing webhook event catalog (each entry's `id`,
795
1310
  * description, payload shape, and active/dormant status).
796
1311
  */
797
1312
  listWebhookEvents(options = {}) {
798
- return this.call("listWebhookEvents", options);
1313
+ return this.list("listWebhookEvents", options);
799
1314
  }
800
1315
  /** List delivery attempts for one webhook endpoint (Stripe-style list). */
801
- listWebhookDeliveries(webhookId, params = {}) {
1316
+ listWebhookDeliveries(webhookId, params = {}, options = {}) {
802
1317
  return this.list("listWebhookDeliveries", {
1318
+ ...options,
803
1319
  path: { id: webhookId },
804
1320
  query: params,
805
1321
  });
@@ -825,9 +1341,44 @@ export class OxinsiderApiClient {
825
1341
  getApiDiscovery(options = {}) {
826
1342
  return this.call("getApiDiscovery", options);
827
1343
  }
1344
+ getPlatforms(options = {}) {
1345
+ return this.call("getPlatforms", options);
1346
+ }
828
1347
  getAccountIdentity(options = {}) {
829
1348
  return this.call("getAccountIdentity", options);
830
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
+ }
831
1382
  getUsage(options = {}) {
832
1383
  return this.call("getUsage", options);
833
1384
  }
@@ -862,12 +1413,16 @@ export class OxinsiderApiClient {
862
1413
  getApiKey() {
863
1414
  return this.apiKey;
864
1415
  }
1416
+ /** Whether this client was built in sandbox mode (#16138). */
1417
+ isSandbox() {
1418
+ return this.sandbox;
1419
+ }
865
1420
  /** The resolved fetch implementation (used by the SSE stream consumer). */
866
1421
  getFetch() {
867
1422
  return this.fetchImpl;
868
1423
  }
869
1424
  buildUrl(operation, options) {
870
- const url = new URL(interpolatePath(operation.path, options.path ?? {}), `${this.baseUrl}/`);
1425
+ const url = resolveApiUrl(this.baseUrl, interpolatePath(operation.path, options.path ?? {}));
871
1426
  for (const [key, value] of Object.entries(options.query ?? {})) {
872
1427
  if (value === null || value === undefined) {
873
1428
  continue;
@@ -883,9 +1438,9 @@ export class OxinsiderApiClient {
883
1438
  }
884
1439
  return url.toString();
885
1440
  }
886
- buildHeaders(operation, options) {
1441
+ buildHeaders(operation, options, accept = "application/json") {
887
1442
  const headers = new Headers(options.headers);
888
- headers.set("accept", "application/json");
1443
+ headers.set("accept", accept);
889
1444
  if (options.body !== undefined && !headers.has("content-type")) {
890
1445
  headers.set("content-type", "application/json");
891
1446
  }
@@ -895,9 +1450,18 @@ export class OxinsiderApiClient {
895
1450
  if (options.idempotencyKey !== undefined) {
896
1451
  headers.set("idempotency-key", options.idempotencyKey);
897
1452
  }
1453
+ if (options.strictQuery) {
1454
+ headers.set("x-query-validation", "strict");
1455
+ }
898
1456
  return headers;
899
1457
  }
900
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
+ */
901
1465
  export function isApiNotModifiedResponse(response) {
902
1466
  return response.object === "not_modified";
903
1467
  }
@@ -925,13 +1489,6 @@ async function parseJson(response) {
925
1489
  return text;
926
1490
  }
927
1491
  }
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
1492
  function notModifiedResponse(response) {
936
1493
  const meta = { status: 304 };
937
1494
  const etag = response.headers.get("etag");
@@ -942,6 +1499,14 @@ function notModifiedResponse(response) {
942
1499
  if (requestId) {
943
1500
  meta.request_id = requestId;
944
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
+ }
945
1510
  return { object: "not_modified", data: null, meta };
946
1511
  }
947
1512
  function isApiEnvelope(body) {
@@ -950,14 +1515,23 @@ function isApiEnvelope(body) {
950
1515
  "object" in body &&
951
1516
  "data" in body);
952
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
+ */
953
1525
  export function isListEnvelope(body) {
954
- return (typeof body === "object" &&
955
- body !== null &&
956
- "object" in body &&
957
- body.object === "list" &&
958
- "data" in body &&
959
- Array.isArray(body.data) &&
960
- "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"));
961
1535
  }
962
1536
  function sportsEdgeObservationsResponse(response) {
963
1537
  const candidate = response;