Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.predexon.com/llms.txt

Use this file to discover all available pages before exploring further.

Engineering patterns we recommend when shipping a Predexon integration. None of this is required to make your first call work — it’s what separates a working prototype from a production system.

Authentication

export PREDEXON_API_KEY="pk_..."
Then in code:
headers = {"x-api-key": os.environ["PREDEXON_API_KEY"]}
Rotate via the dashboard — keys are scoped per workspace, easy to revoke and re-issue.
Free tier covers dev easily. Use a separate Dev or Pro key for staging so you can verify rate-limit behavior matches prod.
If you’re building an account-per-user product, you don’t need per-user Predexon keys. One platform key creates accounts on behalf of end users via the Trading API. End users never see the Predexon key.

Pagination

Most list endpoints support one of two patterns. Use the right one for your scale.
PatternEndpointsWhen to use
Offset-basedDefault on markets, trades, wallet, most analyticsOne-off queries, small/medium result sets, when you need a known total
Cursor-based (keyset)markets/keyset, events/keyset and a few othersLarge backfills, real-time tailing, when you need consistent ordering across pages
# Offset — fine for top-100 lists
def all_markets(limit=100):
    out, offset = [], 0
    while True:
        page = requests.get(
            f"{BASE}/v2/polymarket/markets",
            headers=H,
            params={"limit": limit, "offset": offset, "status": "open"},
        ).json()
        out.extend(page["markets"])
        if not page["pagination"]["has_more"]:
            return out
        offset += limit

# Keyset — required for large backfills (no offset ceiling)
def all_markets_keyset(limit=200):
    out, cursor = [], None
    while True:
        params = {"limit": limit}
        if cursor:
            params["cursor"] = cursor
        page = requests.get(
            f"{BASE}/v2/polymarket/markets/keyset",
            headers=H,
            params=params,
        ).json()
        out.extend(page["markets"])
        if not page["pagination"]["has_more"]:
            return out
        cursor = page["pagination"]["next_cursor"]
Don’t use offset past 10,000 — performance degrades and result quality drops. Switch to keyset for anything bigger.

Retries and exponential backoff

Retry 5xx errors with exponential backoff. Never retry 4xx — those are your bug, not ours.
import time, requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def make_session():
    s = requests.Session()
    s.headers.update({"x-api-key": os.environ["PREDEXON_API_KEY"]})
    retry = Retry(
        total=5,
        backoff_factor=0.5,            # 0.5s, 1s, 2s, 4s, 8s
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET", "POST", "PUT", "DELETE"],
        respect_retry_after_header=True,
    )
    s.mount("https://", HTTPAdapter(max_retries=retry))
    return s

session = make_session()
StatusRetry?Notes
429Yes — with backoffWe send Retry-After; the example above respects it via respect_retry_after_header=True
500/502/503/504Yes — with backoffTransient. Usually self-heals in <30s
409Maybe — onceConcurrent modification (fee policy). Single retry, then surface to user
400/401/403/404NoFix your request

Rate limit handling

You’ll hit 429 long before any other failure mode. Cheap defenses, in order of effort:
1

Cache aggressively

Most market data doesn’t change every second. Cache list-markets and similar slow-moving endpoints for 60s minimum.
from functools import lru_cache
from cachetools import TTLCache, cached

cache = TTLCache(maxsize=1000, ttl=60)

@cached(cache)
def market(condition_id):
    return session.get(f"{BASE}/v2/polymarket/markets", params={"condition_id": condition_id}).json()
2

Batch where the API supports it

/v2/polymarket/wallets/profiles accepts up to 20 wallets per call. /v2/polymarket/wallets/filter lets you filter server-side instead of pulling everything and filtering locally. Use them.
3

Stream instead of poll for live data

If you find yourself polling /v2/polymarket/trades every second, switch to the WebSocket trades channel. Streaming consumes WebSocket subs (cheap) instead of REST quota (expensive).
4

Upgrade tier or talk to us

Free is 1 req/s. Dev is 20 req/s. Pro is 100 req/s. Enterprise is custom. If you have a sustained workload above Pro, email us — we’ll discuss a custom rate before throttling you in production.

WebSocket: reconnect and state rebuild

WebSockets disconnect. Your client must handle it cleanly without losing state.
import json, time
from websockets.sync.client import connect

