Pull the current price for the same event from two prediction market APIs and diff them. Check "Fed cuts rates in September" on Kalshi and Polymarket at the same moment: Kalshi shows YES at 61¢, Polymarket shows YES at $0.58. Three cents isn't rounding error, and it isn't one venue lagging the other. They won't match, and it's not a bug in either API. It's because "price" doesn't mean the same thing on the two platforms you just queried.

That sounds like a minor unit-conversion detail. It isn't. If you're building anything that touches more than one prediction market, a dashboard, a bot, a research tool, a Discord alert, the normalization layer is the actual engineering problem. Get it wrong and a comparison dashboard shows a spread that isn't real, a bot trades on a gap that evaporates once you account for fees, or a research pull quietly treats two different questions as one. Everything else in the pipeline is a REST call.

Here's the short version: five major platforms (Polymarket, Kalshi, Limitless, Predict.Fun, Opinion) all price contracts on a scale that looks like a probability, but each gets there through a different market structure, a different settlement venue, and in one case a different fee curve baked into the number you'd naively read as "the price." Treat them as interchangeable floats and your comparison tool will be confidently wrong.

The five formats, side by side:

Four of these are genuinely "price equals implied probability." The fifth has a fee schedule quietly riding along inside the number.

Problem 1: identical-looking fields, different formulas

The Polymarket case is the one that catches people. Most of the time, the price you pull from the API is a simple midpoint: average the best bid and the best ask, done. But when the spread on a thin market widens past $0.10, the API switches to reporting the last traded price instead. Same field name, same 0-1 range, different formula, and the switch happens silently based on current liquidity. A comparison tool that always treats "price" as "current midpoint" will be quietly wrong on exactly the illiquid markets where a cross-platform comparison would be most interesting.

Kalshi has its own version of this. Because YES and NO are independently quoted, the pair doesn't have to sum to a clean $1.00. You can see a market where YES asks 64¢ and NO asks 39¢, a combined 103¢, and that gap is real spread, not a rounding artifact. If your code assumes no_price = 1 - yes_price, you'll get a number that looks plausible and is wrong.

Problem 2: the fee is sometimes part of the price

This is the sharpest edge, and it's specific to Opinion. Its taker fee isn't a flat percentage tacked on at checkout. It follows a curve shaped by p * (1 - p), where p is the current probability, so the fee is largest when the market is closest to 50/50 and shrinks toward the extremes. A rough illustration, not a real observed quote:

```python

def taker_fee_illustrative(p: float, max_fee: float = 0.02) -> float:

"""

Illustrative shape only, not Opinion's actual published fee schedule.

Fee peaks near p = 0.5 and shrinks toward the extremes.

"""

return max_fee * 4 * p * (1 - p)

for p in (0.05, 0.25, 0.50, 0.75, 0.95): print(p, round(taker_fee_illustrative(p), 4))```

The point isn't the exact coefficients, it's that the number you read off the screen as "Opinion's price" is not a clean probability estimate the way Kalshi's or Polymarket's is. It's a probability estimate plus a variable cost that depends on how uncertain the market currently is. Normalize that against a Kalshi price without accounting for the fee curve, and you're comparing a gross number to a net one.

Problem 3: "same event" is not a stable join key

Say you're matching markets across platforms by event name, "Fed cuts rates in September," and treating the two matched contracts as the same instrument. They usually aren't. Resolution sources differ (which data feed or news outcome triggers settlement), close times differ down to the minute, and Opinion's optimistic-dispute resolution process means a market can settle, get challenged, and get corrected in a way a purely order-book-driven Kalshi or Polymarket market never will. Two contracts with the same plain-English title can be answering two subtly different questions, and the price gap between them can be entirely rational once you read the actual resolution criteria on each platform, not a mispricing to arbitrage.

Problem 4: five venues, five clocks

Kalshi settles centrally. Polymarket, Limitless, and Predict.Fun settle on three different chains (Polygon, Base, Blast respectively) with different block times and finality guarantees. If you're timestamping a "price at time T" across all five for a chart or an alert, a naive ORDER BY timestamp join will silently misalign trades that only look simultaneous because your resolution is too coarse. On Limitless specifically, markets can run as short as one minute, so a timestamp mismatch of even a few seconds can put you a full price move off.

A minimal normalization pass

None of this requires exotic math. It requires not skipping steps. A bare-minimum normalization function has to at least:

```python

def normalize(platform: str, raw_price, spread=None):

"""

Returns a probability in [0, 1]. Deliberately does NOT attempt

fee-adjustment or cross-platform confidence weighting here,

those need platform-specific handling, not a shared helper.

"""

if platform == "kalshi":

return raw_price / 100 # cents -> probability

if platform == "polymarket": # raw_price is already 0-1, but confirm whether it's a # midpoint or a last-trade fallback before treating it # as equivalent to another platform's midpoint quote. return raw_price

if platform in ("limitless", "predict_fun", "opinion"): return raw_price # already 0-1, see per-platform notes above

raise ValueError(f"unhandled platform: {platform}")```

That function is deliberately unimpressive. The real work isn't the conversion, it's knowing, per platform, which of the four problems above applies before you call it "the same number." Skip that and you've built a tool that displays five prices next to each other and calls it a comparison, when it's actually five different measurements dressed up to look like one.

Why this is worth getting right

We ran into all four of these building PredictionHero, which shows live, normalized odds across Polymarket, Kalshi, Limitless, Predict.Fun, and Opinion on one screen. We're not going to walk through our actual normalization and confidence-weighting logic here, that's the part we've spent the most engineering time on, and it's the reason the dashboard is useful instead of just five embedded iframes. But the four problems above are true regardless of who's solving them, and they're the reason "just hit five APIs and put the numbers in a table" undersells what a real cross-platform view requires.

If you're building anything in this space, a research tool, a trading bot, a class project on market efficiency, budget real time for the normalization layer. It's not formatting. It's the part of the system that decides whether your output is a genuine comparison or five numbers that happen to sit in the same row.

Sources

  • Kalshi: How are prices determined? (https://help.kalshi.com/en/articles/13823836-how-are-prices-determined )
  • Polymarket docs: Prices & Orderbook (https://docs.polymarket.com/concepts/prices-orderbook )
  • Limitless Exchange overview, pm.wiki (https://pm.wiki/projects/limitless-exchange )
  • Predict.Fun, Blast-native prediction market:(https://blast.predict.fun/ )
  • Opinion docs: Fees (https://docs.opinion.trade/trade-on-opinion.trade/fees )

PredictionHero aggregates publicly available prediction market data for informational purposes only. This is not financial advice. Prediction markets may not be available in all jurisdictions.