pmxtjs 2.43.25 → 2.44.1

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.
@@ -157,6 +157,14 @@ export declare abstract class Exchange {
157
157
  * never use the GET read path.
158
158
  */
159
159
  protected sidecarPostRequest(methodName: string, args: unknown[]): Promise<any>;
160
+ /**
161
+ * Read a hosted catalog endpoint directly.
162
+ *
163
+ * Hosted-only Router APIs such as matched clusters are not part of the
164
+ * core sidecar method namespace. They live under /v0 and return their own
165
+ * response envelopes, so callers intentionally receive the raw JSON body.
166
+ */
167
+ protected catalogReadRequest(path: string, query?: Record<string, unknown>): Promise<any>;
160
168
  loadMarkets(reload?: boolean): Promise<Record<string, UnifiedMarket>>;
161
169
  fetchMarkets(params?: MarketFetchParams): Promise<UnifiedMarket[]>;
162
170
  fetchMarketsPaginated(params?: any): Promise<PaginatedMarketsResult>;
@@ -484,6 +484,29 @@ export class Exchange {
484
484
  }
485
485
  return response.json();
486
486
  }
487
+ /**
488
+ * Read a hosted catalog endpoint directly.
489
+ *
490
+ * Hosted-only Router APIs such as matched clusters are not part of the
491
+ * core sidecar method namespace. They live under /v0 and return their own
492
+ * response envelopes, so callers intentionally receive the raw JSON body.
493
+ */
494
+ async catalogReadRequest(path, query = {}) {
495
+ const qs = buildSidecarQueryString(query);
496
+ const url = `${this.resolveBaseUrl()}${path}${qs ? `?${qs}` : ''}`;
497
+ const response = await this.fetchWithRetry(url, {
498
+ method: 'GET',
499
+ headers: this.getAuthHeaders(),
500
+ });
501
+ if (!response.ok) {
502
+ const body = await response.json().catch(() => ({}));
503
+ if (body.error && typeof body.error === "object") {
504
+ throw fromServerError(body.error);
505
+ }
506
+ throw new PmxtError(body.error?.message || response.statusText);
507
+ }
508
+ return response.json();
509
+ }
487
510
  // BEGIN GENERATED METHODS