WSS = "wss://wss.predexon.com/v1"
KEY = os.environ["PREDEXON_API_KEY"]

SUBSCRIPTIONS = [
    {"action": "subscribe", "platform": "polymarket", "version": 1,
     "type": "orders", "filters": {"users": ["0x1234..."]}},
    # ... add more
]

def run_forever():
    backoff = 1.0
    while True:
        try:
            with connect(f"{WSS}/{KEY}") as ws:
                for s in SUBSCRIPTIONS:
                    ws.send(json.dumps(s))

                # On every reconnect, re-pull state from REST to bridge the gap
                rebuild_state_from_rest()

                backoff = 1.0  # reset on successful connect
                for raw in ws:
                    msg = json.loads(raw)
                    if msg.get("type") == "event":
                        handle(msg)
                    elif msg.get("data", {}).get("event_type") == "resync":
                        rebuild_state_from_rest()
        except Exception as e:
            print(f"WS disconnect: {e}; backoff {backoff}s")
            time.sleep(backoff)
            backoff = min(backoff * 2, 60)
Three rules:
  1. Re-pull REST state on every reconnect — bridges any events you missed during the disconnect.
  2. Handle resync events from the server — it tells you “rebuild state, more snapshots coming.” Same recovery as a hard reconnect.
  3. Exponential backoff on reconnect attempts, capped at 60s. Don’t hammer the server during an outage.
See WebSocket Subscriptions for connection lifecycle details and WebSocket Overview for keepalive timing.

Idempotency

Trading API order placements aren’t idempotent by default — calling Place Order twice with the same body creates two orders. For at-most-once semantics:
import uuid

# Generate one client_id per *logical* order
client_id = str(uuid.uuid4())

# Pass it in the request. If the network drops and you retry, pass the same client_id.
order = session.post(
    f"{TRADE}/api/accounts/{account_id}/orders",
    json={
        "venue": "polymarket",
        "market": {"tokenId": token_id},
        "side": "buy", "type": "limit",
        "size": "10", "price": "0.50",
        "clientId": client_id,           # ← reuse across retries
    },
).json()
If the server already saw this clientId, it returns the existing order instead of creating a duplicate. Critical for any retry logic on the Trading API.

Testing strategy

Mock your business logic that calls Predexon. The endpoints themselves should be hit in integration tests against real markets — that’s the only way to catch shape changes early.
Trading API has no sandbox. Test against real venues with $1 trades on illiquid markets — cheap real-world signal. Free tier is fine for this; trading endpoints don’t consume your Data API quota.
Record real events to JSON files, replay them through your handler in tests. Lets you test without keeping a connection open.
Capture a real response, snapshot-test against it. When we ship a non-breaking field addition, your test still passes; when a breaking change ships, your test catches it.

Observability

Things you’ll want to instrument from day one:
MetricWhy
Request latency p50/p95/p99 per endpointCatches degradation before users do
Error rate per endpointPinpoints which endpoint broke, not “the API broke”
429 count per minuteTells you when to upgrade tier
WebSocket disconnect frequency + reconnect durationIf reconnect takes >5s, that’s user-visible
Trade placement → confirmation latencyThe Trading API number that matters most
clientId collision rate (should be 0)Indicates retry storms
We don’t have a public status page yet, but GET /health on every base URL returns {"status": "healthy"} — use it as a synthetic check.

Common gotchas

Candles use seconds. Orderbook snapshots use milliseconds. WebSocket events use seconds except the orderbook channel (milliseconds). Always check the page reference.
Polymarket prices are 0–1 decimals. Kalshi prices are 0–100 cents. Normalize on read.
A market has one condition_id and N token_ids (one per outcome). Candles by condition give you market-level OHLCV; candles by token give you per-outcome.
GET /api/accounts/{id}/positions returns per-venue rows by default. Pass ?aggregated=true to collapse cross-venue positions into one row per canonical outcome.
WebSocket requires Dev plan or higher. You won’t see this until you try to connect and get 403. Test early.

Rate Limits & Plans

The detailed limit and free-endpoint matrix.

Authentication

Auth header, x402 pay-per-call, CORS.

WebSocket Subscriptions

Full subscription lifecycle, ack/resync flow.

Migrating from X

Moving from another provider? Common translations.