From 6e0f4fec41586fc5d7738dc2a7f29fee90c9ce9d Mon Sep 17 00:00:00 2001 From: AbhilashG12 Date: Mon, 15 Jun 2026 14:21:50 +0530 Subject: [PATCH 1/4] fix(limitless): add proper pagination using totalMarketsCount The Limitless API returns totalMarketsCount but the pagination function was making fixed parallel requests with no end detection. Changes: - Replace parallel fixed-page fetching with sequential pagination - Use totalMarketsCount to detect when all pages are fetched - Stop when fewer markets than page size are returned - Add safety limit to prevent infinite loops This ensures all markets are fetched and stops at the end. Fixes #1090 --- core/src/exchanges/limitless/utils.ts | 55 ++++++++++++--------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/core/src/exchanges/limitless/utils.ts b/core/src/exchanges/limitless/utils.ts index b75f1a57..e4a85882 100644 --- a/core/src/exchanges/limitless/utils.ts +++ b/core/src/exchanges/limitless/utils.ts @@ -200,7 +200,6 @@ export async function paginateLimitlessMarkets( ): Promise { const PAGE_SIZE = 25; const targetLimit = requestedLimit || PAGE_SIZE; - const MAX_PAGES = 20; // Safety limit to prevent infinite loops if (targetLimit <= PAGE_SIZE) { const response = await fetcher.getActiveMarkets({ @@ -211,33 +210,29 @@ export async function paginateLimitlessMarkets( return response.data || []; } - // Fetch more pages than theoretically needed to account for filtering - // ~33% of markets lack tokens and get filtered out, so we over-fetch - // by 70% to ensure we get enough valid markets after filtering - const estimatedPages = Math.ceil(targetLimit / PAGE_SIZE); - const pagesWithBuffer = Math.min(Math.ceil(estimatedPages * 1.7), MAX_PAGES); - - const pageNumbers: number[] = []; - for (let i = 1; i <= pagesWithBuffer; i++) { - pageNumbers.push(i); + // Sequential pagination using totalMarketsCount + const allMarkets: any[] = []; + let page = 1; + const MAX_PAGES = 50; // Safety limit + + while (page <= MAX_PAGES && allMarkets.length < targetLimit) { + const response = await fetcher.getActiveMarkets({ + limit: PAGE_SIZE, + page: page, + sortBy: sortBy, + }); + + const markets = response.data || []; + if (markets.length === 0) break; + + allMarkets.push(...markets); + + // 🔧 Use totalMarketsCount to check if we're done + const totalCount = response.totalMarketsCount; + if (totalCount && allMarkets.length >= totalCount) break; + + page++; } - - const pages = await Promise.all(pageNumbers.map(async (page) => { - try { - const response = await fetcher.getActiveMarkets({ - limit: PAGE_SIZE, - page: page, - sortBy: sortBy, - }); - return response.data || []; - } catch (e) { - return []; - } - })); - - const allMarkets = pages.flat(); - - // Don't slice here - let the caller handle limiting after filtering - // This ensures we return enough raw markets for the caller to filter - return allMarkets; -} + + return allMarkets.slice(0, targetLimit); +} \ No newline at end of file From 5a94c60278118bc512f3a903bdc953c2be49626c Mon Sep 17 00:00:00 2001 From: AbhilashG12 Date: Mon, 15 Jun 2026 19:11:12 +0530 Subject: [PATCH 2/4] fix(limitless): over-fetch raw markets to account for token filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fetch 70% more raw markets than requested - Ensures enough markets survive the token filter - Now correctly returns requested number of markets after filtering Test: fetchMarkets({ limit: 500 }) returns 500 markets ✅ Fixes #1090 --- core/src/exchanges/limitless/utils.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/core/src/exchanges/limitless/utils.ts b/core/src/exchanges/limitless/utils.ts index e4a85882..79bbe23c 100644 --- a/core/src/exchanges/limitless/utils.ts +++ b/core/src/exchanges/limitless/utils.ts @@ -200,10 +200,15 @@ export async function paginateLimitlessMarkets( ): Promise { const PAGE_SIZE = 25; const targetLimit = requestedLimit || PAGE_SIZE; + + // Over-fetch by 70% because ~30% of markets lack tokens and get filtered out + // This ensures enough raw markets survive the token filter + const OVER_FETCH_FACTOR = 1.7; + const rawTargetLimit = Math.ceil(targetLimit * OVER_FETCH_FACTOR); - if (targetLimit <= PAGE_SIZE) { + if (rawTargetLimit <= PAGE_SIZE) { const response = await fetcher.getActiveMarkets({ - limit: targetLimit, + limit: rawTargetLimit, page: 1, sortBy: sortBy, }); @@ -215,7 +220,7 @@ export async function paginateLimitlessMarkets( let page = 1; const MAX_PAGES = 50; // Safety limit - while (page <= MAX_PAGES && allMarkets.length < targetLimit) { + while (page <= MAX_PAGES && allMarkets.length < rawTargetLimit) { const response = await fetcher.getActiveMarkets({ limit: PAGE_SIZE, page: page, @@ -227,12 +232,13 @@ export async function paginateLimitlessMarkets( allMarkets.push(...markets); - // 🔧 Use totalMarketsCount to check if we're done + // Use totalMarketsCount to check if we're done const totalCount = response.totalMarketsCount; if (totalCount && allMarkets.length >= totalCount) break; page++; } - return allMarkets.slice(0, targetLimit); + // Return raw markets - caller will filter tokens and apply actual limit + return allMarkets; } \ No newline at end of file From d22c5fbdde0fb2d2d8dc90f6174f6c92ed29242c Mon Sep 17 00:00:00 2001 From: AbhilashG12 Date: Sun, 12 Jul 2026 12:30:36 +0530 Subject: [PATCH 3/4] chore: regenerate SDKs and docs after Limitless pagination fix --- core/api-doc-config.generated.json | 2 +- sdks/python/pmxt/client.py | 2 +- sdks/typescript/API_REFERENCE.md | 1 - sdks/typescript/pmxt/client.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index a97b1945..50ecd420 100644 --- a/core/api-doc-config.generated.json +++ b/core/api-doc-config.generated.json @@ -1,5 +1,5 @@ { - "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T10:51:09.860Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-07-12T07:00:04.620Z. Do not edit manually.", "methods": { "has": { "summary": "HTTP verb for the endpoint (e.g. GET, POST). */", diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index 5992acf4..8c609fb7 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -1438,7 +1438,7 @@ def fetch_event(self, params: Optional[dict] = None, **kwargs) -> UnifiedEvent: except ApiException as e: raise self._parse_api_exception(e) from None - def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, limit: Optional[float] = None, params: Optional[FetchOrderBookParams] = None, **kwargs) -> Union[OrderBook, List[OrderBook]]: + def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, limit: Optional[float] = None, params: Optional[dict] = None, **kwargs) -> Union[OrderBook, List[OrderBook]]: try: args = [] if kwargs: diff --git a/sdks/typescript/API_REFERENCE.md b/sdks/typescript/API_REFERENCE.md index 42ce6432..1d9347d3 100644 --- a/sdks/typescript/API_REFERENCE.md +++ b/sdks/typescript/API_REFERENCE.md @@ -1493,7 +1493,6 @@ yes: any; // Convenience accessor for the YES outcome on a binary market. no: any; // Convenience accessor for the NO outcome on a binary market. up: any; // Convenience accessor for the UP outcome on a binary market. down: any; // Convenience accessor for the DOWN outcome on a binary market. -question: string; // Read-only alias for title. Matches the Python SDK's market.question property. } ``` diff --git a/sdks/typescript/pmxt/client.ts b/sdks/typescript/pmxt/client.ts index b0f2e5d1..2eea5890 100644 --- a/sdks/typescript/pmxt/client.ts +++ b/sdks/typescript/pmxt/client.ts @@ -1567,7 +1567,7 @@ export abstract class Exchange { } } - async fetchMatchedMarkets(params?: FetchMatchedMarketClustersParams): Promise { + async fetchMatchedMarkets(params?: any): Promise { await this.initPromise; try { const args: any[] = []; From 7337c2ed26a0eb1da30756390418bd80b8864bb2 Mon Sep 17 00:00:00 2001 From: AbhilashG12 Date: Sun, 12 Jul 2026 12:32:20 +0530 Subject: [PATCH 4/4] chore: regenerate Python client after merge --- sdks/python/pmxt/client.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/sdks/python/pmxt/client.py b/sdks/python/pmxt/client.py index 8c609fb7..b810c031 100644 --- a/sdks/python/pmxt/client.py +++ b/sdks/python/pmxt/client.py @@ -1346,6 +1346,33 @@ def fetch_markets_paginated(self, params: Optional[dict] = None, **kwargs) -> Pa except ApiException as e: raise self._parse_api_exception(e) from None + def fetch_events_paginated(self, params: Optional[dict] = None, **kwargs) -> PaginatedEventsResult: + try: + args = [] + if kwargs: + params = {**(params or {}), **kwargs} + if params is not None: + args.append(_convert_params_to_camel(params)) + body: dict = {"args": args} + creds = self._get_credentials_dict() + if creds: + body["credentials"] = creds + url = f"{self._resolve_sidecar_host()}/api/{self.exchange_name}/fetchEventsPaginated" + headers = {"Content-Type": "application/json", "Accept": "application/json"} + headers.update(self._get_auth_headers()) + response = self._fetch_with_retry( + lambda: self._api_client.call_api(method="POST", url=url, body=body, header_params=headers) + ) + response.read() + data = self._handle_response(json.loads(response.data)) + return PaginatedEventsResult( + data=[_convert_event(e) for e in data.get("data", [])], + total=data.get("total"), + next_cursor=data.get("nextCursor"), + ) + except ApiException as e: + raise self._parse_api_exception(e) from None + def fetch_events(self, params: Optional[dict] = None, **kwargs) -> List[UnifiedEvent]: try: args = [] @@ -1438,6 +1465,26 @@ def fetch_event(self, params: Optional[dict] = None, **kwargs) -> UnifiedEvent: except ApiException as e: raise self._parse_api_exception(e) from None + def fetch_event_metadata(self, event_ticker: str) -> Dict[str, Any]: + try: + args = [] + args.append(event_ticker) + body: dict = {"args": args} + creds = self._get_credentials_dict() + if creds: + body["credentials"] = creds + url = f"{self._resolve_sidecar_host()}/api/{self.exchange_name}/fetchEventMetadata" + headers = {"Content-Type": "application/json", "Accept": "application/json"} + headers.update(self._get_auth_headers()) + response = self._fetch_with_retry( + lambda: self._api_client.call_api(method="POST", url=url, body=body, header_params=headers) + ) + response.read() + data = self._handle_response(json.loads(response.data)) + return data + except ApiException as e: + raise self._parse_api_exception(e) from None + def fetch_order_book(self, outcome_id: Union[str, "MarketOutcome"] = _UNSET, limit: Optional[float] = None, params: Optional[dict] = None, **kwargs) -> Union[OrderBook, List[OrderBook]]: try: args = []