488
511
  async loadMarkets(reload = false) {
489
512
  await this.initPromise;
@@ -639,6 +639,96 @@ export interface EventMatchResult extends Readonly<UnifiedEvent> {
639
639
  /** Cross-venue market matches within this event. */
640
640
  marketMatches: MatchResult[];
641
641
  }
642
+ export type MatchedClusterSort = 'volume' | 'confidence';
643
+ /** Shared filters for matched market/event cluster discovery. */
644
+ export interface MatchedClusterFilterParams {
645
+ /** Text search across cluster titles. */
646
+ query?: string;
647
+ /** Filter both sides of matched edges by category. */
648
+ category?: string;
649
+ /** Comma-separated relation filter. */
650
+ relations?: MatchRelation | MatchRelation[] | string;
651
+ /** Single relation filter. Alias for relations. */
652
+ relation?: MatchRelation;
653
+ /** Minimum match confidence score (0.0 to 1.0). */
654
+ minConfidence?: number;
655
+ /** Venue allow-list. */
656
+ venues?: string | string[];
657
+ /** Venue deny-list. */
658
+ excludeVenues?: string | string[];
659
+ /** Minimum number of venues required in a cluster. */
660
+ minVenues?: number;
661
+ /** Require live orderbook coverage on matched edges. */
662
+ withOrderbook?: boolean;
663
+ /** Only include matches updated after this timestamp. */
664
+ updatedSince?: string | Date;
665
+ /** Include the pairwise match edges used to build each cluster. */
666
+ includeRawMatches?: boolean;
667
+ /** Cluster sort order. */
668
+ sort?: MatchedClusterSort;
669
+ /** Maximum clusters to return. */
670
+ limit?: number;
671
+ /** Pagination offset. */
672
+ offset?: number;
673
+ /** Maximum pairwise edges to scan before clustering. */
674
+ edgeLimit?: number;
675
+ }
676
+ /** Parameters for fetching matched market clusters. */
677
+ export interface FetchMatchedMarketClustersParams extends MatchedClusterFilterParams {
678
+ /** Pass a UnifiedMarket directly instead of marketId/slug/url. */
679
+ market?: UnifiedMarket;
680
+ /** Anchor the response to a specific market ID. */
681
+ marketId?: string;
682
+ /** Anchor the response to a specific market slug. */
683
+ slug?: string;
684
+ /** Anchor the response to a specific market URL. */
685
+ url?: string;
686
+ }
687
+ /** Parameters for fetching matched event clusters. */
688
+ export interface FetchMatchedEventClustersParams extends MatchedClusterFilterParams {
689
+ /** Pass a UnifiedEvent directly instead of eventId/slug/url. */
690
+ event?: UnifiedEvent;
691
+ /** Anchor the response to a specific event ID. */
692
+ eventId?: string;
693
+ /** Anchor the response to a specific event slug. */
694
+ slug?: string;
695
+ /** Anchor the response to a specific event URL. */
696
+ url?: string;
697
+ }
698
+ /** Pairwise edge used to build a matched market cluster. */
699
+ export interface MatchedMarketClusterEdge {
700
+ marketAId: string;
701
+ marketBId: string;
702
+ relation: MatchRelation;
703
+ confidence: number;
704
+ }
705
+ /** Pairwise edge used to build a matched event cluster. */
706
+ export interface MatchedEventClusterEdge extends MatchedMarketClusterEdge {
707
+ eventAId: string;
708
+ eventBId: string;
709
+ }
710
+ /** Connected cluster of semantically matched markets across venues. */
711
+ export interface MatchedMarketCluster {
712
+ clusterId: string;
713
+ canonicalTitle: string | null;
714
+ category?: string | null;
715
+ relations: MatchRelation[];
716
+ confidence: number;
717
+ volume24h: number;
718
+ markets: UnifiedMarket[];
719
+ rawMatches?: MatchedMarketClusterEdge[];
720
+ }
721
+ /** Connected cluster of semantically matched events across venues. */
722
+ export interface MatchedEventCluster {
723
+ clusterId: string;
724
+ canonicalTitle: string | null;
725
+ category?: string | null;
726
+ relations: MatchRelation[];
727
+ confidence: number;
728
+ volume24h: number;
729
+ events: UnifiedEvent[];
730
+ rawMatches?: MatchedEventClusterEdge[];
731
+ }
642
732
  /** Side-by-side price comparison for a matched market. */
643
733
  export interface PriceComparison {
644
734
  /** The matched market. */
@@ -5,7 +5,7 @@
5
5
  * every venue PMXT supports. Only requires a PMXT API key.
6
6
  */
7
7
  import { Exchange } from "./client.js";
8
- import { MatchResult, MatchRelation, EventMatchResult, PriceComparison, ArbitrageOpportunity, UnifiedMarket, UnifiedEvent } from "./models.js";
8
+ import { MatchResult, MatchRelation, EventMatchResult, FetchMatchedEventClustersParams, FetchMatchedMarketClustersParams, MatchedEventCluster, MatchedMarketCluster, PriceComparison, ArbitrageOpportunity, UnifiedMarket, UnifiedEvent } from "./models.js";
9
9
  /** Options for creating a Router. */
10
10
  export interface RouterOptions {
11
11
  /** PMXT API key (required for hosted mode). */
@@ -78,6 +78,20 @@ export declare class Router extends Exchange {
78
78
  limit?: number;
79
79
  includePrices?: boolean;
80
80
  }): Promise<EventMatchResult[]>;
81
+ /**
82
+ * Fetch connected clusters of semantically matched markets across venues.
83
+ *
84
+ * @param marketOrParams - A UnifiedMarket, or an options object.
85
+ */
86
+ fetchMatchedMarketClusters(market: UnifiedMarket): Promise<MatchedMarketCluster[]>;
87
+ fetchMatchedMarketClusters(params?: FetchMatchedMarketClustersParams): Promise<MatchedMarketCluster[]>;
88
+ /**
89
+ * Fetch connected clusters of semantically matched events across venues.
90
+ *
91
+ * @param eventOrParams - A UnifiedEvent, or an options object.
92
+ */
93
+ fetchMatchedEventClusters(event: UnifiedEvent): Promise<MatchedEventCluster[]>;
94
+ fetchMatchedEventClusters(params?: FetchMatchedEventClustersParams): Promise<MatchedEventCluster[]>;
81
95
  /**
82
96
  * Compare prices for the same market across venues.
83
97
  *
@@ -13,6 +13,8 @@ function convertMarket(raw) {
13
13
  label: o.label,
14
14
  price: o.price,
15
15
  priceChange24h: o.priceChange24h,
16
+ bestBid: o.bestBid,
17
+ bestAsk: o.bestAsk,
16
18
  metadata: o.metadata,
17
19
  }));
18
20
  const convertOutcome = (o) => o ? ({
@@ -21,6 +23,8 @@ function convertMarket(raw) {
21
23
  label: o.label,
22
24
  price: o.price,
23
25
  priceChange24h: o.priceChange24h,
26
+ bestBid: o.bestBid,
27
+ bestAsk: o.bestAsk,
24
28
  metadata: o.metadata,
25
29
  }) : undefined;
26
30
  return {
@@ -79,6 +83,58 @@ function parseMatchResult(raw) {
79
83
  sourceMarket: raw.sourceMarket ? convertMarket(raw.sourceMarket) : undefined,
80
84
  };
81
85
  }
86
+ function normalizeQueryValue(value) {
87
+ if (value instanceof Date)
88
+ return value.toISOString();
89
+ return value;
90
+ }
91
+ function appendQuery(query, key, value) {
92
+ if (value !== undefined && value !== null && value !== '') {
93
+ query[key] = normalizeQueryValue(value);
94
+ }
95
+ }
96
+ function addClusterFilters(query, params) {
97
+ appendQuery(query, 'query', params.query);
98
+ appendQuery(query, 'category', params.category);
99
+ appendQuery(query, 'relations', params.relations);
100
+ appendQuery(query, 'relation', params.relation);
101
+ appendQuery(query, 'minConfidence', params.minConfidence);
102
+ appendQuery(query, 'venues', params.venues);
103
+ appendQuery(query, 'excludeVenues', params.excludeVenues);
104
+ appendQuery(query, 'minVenues', params.minVenues);
105
+ appendQuery(query, 'withOrderbook', params.withOrderbook);
106
+ appendQuery(query, 'updatedSince', params.updatedSince);
107
+ appendQuery(query, 'includeRawMatches', params.includeRawMatches);
108
+ appendQuery(query, 'sort', params.sort);
109
+ appendQuery(query, 'limit', params.limit);
110
+ appendQuery(query, 'offset', params.offset);
111
+ appendQuery(query, 'edgeLimit', params.edgeLimit);
112
+ }
113
+ function extractCatalogArray(raw) {
114
+ if (Array.isArray(raw))
115
+ return raw;
116
+ if (Array.isArray(raw?.data))
117
+ return raw.data;
118
+ return [];
119
+ }
120
+ function isUnifiedMarket(value) {
121
+ return Boolean(value && 'marketId' in value && 'title' in value);
122
+ }
123
+ function isUnifiedEvent(value) {
124
+ return Boolean(value && 'id' in value && 'title' in value && 'markets' in value);
125
+ }
126
+ function parseMatchedMarketCluster(raw) {
127
+ return {
128
+ ...raw,
129
+ markets: (raw.markets || []).map(convertMarket),
130
+ };
131
+ }
132
+ function parseMatchedEventCluster(raw) {
133
+ return {
134
+ ...raw,
135
+ events: (raw.events || []).map(convertEvent),
136
+ };
137
+ }
82
138
  /**
83
139
  * Cross-venue intelligence layer.
84
140
  *
@@ -172,6 +228,52 @@ export class Router extends Exchange {
172
228
  throw new Error(`Failed to fetchEventMatches: ${error}`);
173
229
  }
174
230
  }
231
+ async fetchMatchedMarketClusters(marketOrParams = {}) {
232
+ const params = isUnifiedMarket(marketOrParams)
233
+ ? { market: marketOrParams }
234
+ : marketOrParams;
235
+ await this.initPromise;
236
+ const query = {};
237
+ const marketId = params.marketId ?? params.market?.marketId;
238
+ const slug = params.slug ?? (!marketId ? params.market?.slug : undefined);
239
+ const url = params.url ?? (!marketId && !slug ? params.market?.url : undefined);
240
+ appendQuery(query, 'marketId', marketId);
241
+ appendQuery(query, 'slug', slug);
242
+ appendQuery(query, 'url', url);
243
+ addClusterFilters(query, params);
244
+ try {
245
+ const raw = await this.catalogReadRequest('/v0/matched-market-clusters', query);
246
+ return extractCatalogArray(raw).map(parseMatchedMarketCluster);
247
+ }
248
+ catch (error) {
249
+ if (error instanceof Error)
250
+ throw error;
251
+ throw new Error(`Failed to fetchMatchedMarketClusters: ${error}`);
252
+ }
253
+ }
254
+ async fetchMatchedEventClusters(eventOrParams = {}) {
255
+ const params = isUnifiedEvent(eventOrParams)
256
+ ? { event: eventOrParams }
257
+ : eventOrParams;
258
+ await this.initPromise;
259
+ const query = {};
260
+ const eventId = params.eventId ?? params.event?.id;
261
+ const slug = params.slug ?? (!eventId ? params.event?.slug : undefined);
262
+ const url = params.url ?? (!eventId && !slug ? params.event?.url : undefined);
263
+ appendQuery(query, 'eventId', eventId);
264
+ appendQuery(query, 'slug', slug);
265
+ appendQuery(query, 'url', url);
266
+ addClusterFilters(query, params);
267
+ try {
268
+ const raw = await this.catalogReadRequest('/v0/matched-event-clusters', query);
269
+ return extractCatalogArray(raw).map(parseMatchedEventCluster);
270
+ }
271
+ catch (error) {
272
+ if (error instanceof Error)
273
+ throw error;
274
+ throw new Error(`Failed to fetchMatchedEventClusters: ${error}`);
275
+ }
276
+ }
175
277
  async compareMarketPrices(marketOrParams = {}) {
176
278
  const params = 'title' in marketOrParams ? { market: marketOrParams } : marketOrParams;
177
279
  await this.initPromise;
@@ -157,6 +157,14 @@ export declare abstract class Exchange {
157
157
  * never use the GET read path.
158
158
  */
159
159
  protected sidecarPostRequest(methodName: string, args: unknown[]): Promise<any>;
160
+ /**
161
+ * Read a hosted catalog endpoint directly.
162
+ *
163
+ * Hosted-only Router APIs such as matched clusters are not part of the
164
+ * core sidecar method namespace. They live under /v0 and return their own
165
+ * response envelopes, so callers intentionally receive the raw JSON body.
166
+ */
167
+ protected catalogReadRequest(path: string, query?: Record<string, unknown>): Promise<any>;
160
168
  loadMarkets(reload?: boolean): Promise<Record<string, UnifiedMarket>>;
161
169
  fetchMarkets(params?: MarketFetchParams): Promise<UnifiedMarket[]>;
162
170
  fetchMarketsPaginated(params?: any): Promise<PaginatedMarketsResult>;
@@ -487,6 +487,29 @@ class Exchange {
487
487
  }
488
488
  return response.json();
489
489
  }
490
+ /**
491
+ * Read a hosted catalog endpoint directly.
492
+ *
493
+ * Hosted-only Router APIs such as matched clusters are not part of the
494
+ * core sidecar method namespace. They live under /v0 and return their own
495
+ * response envelopes, so callers intentionally receive the raw JSON body.
496
+ */
497
+ async catalogReadRequest(path, query = {}) {
498
+ const qs = buildSidecarQueryString(query);
499
+ const url = `${this.resolveBaseUrl()}${path}${qs ? `?${qs}` : ''}`;
500
+ const response = await this.fetchWithRetry(url, {
501
+ method: 'GET',
502
+ headers: this.getAuthHeaders(),
503
+ });
504
+ if (!response.ok) {
505
+ const body = await response.json().catch(() => ({}));
506
+ if (body.error && typeof body.error === "object") {
507
+ throw (0, errors_js_1.fromServerError)(body.error);
508
+ }
509
+ throw new errors_js_1.PmxtError(body.error?.message || response.statusText);
510
+ }
511
+ return response.json();
512
+ }
490
513
  // BEGIN GENERATED METHODS
491
514
  async loadMarkets(reload = false) {
492
515
  await this.initPromise;
@@ -639,6 +639,96 @@ export interface EventMatchResult extends Readonly<UnifiedEvent> {
639
639
  /** Cross-venue market matches within this event. */
640
640
  marketMatches: MatchResult[];
641
641
  }
642
+ export type MatchedClusterSort = 'volume' | 'confidence';
643
+ /** Shared filters for matched market/event cluster discovery. */
644
+ export interface MatchedClusterFilterParams {
645
+ /** Text search across cluster titles. */
646
+ query?: string;
647
+ /** Filter both sides of matched edges by category. */
648
+ category?: string;
649
+ /** Comma-separated relation filter. */
650
+ relations?: MatchRelation | MatchRelation[] | string;
651
+ /** Single relation filter. Alias for relations. */
652
+ relation?: MatchRelation;
653
+ /** Minimum match confidence score (0.0 to 1.0). */
654
+ minConfidence?: number;
655
+ /** Venue allow-list. */
656
+ venues?: string | string[];
657
+ /** Venue deny-list. */
658
+ excludeVenues?: string | string[];
659
+ /** Minimum number of venues required in a cluster. */
660
+ minVenues?: number;
661
+ /** Require live orderbook coverage on matched edges. */
662
+ withOrderbook?: boolean;
663
+ /** Only include matches updated after this timestamp. */
664
+ updatedSince?: string | Date;
665
+ /** Include the pairwise match edges used to build each cluster. */
666
+ includeRawMatches?: boolean;
667
+ /** Cluster sort order. */
668
+ sort?: MatchedClusterSort;
669
+ /** Maximum clusters to return. */
670
+ limit?: number;
671
+ /** Pagination offset. */
672
+ offset?: number;
673
+ /** Maximum pairwise edges to scan before clustering. */
674
+ edgeLimit?: number;
675
+ }
676
+ /** Parameters for fetching matched market clusters. */
677
+ export interface FetchMatchedMarketClustersParams extends MatchedClusterFilterParams {
678
+ /** Pass a UnifiedMarket directly instead of marketId/slug/url. */
679
+ market?: UnifiedMarket;
680
+ /** Anchor the response to a specific market ID. */
681
+ marketId?: string;
682
+ /** Anchor the response to a specific market slug. */
683
+ slug?: string;
684
+ /** Anchor the response to a specific market URL. */
685
+ url?: string;
686
+ }
687
+ /** Parameters for fetching matched event clusters. */
688
+ export interface FetchMatchedEventClustersParams extends MatchedClusterFilterParams {
689
+ /** Pass a UnifiedEvent directly instead of eventId/slug/url. */
690
+ event?: UnifiedEvent;
691
+ /** Anchor the response to a specific event ID. */
692
+ eventId?: string;
693
+ /** Anchor the response to a specific event slug. */
694
+ slug?: string;
695
+ /** Anchor the response to a specific event URL. */
696
+ url?: string;
697
+ }
698
+ /** Pairwise edge used to build a matched market cluster. */
699
+ export interface MatchedMarketClusterEdge {
700
+ marketAId: string;
701
+ marketBId: string;
702
+ relation: MatchRelation;
703
+ confidence: number;
704
+ }
705
+ /** Pairwise edge used to build a matched event cluster. */
706
+ export interface MatchedEventClusterEdge extends MatchedMarketClusterEdge {
707
+ eventAId: string;
708
+ eventBId: string;
709
+ }
710
+ /** Connected cluster of semantically matched markets across venues. */
711
+ export interface MatchedMarketCluster {
712
+ clusterId: string;
713
+ canonicalTitle: string | null;
714
+ category?: string | null;
715
+ relations: MatchRelation[];
716
+ confidence: number;
717
+ volume24h: number;
718
+ markets: UnifiedMarket[];
719
+ rawMatches?: MatchedMarketClusterEdge[];
720
+ }
721
+ /** Connected cluster of semantically matched events across venues. */
722
+ export interface MatchedEventCluster {
723
+ clusterId: string;
724
+ canonicalTitle: string | null;
725
+ category?: string | null;
726
+ relations: MatchRelation[];
727
+ confidence: number;
728
+ volume24h: number;
729
+ events: UnifiedEvent[];
730
+ rawMatches?: MatchedEventClusterEdge[];
731
+ }
642
732
  /** Side-by-side price comparison for a matched market. */
643
733
  export interface PriceComparison {
644
734
  /** The matched market. */
@@ -5,7 +5,7 @@
5
5
  * every venue PMXT supports. Only requires a PMXT API key.
6
6
  */
7
7
  import { Exchange } from "./client.js";
8
- import { MatchResult, MatchRelation, EventMatchResult, PriceComparison, ArbitrageOpportunity, UnifiedMarket, UnifiedEvent } from "./models.js";
8
+ import { MatchResult, MatchRelation, EventMatchResult, FetchMatchedEventClustersParams, FetchMatchedMarketClustersParams, MatchedEventCluster, MatchedMarketCluster, PriceComparison, ArbitrageOpportunity, UnifiedMarket, UnifiedEvent } from "./models.js";
9
9
  /** Options for creating a Router. */
10
10
  export interface RouterOptions {
11
11
  /** PMXT API key (required for hosted mode). */
@@ -78,6 +78,20 @@ export declare class Router extends Exchange {
78
78
  limit?: number;
79
79
  includePrices?: boolean;
80
80
  }): Promise<EventMatchResult[]>;
81
+ /**
82
+ * Fetch connected clusters of semantically matched markets across venues.
83
+ *
84
+ * @param marketOrParams - A UnifiedMarket, or an options object.
85
+ */
86
+ fetchMatchedMarketClusters(market: UnifiedMarket): Promise<MatchedMarketCluster[]>;
87
+ fetchMatchedMarketClusters(params?: FetchMatchedMarketClustersParams): Promise<MatchedMarketCluster[]>;
88
+ /**
89
+ * Fetch connected clusters of semantically matched events across venues.
90
+ *
91
+ * @param eventOrParams - A UnifiedEvent, or an options object.
92
+ */
93
+ fetchMatchedEventClusters(event: UnifiedEvent): Promise<MatchedEventCluster[]>;
94
+ fetchMatchedEventClusters(params?: FetchMatchedEventClustersParams): Promise<MatchedEventCluster[]>;
81
95
  /**
82
96
  * Compare prices for the same market across venues.
83
97
  *
@@ -49,6 +49,8 @@ function convertMarket(raw) {
49
49
  label: o.label,
50
50
  price: o.price,
51
51
  priceChange24h: o.priceChange24h,
52
+ bestBid: o.bestBid,
53
+ bestAsk: o.bestAsk,
52
54
  metadata: o.metadata,
53
55
  }));
54
56
  const convertOutcome = (o) => o ? ({
@@ -57,6 +59,8 @@ function convertMarket(raw) {
57
59
  label: o.label,
58
60
  price: o.price,
59
61
  priceChange24h: o.priceChange24h,
62
+ bestBid: o.bestBid,
63
+ bestAsk: o.bestAsk,
60
64
  metadata: o.metadata,
61
65
  }) : undefined;
62
66
  return {
@@ -115,6 +119,58 @@ function parseMatchResult(raw) {
115
119
  sourceMarket: raw.sourceMarket ? convertMarket(raw.sourceMarket) : undefined,
116
120
  };
117
121
  }
122
+ function normalizeQueryValue(value) {
123
+ if (value instanceof Date)
124
+ return value.toISOString();
125
+ return value;
126
+ }
127
+ function appendQuery(query, key, value) {
128
+ if (value !== undefined && value !== null && value !== '') {
129
+ query[key] = normalizeQueryValue(value);
130
+ }
131
+ }
132
+ function addClusterFilters(query, params) {
133
+ appendQuery(query, 'query', params.query);
134
+ appendQuery(query, 'category', params.category);
135
+ appendQuery(query, 'relations', params.relations);
136
+ appendQuery(query, 'relation', params.relation);
137
+ appendQuery(query, 'minConfidence', params.minConfidence);
138
+ appendQuery(query, 'venues', params.venues);
139
+ appendQuery(query, 'excludeVenues', params.excludeVenues);
140
+ appendQuery(query, 'minVenues', params.minVenues);
141
+ appendQuery(query, 'withOrderbook', params.withOrderbook);
142
+ appendQuery(query, 'updatedSince', params.updatedSince);
143
+ appendQuery(query, 'includeRawMatches', params.includeRawMatches);
144
+ appendQuery(query, 'sort', params.sort);
145
+ appendQuery(query, 'limit', params.limit);
146
+ appendQuery(query, 'offset', params.offset);
147
+ appendQuery(query, 'edgeLimit', params.edgeLimit);
148
+ }
149
+ function extractCatalogArray(raw) {
150
+ if (Array.isArray(raw))
151
+ return raw;
152
+ if (Array.isArray(raw?.data))
153
+ return raw.data;
154
+ return [];
155
+ }
156
+ function isUnifiedMarket(value) {
157
+ return Boolean(value && 'marketId' in value && 'title' in value);
158
+ }
159
+ function isUnifiedEvent(value) {
160
+ return Boolean(value && 'id' in value && 'title' in value && 'markets' in value);
161
+ }
162
+ function parseMatchedMarketCluster(raw) {
163
+ return {
164
+ ...raw,
165
+ markets: (raw.markets || []).map(convertMarket),
166
+ };
167
+ }
168
+ function parseMatchedEventCluster(raw) {
169
+ return {
170
+ ...raw,
171
+ events: (raw.events || []).map(convertEvent),
172
+ };
173
+ }
118
174
  /**
119
175
  * Cross-venue intelligence layer.
120
176
  *
@@ -208,6 +264,52 @@ class Router extends client_js_1.Exchange {
208
264
  throw new Error(`Failed to fetchEventMatches: ${error}`);
209
265
  }
210
266
  }
267
+ async fetchMatchedMarketClusters(marketOrParams = {}) {
268
+ const params = isUnifiedMarket(marketOrParams)
269
+ ? { market: marketOrParams }
270
+ : marketOrParams;
271
+ await this.initPromise;
272
+ const query = {};
273
+ const marketId = params.marketId ?? params.market?.marketId;
274
+ const slug = params.slug ?? (!marketId ? params.market?.slug : undefined);
275
+ const url = params.url ?? (!marketId && !slug ? params.market?.url : undefined);
276
+ appendQuery(query, 'marketId', marketId);
277
+ appendQuery(query, 'slug', slug);
278
+ appendQuery(query, 'url', url);
279
+ addClusterFilters(query, params);
280
+ try {
281
+ const raw = await this.catalogReadRequest('/v0/matched-market-clusters', query);
282
+ return extractCatalogArray(raw).map(parseMatchedMarketCluster);
283
+ }
284
+ catch (error) {
285
+ if (error instanceof Error)
286
+ throw error;
287
+ throw new Error(`Failed to fetchMatchedMarketClusters: ${error}`);
288
+ }
289
+ }
290
+ async fetchMatchedEventClusters(eventOrParams = {}) {
291
+ const params = isUnifiedEvent(eventOrParams)
292
+ ? { event: eventOrParams }
293
+ : eventOrParams;
294
+ await this.initPromise;
295
+ const query = {};
296
+ const eventId = params.eventId ?? params.event?.id;
297
+ const slug = params.slug ?? (!eventId ? params.event?.slug : undefined);
298
+ const url = params.url ?? (!eventId && !slug ? params.event?.url : undefined);
299
+ appendQuery(query, 'eventId', eventId);
300
+ appendQuery(query, 'slug', slug);
301
+ appendQuery(query, 'url', url);
302
+ addClusterFilters(query, params);
303
+ try {
304
+ const raw = await this.catalogReadRequest('/v0/matched-event-clusters', query);
305
+ return extractCatalogArray(raw).map(parseMatchedEventCluster);
306
+ }
307
+ catch (error) {
308
+ if (error instanceof Error)
309
+ throw error;
310
+ throw new Error(`Failed to fetchMatchedEventClusters: ${error}`);
311
+ }
312
+ }
211
313
  async compareMarketPrices(marketOrParams = {}) {
212
314
  const params = 'title' in marketOrParams ? { market: marketOrParams } : marketOrParams;
213
315
  await this.initPromise;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxtjs",
3
- "version": "2.43.25",
3
+ "version": "2.44.1",
4
4
  "description": "OpenAPI client for pmxtjs",
5
5
  "author": "OpenAPI-Generator",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxtjs",
3
- "version": "2.43.25",
3
+ "version": "2.44.1",
4
4
  "description": "Unified prediction market data API - The ccxt for prediction markets",
5
5
  "author": "PMXT Contributors",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  "unified"
44
44
  ],
45
45
  "dependencies": {
46
- "pmxt-core": "2.43.25",
46
+ "pmxt-core": "2.44.1",
47
47
  "ws": "^8.18.0"
48
48
  },
49
49
  "devDependencies": {
package/pmxt/client.ts CHANGED
@@ -632,6 +632,30 @@ export abstract class Exchange {
632
632
  return response.json();
633
633
  }
634
634
 
635
+ /**
636
+ * Read a hosted catalog endpoint directly.
637
+ *
638
+ * Hosted-only Router APIs such as matched clusters are not part of the
639
+ * core sidecar method namespace. They live under /v0 and return their own
640
+ * response envelopes, so callers intentionally receive the raw JSON body.
641
+ */
642
+ protected async catalogReadRequest(path: string, query: Record<string, unknown> = {}): Promise<any> {
643
+ const qs = buildSidecarQueryString(query);
644
+ const url = `${this.resolveBaseUrl()}${path}${qs ? `?${qs}` : ''}`;
645
+ const response = await this.fetchWithRetry(url, {
646
+ method: 'GET',
647
+ headers: this.getAuthHeaders(),
648
+ });
649
+ if (!response.ok) {
650
+ const body = await response.json().catch(() => ({}));
651
+ if (body.error && typeof body.error === "object") {
652
+ throw fromServerError(body.error);
653
+ }
654
+ throw new PmxtError(body.error?.message || response.statusText);
655
+ }
656
+ return response.json();
657
+ }
658
+
635
659
  // BEGIN GENERATED METHODS
636
660
 
637
661
  async loadMarkets(reload: boolean = false): Promise<Record<string, UnifiedMarket>> {
package/pmxt/models.ts CHANGED
@@ -861,6 +861,124 @@ export interface EventMatchResult extends Readonly<UnifiedEvent> {
861
861
  marketMatches: MatchResult[];
862
862
  }
863
863
 
864
+ export type MatchedClusterSort = 'volume' | 'confidence';
865
+
866
+ /** Shared filters for matched market/event cluster discovery. */
867
+ export interface MatchedClusterFilterParams {
868
+ /** Text search across cluster titles. */
869
+ query?: string;
870
+
871
+ /** Filter both sides of matched edges by category. */
872
+ category?: string;
873
+
874
+ /** Comma-separated relation filter. */
875
+ relations?: MatchRelation | MatchRelation[] | string;
876
+
877
+ /** Single relation filter. Alias for relations. */
878
+ relation?: MatchRelation;
879
+
880
+ /** Minimum match confidence score (0.0 to 1.0). */
881
+ minConfidence?: number;
882
+
883
+ /** Venue allow-list. */
884
+ venues?: string | string[];
885
+
886
+ /** Venue deny-list. */
887
+ excludeVenues?: string | string[];
888
+
889
+ /** Minimum number of venues required in a cluster. */
890
+ minVenues?: number;
891
+
892
+ /** Require live orderbook coverage on matched edges. */
893
+ withOrderbook?: boolean;
894
+
895
+ /** Only include matches updated after this timestamp. */
896
+ updatedSince?: string | Date;
897
+
898
+ /** Include the pairwise match edges used to build each cluster. */
899
+ includeRawMatches?: boolean;
900
+
901
+ /** Cluster sort order. */
902
+ sort?: MatchedClusterSort;
903
+
904
+ /** Maximum clusters to return. */
905
+ limit?: number;
906
+
907
+ /** Pagination offset. */
908
+ offset?: number;
909
+
910
+ /** Maximum pairwise edges to scan before clustering. */
911
+ edgeLimit?: number;
912
+ }
913
+
914
+ /** Parameters for fetching matched market clusters. */
915
+ export interface FetchMatchedMarketClustersParams extends MatchedClusterFilterParams {
916
+ /** Pass a UnifiedMarket directly instead of marketId/slug/url. */
917
+ market?: UnifiedMarket;
918
+
919
+ /** Anchor the response to a specific market ID. */
920
+ marketId?: string;
921
+
922
+ /** Anchor the response to a specific market slug. */
923
+ slug?: string;
924
+
925
+ /** Anchor the response to a specific market URL. */
926
+ url?: string;
927
+ }
928
+
929
+ /** Parameters for fetching matched event clusters. */
930
+ export interface FetchMatchedEventClustersParams extends MatchedClusterFilterParams {
931
+ /** Pass a UnifiedEvent directly instead of eventId/slug/url. */
932
+ event?: UnifiedEvent;
933
+
934
+ /** Anchor the response to a specific event ID. */
935
+ eventId?: string;
936
+
937
+ /** Anchor the response to a specific event slug. */
938
+ slug?: string;
939
+
940
+ /** Anchor the response to a specific event URL. */
941
+ url?: string;
942
+ }
943
+
944
+ /** Pairwise edge used to build a matched market cluster. */
945
+ export interface MatchedMarketClusterEdge {
946
+ marketAId: string;
947
+ marketBId: string;
948
+ relation: MatchRelation;
949
+ confidence: number;
950
+ }
951
+
952
+ /** Pairwise edge used to build a matched event cluster. */
953
+ export interface MatchedEventClusterEdge extends MatchedMarketClusterEdge {
954
+ eventAId: string;
955
+ eventBId: string;
956
+ }
957
+
958
+ /** Connected cluster of semantically matched markets across venues. */
959
+ export interface MatchedMarketCluster {
960
+ clusterId: string;
961
+ canonicalTitle: string | null;
962
+ category?: string | null;
963
+ relations: MatchRelation[];
964
+ confidence: number;
965
+ volume24h: number;
966
+ markets: UnifiedMarket[];
967
+ rawMatches?: MatchedMarketClusterEdge[];
968
+ }
969
+
970
+ /** Connected cluster of semantically matched events across venues. */
971
+ export interface MatchedEventCluster {
972
+ clusterId: string;
973
+ canonicalTitle: string | null;
974
+ category?: string | null;
975
+ relations: MatchRelation[];
976
+ confidence: number;
977
+ volume24h: number;
978
+ events: UnifiedEvent[];
979
+ rawMatches?: MatchedEventClusterEdge[];
980
+ }
981
+
864
982
  /** Side-by-side price comparison for a matched market. */
865
983
  export interface PriceComparison {
866
984
  /** The matched market. */
package/pmxt/router.ts CHANGED
@@ -11,6 +11,10 @@ import {
11
11
  MatchResult,
12
12
  MatchRelation,
13
13
  EventMatchResult,
14
+ FetchMatchedEventClustersParams,
15
+ FetchMatchedMarketClustersParams,
16
+ MatchedEventCluster,
17
+ MatchedMarketCluster,
14
18
  PriceComparison,
15
19
  ArbitrageOpportunity,
16
20
  UnifiedMarket,
@@ -24,6 +28,8 @@ function convertMarket(raw: any): UnifiedMarket {
24
28
  label: o.label,
25
29
  price: o.price,
26
30
  priceChange24h: o.priceChange24h,
31
+ bestBid: o.bestBid,
32
+ bestAsk: o.bestAsk,
27
33
  metadata: o.metadata,
28
34
  }));
29
35
 
@@ -33,6 +39,8 @@ function convertMarket(raw: any): UnifiedMarket {
33
39
  label: o.label,
34
40
  price: o.price,
35
41
  priceChange24h: o.priceChange24h,
42
+ bestBid: o.bestBid,
43
+ bestAsk: o.bestAsk,
36
44
  metadata: o.metadata,
37
45
  }) : undefined;
38
46
 
@@ -95,6 +103,79 @@ function parseMatchResult(raw: any): MatchResult {
95
103
  };
96
104
  }
97
105
 
106
+ function normalizeQueryValue(value: unknown): unknown {
107
+ if (value instanceof Date) return value.toISOString();
108
+ return value;
109
+ }
110
+
111
+ function appendQuery(query: Record<string, unknown>, key: string, value: unknown): void {
112
+ if (value !== undefined && value !== null && value !== '') {
113
+ query[key] = normalizeQueryValue(value);
114
+ }
115
+ }
116
+
117
+ function addClusterFilters(query: Record<string, unknown>, params: {
118
+ query?: string;
119
+ category?: string;
120
+ relations?: MatchRelation | MatchRelation[] | string;
121
+ relation?: MatchRelation;
122
+ minConfidence?: number;
123
+ venues?: string | string[];
124
+ excludeVenues?: string | string[];
125
+ minVenues?: number;
126
+ withOrderbook?: boolean;
127
+ updatedSince?: string | Date;
128
+ includeRawMatches?: boolean;
129
+ sort?: string;
130
+ limit?: number;
131
+ offset?: number;
132
+ edgeLimit?: number;
133
+ }): void {
134
+ appendQuery(query, 'query', params.query);
135
+ appendQuery(query, 'category', params.category);
136
+ appendQuery(query, 'relations', params.relations);
137
+ appendQuery(query, 'relation', params.relation);
138
+ appendQuery(query, 'minConfidence', params.minConfidence);
139
+ appendQuery(query, 'venues', params.venues);
140
+ appendQuery(query, 'excludeVenues', params.excludeVenues);
141
+ appendQuery(query, 'minVenues', params.minVenues);
142
+ appendQuery(query, 'withOrderbook', params.withOrderbook);
143
+ appendQuery(query, 'updatedSince', params.updatedSince);
144
+ appendQuery(query, 'includeRawMatches', params.includeRawMatches);
145
+ appendQuery(query, 'sort', params.sort);
146
+ appendQuery(query, 'limit', params.limit);
147
+ appendQuery(query, 'offset', params.offset);
148
+ appendQuery(query, 'edgeLimit', params.edgeLimit);
149
+ }
150
+
151
+ function extractCatalogArray(raw: any): any[] {
152
+ if (Array.isArray(raw)) return raw;
153
+ if (Array.isArray(raw?.data)) return raw.data;
154
+ return [];
155
+ }
156
+
157
+ function isUnifiedMarket(value: UnifiedMarket | FetchMatchedMarketClustersParams): value is UnifiedMarket {
158
+ return Boolean(value && 'marketId' in value && 'title' in value);
159
+ }
160
+
161
+ function isUnifiedEvent(value: UnifiedEvent | FetchMatchedEventClustersParams): value is UnifiedEvent {
162
+ return Boolean(value && 'id' in value && 'title' in value && 'markets' in value);
163
+ }
164
+
165
+ function parseMatchedMarketCluster(raw: any): MatchedMarketCluster {
166
+ return {
167
+ ...raw,
168
+ markets: (raw.markets || []).map(convertMarket),
169
+ };
170
+ }
171
+
172
+ function parseMatchedEventCluster(raw: any): MatchedEventCluster {
173
+ return {
174
+ ...raw,
175
+ events: (raw.events || []).map(convertEvent),
176
+ };
177
+ }
178
+
98
179
  /** Options for creating a Router. */
99
180
  export interface RouterOptions {
100
181
  /** PMXT API key (required for hosted mode). */
@@ -262,6 +343,72 @@ export class Router extends Exchange {
262
343
  }
263
344
  }
264
345
 
346
+ /**
347
+ * Fetch connected clusters of semantically matched markets across venues.
348
+ *
349
+ * @param marketOrParams - A UnifiedMarket, or an options object.
350
+ */
351
+ async fetchMatchedMarketClusters(market: UnifiedMarket): Promise<MatchedMarketCluster[]>;
352
+ async fetchMatchedMarketClusters(params?: FetchMatchedMarketClustersParams): Promise<MatchedMarketCluster[]>;
353
+ async fetchMatchedMarketClusters(
354
+ marketOrParams: UnifiedMarket | FetchMatchedMarketClustersParams = {},
355
+ ): Promise<MatchedMarketCluster[]> {
356
+ const params: FetchMatchedMarketClustersParams = isUnifiedMarket(marketOrParams)
357
+ ? { market: marketOrParams }
358
+ : marketOrParams;
359
+ await this.initPromise;
360
+
361
+ const query: Record<string, unknown> = {};
362
+ const marketId = params.marketId ?? params.market?.marketId;
363
+ const slug = params.slug ?? (!marketId ? params.market?.slug : undefined);
364
+ const url = params.url ?? (!marketId && !slug ? params.market?.url : undefined);
365
+ appendQuery(query, 'marketId', marketId);
366
+ appendQuery(query, 'slug', slug);
367
+ appendQuery(query, 'url', url);
368
+ addClusterFilters(query, params);
369
+
370
+ try {
371
+ const raw = await this.catalogReadRequest('/v0/matched-market-clusters', query);
372
+ return extractCatalogArray(raw).map(parseMatchedMarketCluster);
373
+ } catch (error) {
374
+ if (error instanceof Error) throw error;
375
+ throw new Error(`Failed to fetchMatchedMarketClusters: ${error}`);
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Fetch connected clusters of semantically matched events across venues.
381
+ *
382
+ * @param eventOrParams - A UnifiedEvent, or an options object.
383
+ */
384
+ async fetchMatchedEventClusters(event: UnifiedEvent): Promise<MatchedEventCluster[]>;
385
+ async fetchMatchedEventClusters(params?: FetchMatchedEventClustersParams): Promise<MatchedEventCluster[]>;
386
+ async fetchMatchedEventClusters(
387
+ eventOrParams: UnifiedEvent | FetchMatchedEventClustersParams = {},
388
+ ): Promise<MatchedEventCluster[]> {
389
+ const params: FetchMatchedEventClustersParams = isUnifiedEvent(eventOrParams)
390
+ ? { event: eventOrParams }
391
+ : eventOrParams;
392
+ await this.initPromise;
393
+
394
+ const query: Record<string, unknown> = {};
395
+ const eventId = params.eventId ?? params.event?.id;
396
+ const slug = params.slug ?? (!eventId ? params.event?.slug : undefined);
397
+ const url = params.url ?? (!eventId && !slug ? params.event?.url : undefined);
398
+ appendQuery(query, 'eventId', eventId);
399
+ appendQuery(query, 'slug', slug);
400
+ appendQuery(query, 'url', url);
401
+ addClusterFilters(query, params);
402
+
403
+ try {
404
+ const raw = await this.catalogReadRequest('/v0/matched-event-clusters', query);
405
+ return extractCatalogArray(raw).map(parseMatchedEventCluster);
406
+ } catch (error) {
407
+ if (error instanceof Error) throw error;
408
+ throw new Error(`Failed to fetchMatchedEventClusters: ${error}`);
409
+ }
410
+ }
411
+
265
412
  // ------------------------------------------------------------------
266
413
  // Price comparison
267
414
  // ------------------------------------------------------------------