Metadata-Version: 2.5
Name: parlayx
Version: 0.0.0
Summary: Official Python client for the ParlayX public API.
Project-URL: Homepage, https://docs.parlayx.com
Project-URL: Documentation, https://docs.parlayx.com
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: cryptography<51,>=45
Requires-Dist: httpx<1,>=0.28
Requires-Dist: pydantic<3,>=2.9
Provides-Extra: stream
Requires-Dist: websockets<18,>=15; extra == 'stream'
Description-Content-Type: text/markdown

# parlayx

Official Python client for the [ParlayX](https://parlayx.com) public API, a single trading interface over aggregated prediction markets.

## Documentation

Full guides, authentication, and API reference live at **[docs.parlayx.com](https://docs.parlayx.com)**.

## Install

```sh
pip install parlayx
```

Requires Python 3.11 or newer. Requests are signed with your Ed25519 key, so the client runs server-side: the private key stays in your own process and only the key id, a timestamp, and the signature go on the wire.

## Usage

```python
from parlayx import ParlayX

with ParlayX.from_env() as client:
    who = client.whoami()
    competitions = client.list_competitions(sport="spt_baseball")
    balance = client.kalshi.get_balance()
```

`from_env()` reads `PARLAYX_KEY_ID` and `PARLAYX_PRIVATE_KEY_HEX`; pass them to `ParlayX(...)` directly if you load them another way.

Orders, positions and balances are venue-scoped, because the venues address markets differently:

```python
from parlayx import KalshiOrderRequest

order = client.kalshi.submit_order(
    KalshiOrderRequest(
        ticker="KXMLBGAME-26SEP01-NYY",
        side="YES",
        action="BUY",
        count=10,
        price_cents=45,
    )
)
print(order.order_id, order.status)
```

Fields accept either spelling: `price_cents` or `priceCents`. The wire always carries the API's own camelCase.

Each of `client.polymarket` and `client.kalshi` carries the same seven methods: `submit_order`, `list_orders`, `get_order`, `cancel_order`, `get_order_fills`, `list_positions` and `get_balance`.

### Idempotency

`submit_order` attaches a fresh idempotency key per call, so retrying a call that failed submits a **second order**. Pass your own key to make a specific retry safe:

```python
client.kalshi.submit_order(order, idempotency_key="my-retry-key")
```

### Errors

Every non-2xx response raises `RequestError`, carrying a stable `code`, the HTTP `status`, a `message` and the parsed `body`:

```python
from parlayx import ApiErrorCode, RequestError

try:
    client.polymarket.get_balance()
except RequestError as error:
    if error.code == ApiErrorCode.insufficient_balance:
        ...
```

### Pagination

Order listings are cursored. The paginators follow `nextPageToken` to exhaustion:

```python
from parlayx import paginate_kalshi_orders

for order in paginate_kalshi_orders(client, status="OPEN"):
    print(order.order_id, order.status)
```

Positions and fills are not cursored and return their full result in one call.

### Async

`AsyncParlayX` is the same surface with `await`, and `apaginate_kalshi_orders` and `apaginate_polymarket_orders` are the async paginators:

```python
from parlayx import AsyncParlayX

async with AsyncParlayX.from_env() as client:
    who = await client.whoami()
```

### Market data stream

The stream is a separate import and needs the extra:

```sh
pip install "parlayx[stream]"
```

It authenticates with the same signing key, re-signing the handshake on every connect, and replays your subscriptions across reconnects so you subscribe once:

```python
from parlayx.stream import StreamClient

async with StreamClient.from_env() as stream:
    await stream.subscribe([{"venue": "polymarket", "tokenId": "97840505..."}])
    async for frame in stream:
        if frame.type == "snapshot":
            print(frame.seq, frame.bids[:3], frame.asks[:3])
        elif frame.type == "delta":
            for change in frame.changes:
                ...  # size 0 removes the level, anything else sets it
```

`channels` defaults to `["book"]`; pass `["book", "trade"]` for the trade tape as well.

Connection lifecycle is reported through callbacks rather than frames, because it describes the connection rather than the market:

```python
StreamClient.from_env(
    on_reconnecting=lambda event: log.info("reconnecting, attempt %s", event.attempt),
    on_terminated=lambda event: log.warning("stream stopped: %s", event.reason),
    on_unparseable=lambda raw: log.warning("dropped an unrecognised frame: %s", raw),
)
```

A terminated stream does not reconnect; build a new client to resume.

### One inconsistency worth knowing

Discovery listings report `venue` in uppercase (`KALSHI`, `POLYMARKET`), while the order and streaming surfaces take it lowercase. The client passes the value through unchanged in both directions rather than quietly rewriting it.

## License

MIT. See [LICENSE](./LICENSE).
