PredictorSDK
Guides

Trading fees

Compute the true cost of a trade on any provider market, and compare venues that quote the same price.

GET /v1/markets/{market_id} returns a trading_fees object describing what the venue charges to trade that market. This is the prediction market's own fee, normalized across all nine platforms — it has nothing to do with your PredictorSDK plan.

Why normalized price is not enough

Every venue's trading fee is non-linear in price, and none of them is a flat percentage. Two venues can quote the identical ask for the same canonical outcome and still differ by more than 100 bps in true cost — and which one is cheaper can invert during the trading day.

A real example, read live on 2026-08-21. MLB full-game total, LAA @ TEX, line 8.5, Over. Both venues quoted 0.41 / 0.42:

MarketFee parametersFee on a $100 taker buyEffective
kalshi:KXMLBTOTAL-26AUG212015LAATEX-9quadratic, rate 0.035$2.03203 bps
same market, after first pitchquadratic, rate 0.07$4.06406 bps
polymarket:3753849quadratic, rate 0.05, exponent 1$2.90290 bps

Kalshi is 87 bps cheaper before the game starts and 116 bps more expensive after. Both facts are readable in advance — the second one from scheduled_change.

The model

Every published fee across the nine platforms is a per-share fee times the traded share count. Only the price term differs, so one descriptor expresses all of them exactly — except Limitless, whose taker curve is published as table rows with no closed form and reports partial with a null taker leg instead of a fabricated rate (a Limitless market with fees disabled is the exception's exception: it reports published with both legs zero, an asserted free):

fee = shares × f(price)          then apply `rounding`
modelf(price)Used by
quadraticrate × (price × (1 − price)) ^ exponentKalshi, Polymarket, AlphaArcade, Pred
min_pricerate × min(price, 1 − price)Predict
notionalrate × priceHyperliquid
none0fee-free markets, and most maker legs

rate is a decimal fraction, never basis points: 0.07 means 7% of the price term.

The number you actually compare across venues is the effective rate on notional, which is f(price) / price. For quadratic at exponent: 1 that reduces to rate × (1 − price) — so a single configured basis-point rate is exact at one price and wrong everywhere else. At rate: 0.07 the effective cost runs from 693 bps at a price of 0.01 down to 7 bps at 0.99.

min_price is a tent, not a parabola: its effective rate is flat at rate for every price at or below 0.5 and only declines above it. Do not fold it into quadratic.

import type { PredictorSDK } from "@predictorsdk/client";

// The effective rate on notional, or null when the magnitude is unknown: a
// null leg (SX Bet, ProphetX), or a null rate or exponent (Hyperliquid, a
// Limitless taker). Never treat null as zero.
function effectiveRate(
  leg: PredictorSDK.MarketDetailFeeLeg | null | undefined,
  price: number,
): number | null {
  if (leg == null) return null;
  switch (leg.model) {
    case "none":
      return 0;
    case "quadratic":
      if (leg.rate == null || leg.exponent == null) return null;
      return (leg.rate * Math.pow(price * (1 - price), leg.exponent)) / price;
    case "min_price":
      if (leg.rate == null) return null;
      return (leg.rate * Math.min(price, 1 - price)) / price;
    case "notional":
      return leg.rate ?? null;
    default:
      return null;
  }
}

Both the taker and maker legs are always returned and you pick. PredictorSDK does not infer which side you will be — that needs an order it has not seen.

Read availability before you read the rate

availability says what is known; the legs say what is charged. They are deliberately on separate axes, so all four meaningful states are distinguishable:

availabilityMeaningWhere you'll see it
publishedThe full parameter set for this market is known.Kalshi, Polymarket, Predict, AlphaArcade, Pred, and any Limitless market whose fees are disabled (metadata.fee: false)
partialFees may be charged, but something upstream was unreadable. Nothing is guessed.a Polymarket market with fees on and no readable schedule; a Kalshi market whose pending-change list could not be read; every fee-enabled Limitless market (the taker curve has no closed form, so the taker leg stays null while the explicitly-free maker leg is asserted); a Pred market whose record omits fee_rate_bps (absent fee configuration is unconfirmed, not zero)
account_specificRates exist but are set per trading account and need your own venue credentials.SX Bet, Hyperliquid
unpublishedThe venue publishes no fee model for this market at all.reserved
unavailableOur own bounded parameter fetch failed or timed out, or the venue exposes no per-market fee parameters at all. Identity and pricing are still served.a degraded Kalshi lookup; every ProphetX market (the Market Maker API publishes no fee fields on the market record)

A fee-free market is not a gap: it is availability: "published" with model: "none" and rate: 0 — an asserted zero. Polymarket's geopolitics and world-events markets are fee-free by policy, and so are 14 Kalshi series.

For account_specific venues, read your own rates directly: SX Bet's authenticated GET /user/fees-v3, or Hyperliquid's POST /info {"type":"userFees"} (public for any address). A non-null model with a null rate — which is how Hyperliquid reports — means the shape is known and the magnitude is not.

Two fields that change the arithmetic

charge_basis tells you what the fee is assessed on. fill means it is taken when the order fills, from the traded shares and the fill price. settlement_profit means it is taken at settlement, on profit, and only on a position that won — SX Bet works this way. That is an expected-value haircut, not an entry cost; modelling it as a percentage of notional is wrong about the shape before it is wrong about the rate. close_or_settlement means it is taken when a position is closed or settles and never when one is opened — Hyperliquid's outcome markets work this way ("Outcome trading only charges fees when closing or settling, not when opening outcome positions"), so a buy that opens a position costs nothing.

scheduled_change carries the next published parameter change and its effective time. It is omitted when there is none. Kalshi publishes these today, and it matters: the entire MLB family currently runs at a half-rate fee_multiplier with a per-event override restoring the full rate at first pitch, so the fee doubles on a game that is already trading.

Once that change fires, it stops being a scheduled_change and starts being the rate in taker/maker — the same market reports rate: 0.035 with a pending change before first pitch, and rate: 0.07 with no pending change after. If availability is partial, an omitted scheduled_change means the pending list was unreadable, not that no change is coming.

"scheduled_change": {
  "effective_at": "2026-08-22T00:15:00Z",
  "taker": { "model": "quadratic", "rate": 0.07, "exponent": 1,
             "rounding": { "direction": "up", "increment": 0.0001 } },
  "maker": { "model": "none", "rate": 0, "exponent": null, "rounding": null }
}

Rounding, and where the number will still be slightly off

rounding is the venue's published rule — { "direction": "up" | "nearest" | "down", "increment": <number> } — or null where the venue publishes none (Predict). Treat null as unknown rather than as "no rounding", which would understate small trades. Pred is the down case: fees are computed in 6-decimal base units and always rounded down, and a fee below the smallest chargeable unit (0.000001) is zero rather than clamped up to it.

Kalshi additionally charges a per-fill rounding fee that restores your balance precision, offset by a rebate once accumulated rounding exceeds $0.01. That depends on how your order fragments across fills and on your member type, so it is not predictable pre-trade and is not modelled here. Expect a small positive difference between the fee you compute and the fee Kalshi charges across many partial fills.

What is deliberately not here

These are all real and they all move the number, which is why they are documented rather than silently omitted — but they are not per-market venue parameters, so they are not fields:

  • Rebates — Polymarket's maker-rebate and taker-rebate programs.
  • Per-account discounts — Predict's 10% invite discount. rate is the published base; a referred account pays less.
  • Volume and staking tiers — Hyperliquid's fee tier depends on rolling 14-day volume and staked HYPE.
  • A deployer's own fee — each Hyperliquid outcome carries its deployer's deployerFeeScale in outcomeMeta. It is 1.0 on outcomes from the out and skew deployers, and trade.xyz documents 0, which adds no deployer fee, for its own.
  • Third-party pass-through — Kalshi fees charged by an FCM, and Polymarket builder fees, which depend on the front end you route through.
  • Maker/taker inference. Both legs are returned; deciding which applies is execution policy.

Cost of the lookup

Polymarket, Predict, AlphaArcade and Pred carry their fee parameters on the same market record the lookup already fetches, so trading_fees costs them nothing. SX Bet, Hyperliquid, ProphetX and Limitless need no fetch at all. Only Kalshi costs extra upstream hops — its parameters live on the parent series plus any scheduled per-event override — and those are bounded and cached. source tells you which case you are in: market_record, series_record, or venue_schedule.

observed_at is when PredictorSDK read the parameters. It is an observation timestamp, not an upstream one — which is why it is not called as_of, the way pricing.as_of is. It is null for venue_schedule (nothing is read at request time) and for unavailable. pricing.observed_at carries the same meaning for the quotes, and pricing.as_of_kind says whether pricing.as_of tracks the quote or is a record stamp that cannot bound quote age. Even a quote stamp can be hours or days old on a thin book, so size any bound to the venue.

On this page