Predict.fun API: Read BNB-Chain Prediction Market Data (Python)

Predict.fun API - OddsPapi API Blog
How To Guides May 5, 2026

Predict.fun: The BNB-Chain Prediction Market You Probably Haven’t Scraped Yet

Predict.fun is an onchain prediction market built on BNB Chain. Binary yes/no markets, share prices trading between 0 and 1, an off-chain CLOB order book matching trades that settle into onchain conditional tokens. If you’ve built tooling against Polymarket’s CLOB or Kalshi’s REST API, the mental model will feel familiar — but the chain is different, the endpoints are different, and there are a few Predict.fun-specific quirks you’ll want to know about before you start hitting the API in anger.

This is a direct Predict.fun integration guide. No OddsPapi wrapper, no “third option” angle — the markets that Predict.fun hosts (politics, culture, crypto, non-sports event markets) aren’t in the OddsPapi coverage set today, so if you want that data you’re going to talk to Predict.fun directly. This post walks you through exactly how.

(When you want to fold sportsbook prices alongside your Predict.fun data for arbitrage or calibration modeling, OddsPapi is the fastest way to pull 350+ bookmakers in one call. But that’s the second half of your stack — not this post.)

The Short Version: Two APIs, Two Environments

Environment Base URL API Key Rate Limit
Mainnet https://api.predict.fun Required (Discord ticket to obtain) 240 req/min default
Testnet https://api-testnet.predict.fun Not required 240 req/min

Use testnet for everything in this tutorial. You don’t need an account, you don’t need a BNB wallet, you don’t need to file a Discord ticket. Mainnet and testnet share the same endpoint surface, and once you have code working against testnet you swap the base URL and add an x-api-key header.

There’s also an official TypeScript SDK on npm and a Python SDK from the team if you don’t feel like hand-rolling HTTP calls. For this tutorial we’re going to call the REST API directly in Python, because that maps onto what you’ll actually debug in DevTools.

The ID Taxonomy (This Is the Part That Trips Everyone)

Predict.fun has four different identifiers that refer to slightly different things. You have to know which one each endpoint wants:

  • Category slug — human-readable string for an event group (e.g. epl-cry-mac-2025-12-14). Used to fetch related markets for one real-world event.
  • Market ID — integer (e.g. 472). This is Predict.fun’s internal primary key for a market and what the REST API routes care about.
  • Condition ID — 32-byte hex string (0xbcad63b0…). Onchain identifier for the CTF (Conditional Tokens Framework) market. This is the BNB-chain hash, the thing you’d quote against the smart contract directly.
  • onChainId (token ID) — 77-digit integer ERC-1155 position ID. One per outcome. This is what you actually buy and sell — it’s the token you’d transfer or stake.

Rule of thumb: if you’re hitting the REST API, use market ID. If you’re talking to the onchain contract, use condition ID (the market hash) and onChainId (the specific outcome token). The relationship is 1 market → N outcomes, each outcome has its own onChainId.

Step 1: List Active Markets

import requests

BASE = "https://api-testnet.predict.fun"

def list_markets(limit=20, cursor=None):
    params = {"limit": limit}
    if cursor:
        params["cursor"] = cursor
    r = requests.get(f"{BASE}/v1/markets", params=params, timeout=10)
    r.raise_for_status()
    return r.json()

page = list_markets(limit=5)
for m in page["data"]:
    print(f"#{m['id']:5} [{m['tradingStatus']:8}] {m['title'][:60]}")

next_cursor = page.get("cursor")
print(f"\nnext cursor: {next_cursor}")

The response is a cursor-paginated envelope:

{
  "data": [ {market...}, {market...}, ... ],
  "cursor": "NDEw"   // base64-encoded cursor; pass it back as ?cursor= on the next page
}

Pagination is cursor-based, not offset-based. Keep hitting ?cursor=NEXT until the response is empty.

The Market Object Shape

Each entry in data[] looks like this (trimmed — there are more fields):

