> ## 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.

# Best practices

> Patterns we've seen work in production: pagination, retries, rate-limit handling, WebSocket reconnect, idempotency, testing strategy.

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

<AccordionGroup>
  <Accordion title="Store the key as an env var, not in code">
    ```bash theme={null}
    export PREDEXON_API_KEY="pk_..."
    ```

    Then in code:

    ```python theme={null}
    headers = {"x-api-key": os.environ["PREDEXON_API_KEY"]}
    ```

    Rotate via the [dashboard](https://dashboard.predexon.com) — keys are scoped per workspace, easy to revoke and re-issue.
  </Accordion>

  <Accordion title="Use one key per environment (dev / staging / prod)">
    Free tier covers dev easily. Use a separate Dev or Pro key for staging so you can verify rate-limit behavior matches prod.
  </Accordion>

  <Accordion title="Per-end-user keys (account-per-user platforms)">
    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](/trading-api/accounts/create-account). End users never see the Predexon key.
  </Accordion>
</AccordionGroup>

***

## Pagination

Most list endpoints support one of two patterns. Use the right one for your scale.

| Pattern                   | Endpoints                                                | When to use                                                                        |
| ------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| **Offset-based**          | Default on `markets`, `trades`, `wallet`, most analytics | One-off queries, small/medium result sets, when you need a known `total`           |
| **Cursor-based (keyset)** | `markets/keyset`, `events/keyset` and a few others       | Large backfills, real-time tailing, when you need consistent ordering across pages |

```python theme={null}
# 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"]
```

<Warning>
  Don't use `offset` past 10,000 — performance degrades and result quality drops. Switch to keyset for anything bigger.
</Warning>

***

## Retries and exponential backoff

Retry **5xx** errors with exponential backoff. **Never** retry 4xx — those are your bug, not ours.

```python theme={null}
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()
```

| Status            | Retry?             | Notes                                                                                      |
| ----------------- | ------------------ | ------------------------------------------------------------------------------------------ |
| `429`             | Yes — with backoff | We send `Retry-After`; the example above respects it via `respect_retry_after_header=True` |
| `500/502/503/504` | Yes — with backoff | Transient. Usually self-heals in `<30s`                                                    |
| `409`             | Maybe — once       | Concurrent modification (fee policy). Single retry, then surface to user                   |
| `400/401/403/404` | No                 | Fix your request                                                                           |

***

## Rate limit handling

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

<Steps>
  <Step title="Cache aggressively">
    Most market data doesn't change every second. Cache list-markets and similar slow-moving endpoints for 60s minimum.

    ```python theme={null}
    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()
    ```
  </Step>

  <Step title="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.
  </Step>

  <Step title="Stream instead of poll for live data">
    If you find yourself polling `/v2/polymarket/trades` every second, switch to the [WebSocket trades channel](/websocket/trades). Streaming consumes WebSocket subs (cheap) instead of REST quota (expensive).
  </Step>

  <Step title="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](mailto:team@predexon.com) — we'll discuss a custom rate before throttling you in production.
  </Step>
</Steps>

***

## WebSocket: reconnect and state rebuild

WebSockets disconnect. Your client must handle it cleanly without losing state.

```python theme={null}
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](/websocket/subscriptions) for connection lifecycle details and [WebSocket Overview](/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:

```python theme={null}
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

<AccordionGroup>
  <Accordion title="Don't mock Predexon endpoints in unit tests">
    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.
  </Accordion>

  <Accordion title="Use low-volume markets for trading integration tests">
    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.
  </Accordion>

  <Accordion title="Replay historical WebSocket events for handler tests">
    Record real events to JSON files, replay them through your handler in tests. Lets you test without keeping a connection open.
  </Accordion>

  <Accordion title="Snapshot endpoint responses">
    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.
  </Accordion>
</AccordionGroup>

***

## Observability

Things you'll want to instrument from day one:

| Metric                                              | Why                                                 |
| --------------------------------------------------- | --------------------------------------------------- |
| Request latency p50/p95/p99 per endpoint            | Catches degradation before users do                 |
| Error rate per endpoint                             | Pinpoints which endpoint broke, not "the API broke" |
| `429` count per minute                              | Tells you when to upgrade tier                      |
| WebSocket disconnect frequency + reconnect duration | If reconnect takes >5s, that's user-visible         |
| Trade placement → confirmation latency              | The 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

<AccordionGroup>
  <Accordion title="Timestamp units differ by endpoint">
    Candles use **seconds**. Orderbook snapshots use **milliseconds**. WebSocket events use seconds except the orderbook channel (milliseconds). Always check the page reference.
  </Accordion>

  <Accordion title="Cents vs decimals">
    Polymarket prices are 0–1 decimals. Kalshi prices are 0–100 cents. Normalize on read.
  </Accordion>

  <Accordion title="condition_id vs token_id">
    A market has one `condition_id` and N `token_id`s (one per outcome). Candles by condition give you market-level OHLCV; candles by token give you per-outcome.
  </Accordion>

  <Accordion title="Aggregated vs per-venue positions">
    `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.
  </Accordion>

  <Accordion title="WebSocket plan gating">
    WebSocket requires Dev plan or higher. You won't see this until you try to connect and get `403`. Test early.
  </Accordion>
</AccordionGroup>

***

## What to read next

<CardGroup cols={2}>
  <Card title="Rate Limits & Plans" icon="gauge-high" href="/rate-limits">
    The detailed limit and free-endpoint matrix.
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    Auth header, x402 pay-per-call, CORS.
  </Card>

  <Card title="WebSocket Subscriptions" icon="bolt" href="/websocket/subscriptions">
    Full subscription lifecycle, ack/resync flow.
  </Card>

  <Card title="Migrating from X" icon="right-left" href="/start-here/migrating">
    Moving from another provider? Common translations.
  </Card>
</CardGroup>
