Metadata-Version: 2.5
Name: parlayx
Version: 3.2.1
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.

Both `client.polymarket` and `client.kalshi` carry seven core methods: `submit_order`, `list_orders`, `get_order`, `cancel_order`, `get_order_fills`, `list_positions` and `get_balance`.

`client.polymarket.cancel_orders` cancels every order you have resting, or narrows to one outcome or market, in a single request:

```python
result = client.polymarket.cancel_orders()
result = client.polymarket.cancel_orders(token_id=token_id)
```

It returns `cancelled` and `not_cancelled`, and a partial outcome is a success rather than an error. Read `not_cancelled` for the orders that are still live and the code saying why.

Kalshi accepts an optional ticker and a continuation token:

```python
result = client.kalshi.cancel_orders(ticker="KXNBAGAME-26OCT20OKCSAS-SAS")
while result.next_page_token:
    result = client.kalshi.cancel_orders(
        ticker="KXNBAGAME-26OCT20OKCSAS-SAS",
        page_token=result.next_page_token,
    )
```

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

### ProphetX order requests

`ProphetXOrderRequest` is a plain Pydantic model, so fields are read and assigned directly and `model_dump()` returns the request body:

```python
from parlayx import ProphetXOrderRequest

order = ProphetXOrderRequest(strike_id="strike-id", price="0.400000", quantity=25)
order.quantity = 20
```

Set `time_in_force={"type": "AUTO_CANCEL"}` for the venue's timed cancellation; it cannot be combined with `post_only=True`, and the API answers 400 if you send both.

`price` is the cost of one share in dollars, greater than 0 and less than 1, with at most six decimal places. ProphetX quotes a fixed set of prices, and one it does not quote is placed at the nearest it does quote at or below the price you sent, so an order never fills worse than you asked. `submit_order` reports the price actually placed on its response, and `get_ladder` returns the set.

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