Metadata-Version: 2.5
Name: parlayx
Version: 3.2.5
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="BID",
        count="10",
        price="0.45",
    )
)
print(order.order_id, order.status)
```

Fields accept either spelling, such as `time_in_force` or `timeInForce`. 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": "FOK"}` for the venue's fill-or-kill, where an order with no match is cancelled at the venue after about two seconds. 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.

## Connections

From 3.2.3 the client keeps its connection to the API open between requests, so
an order sent after a few seconds of quiet does not pay for a new TLS handshake. Create
one client when your process starts and reuse it.

Close it when your work is done, or use it as a context manager:

```python
with ParlayX() as client:
    client.whoami()
```

Supplying your own `transport` replaces the pool, and with it the setting that
holds the connection open.

## Migrating to 3.2.3

Kalshi orders and positions are now described the way Kalshi describes them, and
every field below changed shape. Nothing else changed; the connection behaviour
above is separate and needs nothing from you.

A Kalshi order carries one `side`, either `"BID"` or `"ASK"`, in place of the old
`side` (`"YES"` / `"NO"`) and `action` (`"BUY"` / `"SELL"`) pair. A `BID` buys YES
contracts and profits if the market resolves YES; an `ASK` sells them and profits
if it resolves NO. Selling YES and buying NO are the same order on Kalshi's book,
so both are `"ASK"`.

```python
# 3.2.2
order = KalshiOrderRequest(ticker=ticker, side="YES", action="BUY", count=10, price_cents=38)
# 3.2.3
order = KalshiOrderRequest(ticker=ticker, side="BID", count="10", price="0.38")
```

The old pair maps across like this:

| 3.2.2 `side` + `action` | 3.2.3 `side` | 3.2.3 `price`                 |
| ----------------------- | ------------ | ----------------------------- |
| `YES` + `BUY`           | `BID`        | the YES price you sent        |
| `YES` + `SELL`          | `ASK`        | the YES price you sent        |
| `NO` + `BUY`            | `ASK`        | 1 minus the NO price you sent |
| `NO` + `SELL`           | `BID`        | 1 minus the NO price you sent |

`count` and `price` are decimal strings rather than numbers. `price` is dollars
per contract on the market's YES book, replacing `price_cents`; `"0.38"` is what
`price_cents=38` used to mean. Contracts trade in steps of 0.01 and prices go as
fine as $0.0001, so sizes and prices Kalshi accepts are no longer refused before
they reach it. Each market publishes its own grid of valid prices, and a price
off that grid is refused rather than moved to the nearest one.

Order and fill reads changed to match: `side` reads `"BID"` or `"ASK"`, `count`
and `price` are decimal strings on the YES book, and `average_price` replaces
`average_price_cents`. Orders placed before the upgrade read back in the new
vocabulary, including ones placed on the NO leg. Amend and decrease take the same
decimal strings, and their `previous` block reports `price` and `count` the same
way.

Positions changed with them. A Kalshi position is keyed by ticker alone, because
the venue nets a market into one holding, and `side` is gone from the row.
`contracts` is a decimal string on the market's YES side and its sign says which
way the holding leans: positive is long YES, and negative is long NO, which is
the same holding as short YES. It is a decimal because contracts trade in steps
of 0.01. A ticker you hold both ways is one row now rather
than two you have to net yourself.

```python
from decimal import Decimal

# 3.2.2: two rows for one holding, netted by hand, as integers
long, short = (p for p in positions if p.ticker == ticker)
net = long.contracts - short.contracts
# 3.2.3: one row, and the venue's own number
(position,) = (p for p in positions if p.ticker == ticker)
net = Decimal(position.contracts)
```

`average_price` and `mark` replace `average_price_cents` and `mark_cents`, as
dollar strings from 0 to 1 on that same YES side, which is the frame you submit a
price in. They are no longer rounded to a whole cent, so a market quoting in
tenths of a cent reports the price it actually trades at. `average_price` reads
`None` when the holding has no single entry price, and `value_usd` and
`unrealized_pnl_usd` compose from the two published figures.

## License

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