{
  "id": 472,
  "conditionId": "0xbcad63b00f19d2258f318615eedf2bab7ec5afbec1426d4497f8db19ce3e10f1",
  "title": "Crystal Palace FC",
  "question": "Crystal Palace FC",
  "description": "Crystal Palace FC",
  "categorySlug": "epl-cry-mac-2025-12-14",
  "tradingStatus": "OPEN",
  "status": "REGISTERED",
  "feeRateBps": 200,
  "decimalPrecision": 3,
  "isNegRisk": true,
  "kalshiMarketTicker": null,
  "outcomes": [
    {
      "indexSet": 1,
      "name": "Yes",
      "onChainId": "99687883996711364087088282638523080562700966541094159995494261186865075656885",
      "status": null
    },
    {
      "indexSet": 2,
      "name": "No",
      "onChainId": "6040251633342283005115896327558952818368938050220830653939030411351823082209",
      "status": null
    }
  ],
  "resolverAddress": "0x52DA245ac170155391e7607c67b77D549005002d",
  "shareThreshold": 100,
  "spreadThreshold": 0.06
}

Three fields worth highlighting:

  • feeRateBps — trading fee in basis points (200 = 2%). This is deducted from realized PnL, not from your order.
  • isNegRisk — tells you whether the market is part of a “negative risk” group (multiple mutually-exclusive markets that share collateral). Relevant for categorical event markets, irrelevant for single yes/no binaries.
  • kalshiMarketTicker — if not null, this market mirrors a Kalshi US-regulated market with the same ticker. Useful if you’re building Kalshi↔onchain arbitrage — you know which Predict.fun market to query when you see the Kalshi ticker.

Step 2: Fetch the Order Book for a Single Market

def get_orderbook(market_id):
    r = requests.get(
        f"{BASE}/v1/markets/{market_id}/orderbook",
        timeout=10,
    )
    r.raise_for_status()
    return r.json()["data"]

book = get_orderbook(472)
print(f"market {book['marketId']}")
print("asks:", book["asks"])
print("bids:", book["bids"])

Response:

{
  "marketId": 472,
  "asks": [[0.614, 2.2894]],
  "bids": [],
  "lastOrderSettled": {
    "id": "312750",
    "kind": "LIMIT",
    "marketId": 472,
    "outcome": "No",
    "price": "0.61",
    "side": "Bid"
  },
  "updateTimestampMs": 1775914128448
}

A few things to notice:

  • Ladders are [price, size] tuples, sorted best first. asks[0] is the best ask, bids[0] is the best bid.
  • Prices are share prices (0–1), not decimal odds. 0.614 means “buy one YES share for $0.614 in USDC, redeems for $1 if yes resolves.” Convert to decimal odds with 1 / share_price if you need to compare against a sportsbook.
  • Size is in shares, not dollars. A size of 2.2894 at price 0.614 means 2.2894 shares available, or ~$1.41 of USDC notional.
  • lastOrderSettled is the most recent matched trade. Useful for populating a “last sale” column in a dashboard without having to query trade history separately.
  • Empty ladder side = no open orders. In the sample above, there are zero open bids. That’s normal for thin markets.

Step 3: Compute Implied Probability and Spread

The whole point of share prices is they already read as implied probability. Turn that into a quick sanity check:

def analyze(book):
    asks = book["asks"]
    bids = book["bids"]

    best_ask = asks[0][0] if asks else None
    best_bid = bids[0][0] if bids else None

    if best_ask and best_bid:
        mid = (best_ask + best_bid) / 2
        spread = best_ask - best_bid
        print(f"mid price: {mid:.3f} ({mid*100:.1f}% implied prob)")
        print(f"spread:    {spread:.3f} ({spread/mid*100:.2f}% of mid)")
    elif best_ask:
        print(f"best ask only: {best_ask} — no bids in book")
    elif best_bid:
        print(f"best bid only: {best_bid} — no asks in book")
    else:
        print("empty book")

analyze(get_orderbook(472))

That spread number is the round-trip cost of taking and flipping a position. On a healthy Predict.fun market it’s tight (sub-2%). On a thin market it can be 10%+, in which case you’re pricing the market, not taking it.

Step 4: Pull Price History (Timeseries)

Predict.fun exposes a timeseries endpoint for historical prices — useful for charting, backtesting, and calibration. Query it by market ID:

def get_timeseries(market_id, interval="1h"):
    r = requests.get(
        f"{BASE}/v1/markets/{market_id}/timeseries",
        params={"interval": interval},
        timeout=10,
    )
    r.raise_for_status()
    return r.json()

# Grab 1-hour candles for a market
ts = get_timeseries(472, interval="1h")
for point in ts.get("data", [])[:10]:
    print(point)

