Metadata-Version: 2.5
Name: parlayx
Version: 1.1.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
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: cryptography<51,>=45
Requires-Dist: httpx<1,>=0.28
Requires-Dist: pydantic<3,>=2.9
Requires-Dist: websockets<18,>=15
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

Set your credentials in the environment:

```sh
export PARLAYX_KEY_ID="3f7c1b2a-9d4e-4a61-b8f0-2c5d7e91a4b3"
export PARLAYX_PRIVATE_KEY_HEX="a3f1...9c20"  # 64 hex characters
```

```python
from parlayx import ParlayX

client = ParlayX()

who = client.whoami()
competitions = client.list_competitions(sport="spt_baseball")
balance = client.kalshi.get_balance()
```

`PARLAYX_PRIVATE_KEY_HEX` is your key's 32-byte seed as 64 lowercase hex characters.

Pass either credential directly to take it from somewhere else, such as a secret manager (`vault` below is your own client). An argument always wins over the environment, and each resolves on its own, so the key id can be inline while the private key stays out of your source:

```python
client = ParlayX(private_key_hex=vault.read("parlayx/private-key"))
```

A credential that is neither passed nor exported raises `ValueError` naming the variable it wanted, at construction rather than on your first request. Only an omitted argument falls back to the environment: an argument you did pass is used as given, and a blank one is rejected rather than replaced, so a lookup of your own that returned `""` cannot end up signing as whatever account the environment happens to hold.

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:
        ...
```

A `RATE_LIMITED` refusal also carries `retry_after_seconds`, read from the `Retry-After` header. It is `None` when the header is absent or unreadable, so keep a fallback:

```python
import time

try:
    balance = client.polymarket.get_balance()
except RequestError as error:
    if error.code != "RATE_LIMITED":
        raise
    time.sleep(error.retry_after_seconds or 1)
    balance = client.polymarket.get_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() as client:
    who = await client.whoami()
```

### Market data stream

The stream is a separate import, with nothing extra to install:

```python
from parlayx.stream import StreamClient
```

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

stream = StreamClient()

async with 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(
    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).
