UNPKG

ccxt

Version:

A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go

965 lines (963 loc) 161 kB
// ---------------------------------------------------------------------------- // PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN: // https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code // EDIT THE CORRESPONDENT .ts FILE INSTEAD /// <reference lib="es2015" /> // --------------------------------------------------------------------------- // // Limitless CCXT Exchange adapter (https://limitless.exchange) // // Hierarchy: Group markets (events) → Child markets → YES/NO outcomes // // Each child market becomes one CCXT market with an outcomes list: // market.id: slug // market.symbol: SLUG_SHORT // outcomes[i].symbol: SLUG_SHORT:YES / SLUG_SHORT:NO // // Sizes in the order book are in USDC micro-units (6 decimals) → ÷ 1_000_000. // // --------------------------------------------------------------------------- import { sha256 } from '@noble/hashes/sha2.js'; import { secp256k1 } from '@noble/curves/secp256k1.js'; import { keccak_256 as keccak } from '@noble/hashes/sha3.js'; import Exchange from '../abstract/prediction/limitless.js'; import { ArgumentsRequired, BadRequest, InvalidAddress, InvalidOrder, OrderNotFound, ExchangeError } from '../base/errors.js'; import { Precise } from '../base/Precise.js'; import { ecdsa } from '../base/functions.js'; // --------------------------------------------------------------------------- /** * @class limitless * @augments Exchange */ export default class limitless extends Exchange { describe() { return this.deepExtend(super.describe(), { 'id': 'limitless', 'name': 'Limitless', 'countries': [], 'rateLimit': 200, 'certified': false, 'pro': false, 'has': { 'CORS': undefined, 'spot': false, 'margin': false, 'swap': false, 'future': false, 'option': false, 'approve': true, 'cancelAllOrders': true, 'cancelOrder': true, 'cancelOrders': true, 'createOrder': true, 'fetchAccounts': true, 'fetchBalance': false, 'fetchClosedOrders': true, 'fetchCurrencies': false, 'fetchEvent': true, 'fetchEvents': true, 'fetchMarkets': true, 'fetchMyTrades': true, 'fetchOHLCV': true, 'fetchOpenOrders': true, 'fetchOrder': true, 'fetchOrderBook': true, 'fetchOrders': true, 'fetchOrdersByIds': true, 'fetchPositions': true, 'fetchTicker': true, 'fetchTickers': true, 'fetchTrades': true, 'prediction': true, 'redeem': true, }, 'timeframes': { '1h': '1h', '6h': '6h', '1d': '1d', '1w': '1w', '1M': '1m', }, 'urls': { 'logo': 'https://github.com/user-attachments/assets/bb10c8b4-7b97-49f5-927c-057709dbaf7e', 'api': { 'limitless': 'https://api.limitless.exchange', }, 'www': 'https://limitless.exchange', 'doc': ['https://docs.limitless.exchange'], }, 'api': { 'limitless': { 'public': { 'get': { 'markets/active': 1, 'markets/active/{categoryId}': 1, 'categories': 1, 'markets/{addressOrSlug}': 1, 'markets/categories/count': 1, 'markets/active/slugs': 1, 'markets/search': 1, 'markets/{slug}/orderbook': 1, 'markets/{slug}/historical-price': 1, 'auth/signing-message': 1, 'markets/{addressOrSlug}/oracle-candles': 1, 'markets/{slug}/get-feed-events': 1, 'markets/{slug}/events': 1, 'markets/timeline': 1, 'markets/{slug}/timeline': 1, 'navigation': 1, 'market-pages/by-path': 1, 'market-pages/{id}/markets': 1, 'property-keys': 1, 'property-keys/{id}': 1, 'property-keys/{id}/options': 1, 'portfolio/{account}/traded-volume': 1, 'portfolio/{account}/positions': 1, 'portfolio/{account}/pnl-chart': 1, }, }, 'private': { 'get': { 'auth/api-keys': 1, 'profiles/partner-accounts': 1, 'markets/{slug}/user-orders': 1, 'portfolio/positions': 1, 'portfolio/trades': 1, 'markets/{slug}/locked-balance': 1, 'profiles/me': 1, 'profiles/{account}': 1, 'portfolio/pnl-chart': 1, 'portfolio/history': 1, 'portfolio/points': 1, 'portfolio/trading/allowance': 1, 'auth/api-tokens/capabilities': 1, 'auth/api-tokens': 1, 'profiles/partner-accounts/{profileId}/allowances': 1, }, 'post': { 'auth/logout': 1, 'auth/api-keys': 1, 'auth/login': 1, 'orders': 1, 'orders/cancel': 1, 'orders/cancel-batch': 1, 'orders/batch-cancel': 1, 'orders/status/batch': 1, 'portfolio/redeem': 1, 'portfolio/withdraw': 1, 'portfolio/withdrawal-addresses': 1, 'auth/api-tokens/derive': 1, 'profiles/partner-accounts': 1, 'profiles/partner-accounts/{profileId}/allowances/retry': 1, }, 'delete': { 'auth/api-keys': 1, 'orders/{order_id}': 1, 'orders/all/{slug}': 1, 'auth/api-tokens/{tokenId}': 1, 'portfolio/withdrawal-addresses/{address}': 1, }, }, }, }, 'requiredCredentials': { 'apiKey': true, // Limitless API key 'secret': true, 'privateKey': true, // embedded/trading wallet key — createOrder signs with it (env-var loading needs this true) }, 'fees': { 'trading': { 'tierBased': false, 'percentage': true, 'maker': 0.02, 'taker': 0.02, }, }, 'options': { 'defaultFetchMarketsPages': 5, 'marketsPageSize': 25, 'usdcDecimals': 6, // Limitless sizes are 6-decimal USDC 'warnOnCancelAllOrdersWithOutcome': true, // cancelAllOrders with an outcome will cancel all orders for the entire slug (both YES and NO outcomes), so we warn by default to prevent mistakes. Set this option to false to suppress the warning. 'zeroAddress': '0x0000000000000000000000000000000000000000', 'chainId': 8453, // Base 'rpcUrl': 'https://mainnet.base.org', // Base RPC used by approve() for the on-chain allowance tx 'collateralAddress': '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC on Base (default approve token) 'exchangeAddress': '0x05c748E2f4DcDe0ec9Fa8DDc40DE6b867f923fa5', // Limitless CTF exchange (default approve spender) 'createMarketBuyOrderRequiresPrice': true, }, 'exceptions': { 'exact': {}, 'broad': { 'Order not found': OrderNotFound, 'Orders not found': OrderNotFound, 'already canceled': OrderNotFound, 'must be a UUIDv4': BadRequest, 'Invalid signature': InvalidOrder, }, }, }); } /** * @method * @name limitless#fetchMarkets * @description fetches all active limitless markets paginated and returns one CCXT market per child market, each containing a list of outcome objects (YES/NO) * @see https://docs.limitless.exchange/api-reference/markets/get-active-markets * @param {object} [params] extra parameters specific to the exchange API endpoint * @param {string} [params.query] a single search query string to filter markets by * @param {string[]} [params.queries] multiple search query strings (alternative to query) * @param {int} [params.limit] max number of markets to collect (defaults to options.fetchMarketsLimit, 1000); caps the pages fetched * @returns {object[]} an array of objects representing market data */ async fetchMarkets(params = {}) { const queries = this.parseSearchQueries(params); const rest = this.omit(params, ['query', 'queries', 'limit']); // scope the listing: without a search query loadMarkets would otherwise page through // every active limitless market. Cap the total number of markets collected. const maxMarkets = this.safeInteger(params, 'limit', this.safeInteger(this.options, 'fetchMarketsLimit', 1000)); let allRaw = []; const queriesLength = queries.length; if (queries && queriesLength > 0) { const requestedLimit = this.safeInteger(params, 'limit', 50); // the search endpoint rejects limit > 50 - cap the per-query request and let // maxMarkets bound the overall collection const limit = Math.min(requestedLimit, 50); const searchRest = this.omit(rest, ['limit']); const seen = {}; for (let i = 0; i < queries.length; i++) { const q = queries[i]; const response = await this.limitlessPublicGetMarketsSearch(this.extend({ 'query': q, 'limit': limit }, searchRest)); const found = this.safeList(response, 'markets', []); for (let j = 0; j < found.length; j++) { const raw = found[j]; const slug = this.safeString(raw, 'slug'); if (slug && !(slug in seen)) { seen[slug] = true; allRaw.push(raw); } } } } else { let page = 1; const pageSize = this.safeInteger(this.options, 'marketsPageSize', 25); const request = { 'page': page, 'limit': pageSize, }; const firstPageResponse = await this.limitlessPublicGetMarketsActive(this.extend(request, rest)); const totalMarketsCount = this.safeInteger(firstPageResponse, 'totalMarketsCount'); const firstData = this.safeList(firstPageResponse, 'data', []); allRaw = this.arrayConcat(allRaw, firstData); const promises = []; const cappedPages = Math.ceil(maxMarkets / pageSize); const knownTotal = (totalMarketsCount !== undefined) ? totalMarketsCount : 0; const allPages = Math.ceil(knownTotal / pageSize); const totalPages = Math.min(allPages, cappedPages); for (let i = 2; i <= totalPages; i++) { page = i; request['page'] = page; promises.push(this.limitlessPublicGetMarketsActive(this.extend(request, rest))); } const responses = await Promise.all(promises); const length = responses.length; for (let j = 0; j < length; j++) { const response = this.safeDict(responses, j); const data = this.safeList(response, 'data', []); allRaw = this.arrayConcat(allRaw, data); } const lastPageResponse = this.safeDict(responses, length - 1); const lastPageData = this.safeList(lastPageResponse, 'data', []); const lastPageLength = lastPageData.length; const allRawLength = allRaw.length; if (lastPageLength >= pageSize && allRawLength < maxMarkets) { while (true) { page = this.sum(page, 1); request['page'] = page; const response = await this.limitlessPublicGetMarketsActive(this.extend(request, rest)); const rawPageMarkets = this.safeList(response, 'data', response); const page_markets = (rawPageMarkets !== undefined) ? rawPageMarkets : []; const pageMarketsLength = page_markets.length; if (!page_markets || pageMarketsLength === 0) { break; } for (let i = 0; i < page_markets.length; i++) { const raw = page_markets[i]; allRaw.push(raw); } const allRawCount = allRaw.length; if (pageMarketsLength < pageSize || allRawCount >= maxMarkets) { break; } } } } const markets = []; const eventGroups = {}; // group rows carry their tradeable children in a nested `markets` list — expand them // into regular rows before parsing (a group row itself has no tokens) const expandedRaw = this.expandGroupRows(allRaw); for (let i = 0; i < expandedRaw.length; i++) { const raw = expandedRaw[i]; const groupId = this.safeStringN(raw, ['groupSlug', 'groupId'], this.safeString(raw, 'slug')); const eventKey = groupId ? this.shortenSlug(groupId) : undefined; const m = this.parseMarket(raw); markets.push(m); if (eventKey) { if (!(eventKey in eventGroups)) { eventGroups[eventKey] = { 'groupId': groupId, 'title': this.safeString2(raw, 'groupTitle', 'title', groupId), 'raw': raw, 'markets': [] }; } const eventGroup = eventGroups[eventKey]; // push through a local and write the slice back — the go transpiler's // AppendToArray reassigns only a local copy of a map-stored array, so a // direct push on eventGroup['markets'] loses the element in go const groupMarkets = eventGroup['markets']; groupMarkets.push(m); eventGroup['markets'] = groupMarkets; } } const eventsDict = {}; const eventKeys = Object.keys(eventGroups); for (let i = 0; i < eventKeys.length; i++) { const eventKey = eventKeys[i]; const g = eventGroups[eventKey]; eventsDict[eventKey] = this.parseEvent(g); } this.events = eventsDict; const marketsLength = markets.length; if (marketsLength > maxMarkets) { return this.arraySlice(markets, 0, maxMarkets); } return markets; } parseMarket(raw) { // // { // "id":"36814", // "automationType":"manual", // "conditionId":"0x11287d02d8067ff3d3d8bd21b212ebcfdc20b638f7f6440e4115f649e6b57015", // "negRiskRequestId":null, // "description":"<p>This market will resolve to “Yes” if Donald Trump resigns or is removed as President or otherwise ceases to be the President of the United States for any period of time by December 31, 2026, 11:59 PM ET. Otherwise, this market will resolve to “No”.</p><p>An announcement of Donald Trump's resignation/removal before this market's end date will immediately resolve this market to \\""Yes\\"", regardless of when the announced resignation/removal goes into effect.</p><p>Only permanent removal from office will qualify. Temporary removal (e.g. temporary invocation of the 25th Amendment under Section 3 or a Section 4 invocation not sustained by both Houses of Congress) or impeachment without removal will not count.</p><p>A sustained invocation of the Twenty-Fifth Amendment, Section 4 (i.e., if both Houses of Congress, by two-thirds vote, uphold the Vice President and Cabinet’s determination of presidential inability) will qualify for a \\""Yes\\"" resolution.</p><p>The resolution source for this market will be a consensus of credible reporting.</p>", // "collateralToken":{ // "address":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // "decimals":"6", // "symbol":"USDC" // }, // "title":"💎 Trump out as President before 2027?", // "proxyTitle":null, // "expirationDate":"Jan 1, 2027", // "expirationTimestamp":"1798779540000", // "createdAt":"2026-01-20T18:17:48.298Z", // "updatedAt":"2026-02-24T17:00:11.833Z", // "categories":[ // "Politics" // ], // "status":"FUNDED", // "expired":false, // "hidden":false, // "creator":{ // "name":"Limitless", // "imageURI":"https://limitless.exchange/assets/images/logo.svg", // "link":"https://x.com/trylimitless" // }, // "tags":[ // "Limitless" // ], // "volume":"290091252", // "volumeFormatted":"290.091252", // "tokens":{ // "yes":"56154308742753982686710750162015444986563701968079760676518531584453506363044", // "no":"32572248812801208874557774576516861470423415416073401354576860825663488568217" // }, // "prices":[ // 0.164, // 0.836 // ], // "isOther":false, // "isRewardable":true, // "slug":"trump-out-as-president-before-2027-1768933068297", // "tradeType":"clob", // "venue":{ // "exchange":"0x05c748E2f4DcDe0ec9Fa8DDc40DE6b867f923fa5", // "adapter":null // }, // "marketType":"single", // "priorityIndex":"0", // "winningOutcomeIndex":null, // "metadata":{ // "fee":true, // "isBannered":false, // "isPolyArbitrage":true // }, // "trends":{ // "hourly":{ // "value":"3", // "rank":"395" // } // }, // "settings":{ // "minSize":"100000000", // "maxSpread":"0.035", // "dailyReward":"5", // "rewardsEpoch":"0.003472222222222222", // "c":"3", // "rebateRate":"0" // }, // "imageUrl":"https://cdn.limitless.exchange/markets-logo/36814/9daba01d-6bcd-4a2c-9187-f4264b7191da.png", // "logo":"https://cdn.limitless.exchange/markets-logo/36814/9daba01d-6bcd-4a2c-9187-f4264b7191da.png" // } // const slug = this.safeString(raw, 'slug'); const address = this.safeString(raw, 'address', slug); // groupSlug is stamped by expandGroupRows on children of a group row — prefer it over // the child's own numeric groupId so symbols/handles derive from the readable slug const groupId = this.safeStringN(raw, ['groupSlug', 'groupId'], slug); // CTF condition id — needed to redeem a resolved winning position const conditionId = this.safeString(raw, 'conditionId'); const tokens = this.safeValue(raw, 'tokens', {}); // the listing exposes `expired` + `status` (FUNDED/RESOLVED/…), not an `active` flag; a // market is tradeable only while it is FUNDED and not yet expired const isExpired = this.safeBool(raw, 'expired', false); const marketStatus = this.safeString(raw, 'status'); const active = !isExpired && (marketStatus === 'FUNDED'); // expiry is a ms timestamp string (`expirationTimestamp`); `deadline`/`expiresAt` do not exist const expiryTimestamp = this.safeInteger(raw, 'expirationTimestamp'); // limitless reports lifetime volume (human-readable in `volumeFormatted`), not a 24h figure const volume24h = this.safeNumber(raw, 'volumeFormatted'); // resolution: winningOutcomeIndex is null until the market resolves, then the winning outcome index const winningOutcomeIndex = this.safeInteger(raw, 'winningOutcomeIndex'); const marketResolved = (winningOutcomeIndex !== undefined); let resolvedOutcome = undefined; const marketSymbol = this.slugToMarketSymbol(groupId, slug); // amount precision comes from the collateral token decimals (USDC, 6); limitless does not // expose a price tick, so 0.001 is the platform convention const collateralToken = this.safeDict(raw, 'collateralToken', {}); const collateralDecimals = this.safeInteger(collateralToken, 'decimals', this.safeInteger(this.options, 'usdcDecimals', 6)); const precision = { 'amount': this.parseNumber(this.parsePrecision(this.numberToString(collateralDecimals))), 'price': 0.001, }; const outcomes = []; const tokenEntries = Object.keys(tokens); for (let i = 0; i < tokenEntries.length; i++) { const outcomeLabel = tokenEntries[i]; const tokenData = tokens[outcomeLabel]; const tokenId = tokenData; const outcomeHandle = this.slugToOutcomeSymbol(groupId, slug, outcomeLabel); // winningOutcomeIndex indexes the API's canonical outcome order (yes=0, no=1 for // limitless's binary yes/no markets). Object.keys iteration order is NOT stable across // languages (Go randomizes map iteration), so map the leg to its canonical index by // label rather than by loop position — otherwise Go/Java flag the wrong winner const labelLower = outcomeLabel.toLowerCase(); let legIndex = i; if (labelLower === 'yes') { legIndex = 0; } else if (labelLower === 'no') { legIndex = 1; } let winnerRaw = undefined; let settleFractionRaw = undefined; if (marketResolved) { winnerRaw = (legIndex === winningOutcomeIndex); settleFractionRaw = winnerRaw ? 1 : 0; if (winnerRaw) { resolvedOutcome = outcomeHandle; } } // effectively-final copies for the object literal below (Java cannot capture a // reassigned local into the anonymous inner class it emits for a map literal) const winner = winnerRaw; const settleFraction = settleFractionRaw; outcomes.push({ 'outcome': outcomeHandle, 'outcomeId': tokenId, 'market': marketSymbol, 'label': outcomeLabel, 'active': active, 'winner': winner, 'settleFraction': settleFraction, 'precision': precision, 'info': { 'slug': slug, 'address': address, 'conditionId': conditionId, 'outcomeLabel': outcomeLabel, 'tokenId': tokenId, 'volume24h': volume24h, }, }); } const outcomesLength = outcomes.length; // effectively-final copy for the market object literal below (reassigned in the loop) const marketResolvedOutcome = resolvedOutcome; return { 'id': slug, 'market': marketSymbol, 'marketType': (outcomesLength > 2) ? 'categorical' : 'binary', 'executionModel': 'clob', 'collateral': 'USDC', 'base': slug, 'quote': 'USDC', 'settle': undefined, 'baseId': slug, 'quoteId': 'USDC', 'settleId': undefined, 'type': 'prediction', 'spot': false, 'margin': false, 'swap': false, 'future': false, 'option': false, 'prediction': true, 'active': active, 'resolved': marketResolved, 'resolvedOutcome': marketResolvedOutcome, 'contract': false, 'linear': undefined, 'inverse': undefined, 'contractSize': undefined, 'expiry': expiryTimestamp, 'expiryDatetime': this.iso8601(expiryTimestamp), 'strike': undefined, 'optionType': undefined, 'taker': 0.02, 'maker': 0.02, 'percentage': true, 'tierBased': false, 'feeSide': 'get', 'precision': precision, 'limits': { 'leverage': { 'min': 1, 'max': 1 }, 'amount': { 'min': 0, 'max': undefined }, 'price': { 'min': 0.001, 'max': 0.999 }, 'cost': { 'min': undefined, 'max': undefined }, }, 'outcomes': outcomes, 'info': this.extend(raw, { 'slug': slug, 'address': address, 'volume24h': volume24h, }), 'created': undefined, }; } /** * @method * @name limitless#fetchEvent * @description fetches a single prediction-market event by its market slug or address * @see https://docs.limitless.exchange/api-reference/markets/get-market * @param {string} id the market slug or address * @param {object} [params] extra parameters specific to the exchange API endpoint * @returns {object} a [prediction event structure](https://docs.ccxt.com/#/?id=prediction-event-structure) */ async fetchEvent(id, params = {}) { const request = { 'addressOrSlug': id }; const response = await this.limitlessPublicGetMarketsAddressOrSlug(this.extend(request, params)); // a group response carries its tradeable children in `markets` (each a full market row // with tokens) — expandGroupRows unwraps them; a single market has no nested markets // and wraps as its own one-market event, which parseEvent's loop then parses const rows = this.expandGroupRows([response]); const wrapped = this.extend(response, { 'markets': rows }); const event = this.parseEvent(wrapped); this.indexEventOutcomes(event); return event; } /** * @ignore * @method * @name limitless#expandGroupRows * @description flattens listing rows — a 'group' row carries no tradeable tokens itself and * its children (each a full market row with tokens) appear nowhere else in the listing, so * each group row is replaced by its nested markets, tagged with the group's slug and title * (the child's own groupId is an opaque numeric venue id) so they regroup under one readable event * @param {object[]} rawRows raw listing rows, single-market and group rows mixed * @returns {object[]} raw single-market rows only */ expandGroupRows(rawRows) { const result = []; for (let i = 0; i < rawRows.length; i++) { const raw = rawRows[i]; const rowType = this.safeString(raw, 'marketType'); const nestedMarkets = this.safeList(raw, 'markets'); if ((rowType === 'group') && (nestedMarkets !== undefined)) { const groupSlug = this.safeString(raw, 'slug'); const groupTitle = this.safeString(raw, 'title', groupSlug); const nestedMarketsLength = nestedMarkets.length; for (let j = 0; j < nestedMarketsLength; j++) { // extend copies — the raw child stays untouched const tagged = this.extend(nestedMarkets[j], { 'groupSlug': groupSlug, 'groupTitle': groupTitle }); result.push(tagged); } } else { result.push(raw); } } return result; } parseEvent(event) { // { // "groupId":"trump-out-as-president-before-2027-1768933068297", // "title":"💎 Trump out as President before 2027?", // "raw":{ // "id":"36814", // "automationType":"manual", // "conditionId":"0x11287d02d8067ff3d3d8bd21b212ebcfdc20b638f7f6440e4115f649e6b57015", // "negRiskRequestId":null, // "description":"<p>This market will resolve to “Yes” if Donald Trump resigns or is removed as President or otherwise ceases to be the President of the United States for any period of time by December 31, 2026, 11:59 PM ET. Otherwise, this market will resolve to “No”.</p><p>An announcement of Donald Trump's resignation/removal before this market's end date will immediately resolve this market to \\""Yes\\"", regardless of when the announced resignation/removal goes into effect.</p><p>Only permanent removal from office will qualify. Temporary removal (e.g. temporary invocation of the 25th Amendment under Section 3 or a Section 4 invocation not sustained by both Houses of Congress) or impeachment without removal will not count.</p><p>A sustained invocation of the Twenty-Fifth Amendment, Section 4 (i.e., if both Houses of Congress, by two-thirds vote, uphold the Vice President and Cabinet’s determination of presidential inability) will qualify for a \\""Yes\\"" resolution.</p><p>The resolution source for this market will be a consensus of credible reporting.</p>", // "collateralToken":{ // "address":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // "decimals":"6", // "symbol":"USDC" // }, // "title":"💎 Trump out as President before 2027?", // "proxyTitle":null, // "expirationDate":"Jan 1, 2027", // "expirationTimestamp":"1798779540000", // "createdAt":"2026-01-20T18:17:48.298Z", // "updatedAt":"2026-02-24T17:00:11.833Z", // "categories":[ // "Politics" // ], // "status":"FUNDED", // "expired":false, // "hidden":false, // "creator":{ // "name":"Limitless", // "imageURI":"https://limitless.exchange/assets/images/logo.svg", // "link":"https://x.com/trylimitless" // }, // "tags":[ // "Limitless" // ], // "volume":"290091252", // "volumeFormatted":"290.091252", // "tokens":{ // "yes":"56154308742753982686710750162015444986563701968079760676518531584453506363044", // "no":"32572248812801208874557774576516861470423415416073401354576860825663488568217" // }, // "prices":[ // 0.164, // 0.836 // ], // "isOther":false, // "isRewardable":true, // "slug":"trump-out-as-president-before-2027-1768933068297", // "tradeType":"clob", // "venue":{ // "exchange":"0x05c748E2f4DcDe0ec9Fa8DDc40DE6b867f923fa5", // "adapter":null // }, // "marketType":"single", // "priorityIndex":"0", // "winningOutcomeIndex":null, // "metadata":{ // "fee":true, // "isBannered":false, // "isPolyArbitrage":true // }, // "trends":{ // "hourly":{ // "value":"3", // "rank":"395" // } // }, // "settings":{ // "minSize":"100000000", // "maxSpread":"0.035", // "dailyReward":"5", // "rewardsEpoch":"0.003472222222222222", // "c":"3", // "rebateRate":"0" // }, // "imageUrl":"https://cdn.limitless.exchange/markets-logo/36814/9daba01d-6bcd-4a2c-9187-f4264b7191da.png", // "logo":"https://cdn.limitless.exchange/markets-logo/36814/9daba01d-6bcd-4a2c-9187-f4264b7191da.png" // }, // "markets":[ // { // "id":"trump-out-as-president-before-2027-1768933068297", // "symbol":"TRUMP_OUT_PRESIDENT_2027_1768933068297", // "base":"trump-out-as-president-before-2027-1768933068297", // "quote":"USDC", // "baseId":"trump-out-as-president-before-2027-1768933068297", // "quoteId":"USDC", // "type":"prediction", // "spot":false, // "margin":false, // "swap":false, // "future":false, // "option":false, // "prediction":true, // "active":true, // "contract":false, // "taker":0.02, // "maker":0.02, // "percentage":true, // "tierBased":false, // "feeSide":"get", // "precision":{ // "amount":0.000001, // "price":0.001 // }, // "limits":{ // "leverage":{ // "min":1, // "max":1 // }, // "amount":{ // "min":0 // }, // "price":{ // "min":0.001, // "max":0.999 // }, // "cost":{ // } // }, // "outcomes":[ // { // "id":"trump-out-as-president-before-2027-1768933068297/yes", // "symbol":"TRUMP_OUT_PRESIDENT_2027_1768933068297:YES", // "marketSymbol":"TRUMP_OUT_PRESIDENT_2027_1768933068297", // "label":"yes", // "active":true, // "info":{ // "slug":"trump-out-as-president-before-2027-1768933068297", // "address":"trump-out-as-president-before-2027-1768933068297", // "outcomeLabel":"yes", // "tokenId":"trump-out-as-president-before-2027-1768933068297/yes" // } // }, // { // "id":"trump-out-as-president-before-2027-1768933068297/no", // "symbol":"TRUMP_OUT_PRESIDENT_2027_1768933068297:NO", // "marketSymbol":"TRUMP_OUT_PRESIDENT_2027_1768933068297", // "label":"no", // "active":true, // "info":{ // "slug":"trump-out-as-president-before-2027-1768933068297", // "address":"trump-out-as-president-before-2027-1768933068297", // "outcomeLabel":"no", // "tokenId":"trump-out-as-president-before-2027-1768933068297/no" // } // } // ], // "info":{ // "id":"36814", // "automationType":"manual", // "conditionId":"0x11287d02d8067ff3d3d8bd21b212ebcfdc20b638f7f6440e4115f649e6b57015", // "negRiskRequestId":null, // "description":"<p>This market will resolve to “Yes” if Donald Trump resigns or is removed as President or otherwise ceases to be the President of the United States for any period of time by December 31, 2026, 11:59 PM ET. Otherwise, this market will resolve to “No”.</p><p>An announcement of Donald Trump's resignation/removal before this market's end date will immediately resolve this market to \\""Yes\\"", regardless of when the announced resignation/removal goes into effect.</p><p>Only permanent removal from office will qualify. Temporary removal (e.g. temporary invocation of the 25th Amendment under Section 3 or a Section 4 invocation not sustained by both Houses of Congress) or impeachment without removal will not count.</p><p>A sustained invocation of the Twenty-Fifth Amendment, Section 4 (i.e., if both Houses of Congress, by two-thirds vote, uphold the Vice President and Cabinet’s determination of presidential inability) will qualify for a \\""Yes\\"" resolution.</p><p>The resolution source for this market will be a consensus of credible reporting.</p>", // "collateralToken":{ // "address":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // "decimals":"6", // "symbol":"USDC" // }, // "title":"💎 Trump out as President before 2027?", // "proxyTitle":null, // "expirationDate":"Jan 1, 2027", // "expirationTimestamp":"1798779540000", // "createdAt":"2026-01-20T18:17:48.298Z", // "updatedAt":"2026-02-24T17:00:11.833Z", // "categories":[ // "Politics" // ], // "status":"FUNDED", // "expired":false, // "hidden":false, // "creator":{ // "name":"Limitless", // "imageURI":"https://limitless.exchange/assets/images/logo.svg", // "link":"https://x.com/trylimitless" // }, // "tags":[ // "Limitless" // ], // "volume":"290091252", // "volumeFormatted":"290.091252", // "tokens":{ // "yes":"56154308742753982686710750162015444986563701968079760676518531584453506363044", // "no":"32572248812801208874557774576516861470423415416073401354576860825663488568217" // }, // "prices":[ // 0.164, // 0.836 // ], // "isOther":false, // "isRewardable":true, // "slug":"trump-out-as-president-before-2027-1768933068297", // "tradeType":"clob", // "venue":{ // "exchange":"0x05c748E2f4DcDe0ec9Fa8DDc40DE6b867f923fa5", // "adapter":null // }, // "marketType":"single", // "priorityIndex":"0", // "winningOutcomeIndex":null, // "metadata":{ // "fee":true, // "isBannered":false, // "isPolyArbitrage":true // }, // "trends":{ // "hourly":{ // "value":"3", // "rank":"395" // } // }, // "settings":{ // "minSize":"100000000", // "maxSpread":"0.035", // "dailyReward":"5", // "rewardsEpoch":"0.003472222222222222", // "c":"3", // "rebateRate":"0" // }, // "imageUrl":"https://cdn.limitless.exchange/markets-logo/36814/9daba01d-6bcd-4a2c-9187-f4264b7191da.png", // "logo":"https://cdn.limitless.exchange/markets-logo/36814/9daba01d-6bcd-4a2c-9187-f4264b7191da.png", // "address":"trump-out-as-president-before-2027-1768933068297" // } // } // ] // } const groupId = this.safeString(event, 'address', this.safeString(event, 'groupId', this.safeString(event, 'slug'))); const endDate = this.safeString(event, 'deadline', this.safeString(event, 'expiresAt')); const title = this.safeString(event, 'title', groupId); const markets = []; const rawMarkets = this.safeList(event, 'markets', []); // aggregate 24h volume across the markets so sort by volume works let totalVolume = 0; for (let i = 0; i < rawMarkets.length; i++) { const rawMarket = rawMarkets[i]; // an already-parsed ccxt market row carries the unified 'market' handle + outcomes // with 'symbol' kept as a legacy fallback — don't run it through parseMarket again const marketSymbol = this.safeString2(rawMarket, 'market', 'symbol'); const marketOutcomes = this.safeList(rawMarket, 'outcomes'); if (marketSymbol !== undefined && marketOutcomes !== undefined) { markets.push(rawMarket); } else { markets.push(this.parseMarket(rawMarket)); } const marketInfo = this.safeDict(rawMarket, 'info', rawMarket); // use volumeFormatted (human units) — the raw `volume` is 1e-6 fixed-point, which would // make the event volume 1,000,000x too big and useless for cross-venue ranking totalVolume = this.sum(totalVolume, this.safeNumber(marketInfo, 'volumeFormatted', 0)); } return this.extend({ 'id': groupId, 'slug': groupId, 'event': groupId ? this.shortenSlug(groupId) : undefined, 'title': title, 'description': this.safeString(event, 'description'), 'markets': markets, 'volume': totalVolume, 'liquidity': this.safeNumber(event, 'liquidity'), 'url': this.safeString(event, 'url'), 'image': this.safeString(event, 'imageUrl', this.safeString(event, 'image')), 'active': this.safeBool(event, 'active', true), 'resolved': this.safeBool(event, 'resolved', false), 'category': this.safeString(event, 'category'), 'tags': this.safeList(event, 'tags'), 'created': this.parse8601(this.safeString(event, 'createdAt')), 'createdDatetime': this.safeString(event, 'createdAt'), 'end': endDate ? this.parse8601(endDate) : undefined, 'endDatetime': endDate, 'lastUpdatedAt': this.parse8601(this.safeString(event, 'updatedAt')), 'resolutionSource': this.safeString(event, 'resolutionSource'), 'info': event, }); } /** * @method * @name limitless#fetchTicker * @description fetches the current price and best bid/ask for a single outcome token, combining the market detail and order book endpoints * @see https://docs.limitless.exchange/api-reference/markets/get-market * @see https://docs.limitless.exchange/api-reference/trading/orderbook * @param {string} outcome unified outcome like TRUMP_OUT_PRESIDENT_2027:YES or an outcome token id * @param {object} [params] extra parameters specific to the exchange API endpoint * @returns {object} a [prediction ticker structure](https://docs.ccxt.com/#/?id=prediction-ticker-structure) */ async fetchTicker(outcome, params = {}) { await this.loadOutcome(outcome); const outcomeObj = this.outcome(outcome); const slug = this.safeString(outcomeObj['info'], 'slug'); const request = { 'addressOrSlug': slug, }; const promises = [ this.limitlessPublicGetMarketsAddressOrSlug(this.extend(request, params)), this.limitlessPublicGetMarketsSlugOrderbook({ 'slug': slug }), ]; const responses = await Promise.all(promises); const response = responses[0]; // // { // "id": "36814", // "automationType": "manual", // "conditionId": "0x11287d02d8067ff3d3d8bd21b212ebcfdc20b638f7f6440e4115f649e6b57015", // "negRiskRequestId": null, // "description": "<p>This market will resolve to Yes if Donald Trump resigns or is removed as President or otherwise ceases to be the President of the United States for any period of time by December 31, 2026, 11:59 PM ET. Otherwise, this market will resolve to No.</p><p>An announcement of Donald Trump's resignation/removal before this market's end date will immediately resolve this market to Yes, regardless of when the announced resignation/removal goes into effect.</p><p>Only permanent removal from office will qualify. Temporary removal (e.g. temporary invocation of the 25th Amendment under Section 3 or a Section 4 invocation not sustained by both Houses of Congress) or impeachment without removal will not count.</p><p>A sustained invocation of the Twenty-Fifth Amendment, Section 4 (i.e., if both Houses of Congress, by two-thirds vote, uphold the Vice President and Cabinets determination of presidential inability) will qualify for a Yes resolution.</p><p>The resolution source for this market will be a consensus of credible reporting.</p>", // "collateralToken": { // "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // "decimals": "6", // "symbol": "USDC" // }, // "title": "Trump out as President before 2027?", // "proxyTitle": null, // "expirationDate": "Jan 1, 2027", // "expirationTimestamp": "1798779540000", // "createdAt": "2026-01-20T18:17:48.298Z", // "updatedAt": "2026-04-09T10:47:02.254Z", // "categories": [ "Politics" ], // "status": "FUNDED", // "expired": false, // "hidden": false, // "creator": { // "name": "Limitless", // "imageURI": "https://limitless.exchange/assets/images/logo.svg", // "link": "https://x.com/trylimitless" // }, // "tags": [ "Limitless" ], // "volume": "1032001807", // "volumeFormatted": "1032.001807", // "tokens": { // "yes": "56154308742753982686710750162015444986563701968079760676518531584453506363044", // "no": "32572248812801208874557774576516861470423415416073401354576860825663488568217" // }, // "prices": [ 0.155, 0.845 ], // "tradePrices": { // "buy": { "market": [Array], "limit": [Array] }, // "sell": { "market": [Array], "limit": [Array] } // }, // "isOther": false, // "isRewardable": true, // "slug": "trump-out-as-president-before-2027-1768933068297", // "tradeType": "clob", // "venue": { // "exchange": "0x05c748E2f4DcDe0ec9Fa8DDc40DE6b867f923fa5", // "adapter": null // }, // "marketType": "single", // "priorityIndex": "0", // "winningOutcomeIndex": null, // "metadata": { "fee": true, "isBannered": false, "isPolyArbitrage": true }, // "settings": { // "minSize": "100000000", // "maxSpread": "0.035", // "dailyReward": "5", // "rewardsEpoch": "0.003472222222222222", // "c": "3", // "rebateRate": "0" // }, // "imageUrl": "https://cdn.limitless.exchange/markets-logo/36814/9daba01d-6bcd-4a2c-9187-f4264b7191da.png", // "logo": "https://cdn.limitless.exchange/markets-logo/36814/9daba01d-6bcd-4a2c-9187-f4264b7191da.png" // } // const tickerInput = { 'market': response, 'book': responses[1] }; return this.parsePredictionTicker(tickerInput, outcomeObj); } /** * @ignore * @method * @name limitless#parsePredi