(Interval options and exact field names are in the OpenAPI reference at https://api.predict.fun/docs — the testnet response matches mainnet so you can develop against testnet without burning your mainnet rate limit.)

Step 5: Filter by Category

Every market belongs to a category (a real-world event group). Use the category endpoint to find all markets for one event:

def markets_in_category(slug):
    r = requests.get(
        f"{BASE}/v1/categories/{slug}",
        timeout=10,
    )
    r.raise_for_status()
    return r.json()

data = markets_in_category("epl-cry-mac-2025-12-14")
# Returns the category metadata plus the markets attached to it
print(data)

For a soccer match, expect the category to contain multiple Predict.fun markets — home win, away win, draw, totals, and often a set of flat-binary player/event markets. Each has its own id and its own orderbook.

Step 6: Trading (Signed Orders — Write Side)

Reading is free and unauthenticated on testnet. Trading requires authentication:

  1. Hit GET /v1/auth/message to retrieve an EIP-712 typed-data message to sign.
  2. Sign it with your EOA wallet or a Privy smart wallet (BNB Chain).
  3. POST the signature to /v1/auth/jwt; you get back a JWT.
  4. Include the JWT in the Authorization: Bearer ... header on order-placement endpoints.

For most readers of this post, you’re staying on the read side — orderbook, price, history — and trading happens through the official SDKs or the Predict.fun UI. If you do need to trade programmatically, the Python SDK wraps the signing flow cleanly.

Gotchas You’ll Hit in the First Hour

  • Testnet markets expire. The testnet is a real sandbox — markets resolve, new ones get created, and the IDs you hardcode today will not exist tomorrow. Fetch a live market ID at the start of any script.
  • Prices are strings in some endpoints, floats in others. The orderbook returns prices as numeric, but lastOrderSettled.price is a string. Cast before math.
  • Empty ladders are normal. A market can have zero bids and one ask, or vice versa. Your code needs to handle both sides being empty.
  • Share price ≠ decimal odds. Predict.fun is native-onchain and uses the 0–1 share-price convention like Polymarket. Convert with 1 / share_price before comparing to a sportsbook’s decimal odds.
  • Mainnet rate limit is 240 req/min per key. That’s generous but not unlimited. If you’re polling 500 markets for live prices, use the orderbook endpoint selectively and consider timeseries for historical work.

Where Predict.fun Fits in Your Stack

Predict.fun is strongest on onchain-native markets — crypto events, culture, politics — where liquidity has shifted toward decentralized venues. It does host some sports markets (the EPL example above is live on testnet), and when kalshiMarketTicker is set, you have a direct bridge to the same underlying event on a US-regulated venue.

The natural partner data set is sportsbook odds for the same events — and that’s exactly what OddsPapi is built for. Pull Predict.fun’s implied probability from the orderbook, pull Pinnacle’s no-vig line from /v4/odds?bookmakers=pinnacle, and the gap between them is the signal a prediction-market-vs-sportsbook model lives on.

# Predict.fun side: implied probability from share price
predict_mid = (best_ask + best_bid) / 2  # e.g. 0.58

# OddsPapi side: no-vig implied probability from Pinnacle
import requests
odds = requests.get(
    "https://api.oddspapi.io/v4/odds",
    params={"apiKey": "YOUR_ODDSPAPI_KEY", "fixtureId": "...", "bookmakers": "pinnacle"},
).json()
pin_price = odds["bookmakerOdds"]["pinnacle"]["markets"]["101"]["outcomes"]["101"]["players"]["0"]["price"]
pin_implied = 1 / pin_price  # needs de-juicing for fair comparison

print(f"predict.fun: {predict_mid:.1%}  vs  pinnacle: {pin_implied:.1%}")

One venue for the onchain probability, one venue for 350+ sportsbooks’ worth of traditional pricing. That’s the whole prediction-market stack with two HTTP calls.

Get Started

  1. Clone the tutorial above against https://api-testnet.predict.fun — no key, no wallet, works immediately.
  2. File a Discord ticket with the Predict.fun team to get a mainnet API key when you’re ready to move to real data.
  3. For the sportsbook side of your comparison model, grab a free OddsPapi key at oddspapi.io and pull Pinnacle, Betfair Exchange, Polymarket, and Kalshi in one call.
curl "https://api-testnet.predict.fun/v1/markets?limit=5"

Zero auth. Public testnet. Start reading onchain prediction-market orderbooks in one HTTP call.