Skip to main content

Market Normalization

Each platform's read-proxy normalizes the venue's raw upstream response into a platform-specific TypeScript interfaceDFlowMarket, PolyMarket, LimitlessMarket, PredictNormalizedMarket, RainMarket, OpinionMarket, OvertimeMarket. These interfaces are defined in each services/<platform>/markets.ts (and several are mirrored on the web side in lib/<platform>Trade.ts). There is no shared base typemagma-sdk's src/types.ts is generic and defines no cross-platform market shape.

Why there is no single unified type

The eight venues are genuinely different products and a forced "union" type would lose the fields each one needs to trade:

  • Identifier differs. Polymarket keys on conditionId; Limitless on slug / address / conditionId; Kalshi/DFlow on ticker (+ eventTicker); Predict on id; Overtime on gameId; Rain/Opinion on id.
  • Outcome model differs. Binary YES/NO token ids (Polymarket clobTokenIds, Predict yesTokenId/noTokenId, DFlow accounts.*.yesMint/noMint) vs sports odds arrays (Overtime odds[], lines, player props, combined positions) vs AMM yes/no prices (Rain) vs macro thresholds (Opinion economicIndicator/threshold/condition).
  • Pricing differs. Some return yesPrice/noPrice 0–1; Polymarket returns stringified outcomePrices; Limitless can return percentages 0–100; Kalshi returns bid/ask strings; Overtime returns American/decimal/implied odds.
  • Trade fields differ. Neg-risk and yield-bearing families (Predict), CLOB venue/adapter (Limitless), settlement mint (DFlow), merkle proof (Overtime).

So each platform keeps its own interface, and the client composes them per-tab. The one thing the backend does standardize across platforms is a derived MAGMA category (SPORTS, POLITICAL, ONCH, ECONOMICS, GEOPOLITICAL, …) so the /markets grid can filter and color uniformly.

The per-platform <Platform>Market interfaces

The defining fields of each (see the source for the full shape):

Interface (file)Primary idOutcome / price fieldsTrade-relevant extras
DFlowMarket (dflow/markets.ts)ticker, eventTickeryesBid/yesAsk/noBid/noAsk (strings)accounts (per settlement mint: yesMint, noMint, settlementMint, marketLedger), status, result
PolyMarket (polymarket/markets.ts)conditionId, slugoutcomes[], outcomePrices[], yesTokenId, noTokenIdenableOrderBook, closed, resolved, liquidity
LimitlessMarket (limitless/markets.ts)id, address, conditionId, slugyesPrice, noPrice (rescaled from 0–100)tradeType, marketType, tradable, collateralDecimals
PredictNormalizedMarket (predict/markets.ts)id, slugyesPrice, noPrice, outcomes[] (indexSet, tokenId, bid/ask)feeRateBps, isYieldBearing, isNegRisk, conditionId, source:'predict', chain:'bnb'
RainMarket (rain/markets.ts)idyesPrice, noPricechain, liquidityPool, status, resolution, isPrivate
OpinionMarket (opinion/markets.ts)idyesPrice, noPriceeconomicIndicator, threshold, condition, aiOracleConfidence, dataSources[]
OvertimeMarket (overtime/markets.ts)gameIdodds[] (american/decimal/impl)leagueId, typeId, line, maturity, proof[], playerProps, childMarkets[]
A field-rich example — PredictNormalizedMarket

Predict's normalized market carries its own source/chain tags, per-outcome CTF indexSet (1 = YES, 2 = NO) and tokenId, derived yesPrice/noPrice from best bid/ask, plus the isNegRisk / isYieldBearing flags the SDK needs to pick the right exchange at order-build time. None of those fields are meaningful for Overtime's sports markets — which is exactly why the types stay separate.

Common verbs

Despite the different shapes, the services expose a consistent verb set (names vary slightly; the route shape is uniform). Not every platform implements every verb:

VerbPurposeRoute shapePlatforms
get listnormalized active markets (paged, category filter, cached)GET /v1/<p>/marketsall 8
get onesingle market detail by the platform's idGET /v1/<p>/markets/:idall (:ticker, :conditionId, :slug, :gameId, :id)
searchkeyword searchGET /v1/<p>/markets/search?q=Polymarket, Limitless, Predict*, Rain, Opinion, Overtime
orderbookCLOB depthGET /v1/<p>/markets/:id/orderbookDFlow, Predict, Opinion
positionsa wallet's holdingsGET /v1/<p>/positions/:walletall 8
quotelive price/payout for a sizePOST /v1/<p>/trade/quote · POST /v1/<p>/quoteRain, Overtime (DFlow via build)

* Predict exposes search via its market list/detail; the dedicated search route is present on Polymarket/Limitless/Rain/Opinion/Overtime. See API Reference for the exact path per platform.

Positions normalization

positions is the verb that most often has no direct upstream endpoint, so the service assembles it:

  • DFlow / Kalshi has no positions API — the service reads the wallet's on-chain Token-2022 accounts (getTokenAccountsByOwner) and joins each mint to market metadata (accounts.yesMint/noMint) to produce side, mark price, value, won, resolved, and redeemable.
  • Polymarket reads the Data API /positions?user=.
  • Limitless / Predict / Rain / Opinion / Overtime read each venue's portfolio/positions feed and return it under a positions array.

How the client composes them

The /markets page (magma-web/src/app/(auth)/markets/page.tsx) keeps per-platform state and fetches each platform with its own typed lib function (fetchRainMarkets, fetchOpinionMarkets, fetchOvertimeMarkets, the Kalshi/Polymarket/Limitless/Predict fetchers, etc.). It then:

  1. renders all venues into one categorized, searchable grid alongside MAGMA-native binary markets, keyed by category color;
  2. routes each card to its own trade modal (KalshiTradeModal, PolymarketTradeModal, LimitlessTradeModal, PredictTradeModal, RainTradeModal, OpinionTradeModal, OvertimeBetModal), each typed to its platform's <Platform>Market; and
  3. resolves the market's chain (VENUE_CHAIN_KEY / useChainController for EVM, solana:mainnet for Kalshi) and switches the wallet before the modal's lib signs.

The composition is client-side and per-platform — the unification is presentational, not a shared data model.

See also