Metadata-Version: 2.5
Name: polynode
Version: 0.13.0
Summary: Python SDK for the Polynode real-time prediction market data platform
Project-URL: Homepage, https://polynode.dev
Project-URL: Documentation, https://docs.polynode.dev
Project-URL: Repository, https://github.com/Polynode-Dev/sdk-python
Author-email: Polynode <josh@quantish.live>
License: MIT
Keywords: polymarket,polynode,prediction-markets,trading,websocket
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Requires-Dist: websockets>=12.0
Provides-Extra: all
Requires-Dist: aiosqlite>=0.20; extra == 'all'
Requires-Dist: eth-account>=0.13; extra == 'all'
Requires-Dist: web3>=7.0; extra == 'all'
Provides-Extra: cache
Requires-Dist: aiosqlite>=0.20; extra == 'cache'
Provides-Extra: trading
Requires-Dist: eth-account>=0.13; extra == 'trading'
Requires-Dist: web3>=7.0; extra == 'trading'
Description-Content-Type: text/markdown

# polynode

Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.

**New in v0.13.0:** Trading adds explicit `user_owned` execution. Existing builder mode remains the default; opted-in wallets use zero builder attribution, one wallet-ownership authorization, and strict wallet-bound gasless credentials. Builder credentials and nonzero builder codes fail closed in this mode. EOA-controlled Safe and deposit wallets are supported; legacy Magic/proxy wallets are intentionally excluded from the first release.

**In v0.12.2:** Python provides the same core capabilities as the TypeScript and Rust SDKs: complete V3 API access, the V3 perps WebSocket, reconnect-aware settlement delivery, and PN1 orderbook integrity. Unknown additive events remain available as raw payloads, decimal values remain precision-safe, and any local queue eviction is reported.

**New in v0.11.0:** Current-production parity. Trading now defaults to CLOB V2 on `clob.polymarket.com`, uses Polynode's public builder attribution unless overridden, omits removed V1 wire fields, and supports V2 GTD expiration. Managed 5-minute, 15-minute, and 4-hour streams select the required 30/60-second Chainlink TWAP lookbacks on a dedicated connection and reconnect/resubscribe at every market rotation. WebSocket models, presets, and filters now cover current redemption, position-conversion, dome/fill, and PM2 combo events. REST position queries now include redeemable/condition filters, multi-wallet batches, and market-holder views; connection and status observability match the current public API.

**New in v0.10.8:** POLY_1271 V2 order signatures now normalize the ERC-7739 `TypedDataSign` recovery byte to Ethereum `v=27/28` for on-chain ERC-1271 validation.

**In v0.10.7:** Polymarket V2 deposit-wallet trading fixes. `ensure_ready()` detects deployed `POLY_1271` wallets correctly, V2 type-3 orders use the deposit wallet as both maker and signer, and existing local credentials can be repaired by rerunning `ensure_ready()`.

## Install

```bash
pip install polynode
```

For trading support:
```bash
pip install 'polynode[trading]'
```

## Quick Start

### REST API

```python
from polynode import PolyNode

with PolyNode(api_key="pn_live_...") as pn:
    status = pn.status()
    connections = pn.connections()
    markets = pn.markets(count=10)
    settlements = pn.recent_settlements(count=5)
    wallet_positions = pn.wallet_positions(
        address, redeemable=True, condition_id=condition_id
    )
    batch_positions = pn.multi_wallet_positions([address, second_address], limit=100)
    market_positions = pn.market_positions(
        condition_id, sort_by="CURRENT_VALUE", min_size=0.01
    )
    onchain_positions = pn.wallet_onchain_positions(
        address, since=window_start, tag_slug="crypto"
    )
```

### Sports and Online Context

```python
from polynode import PolyNode

with PolyNode(api_key="pn_live_...") as pn:
    state = pn.sports_game_state(
        "nba-cle-nyk-2026-05-31",
        price_limit_tokens=20,
    )

    context = pn.sports_game_context(
        "nba-cle-nyk-2026-05-31",
        sources=["online"],
        query_set="injuries",
        max_queries=2,
        max_per_query=5,
        include_state=True,
    )

    web = pn.search_online(
        "Cavaliers Knicks injury news",
        max_results=5,
    )
```

### Async REST

```python
import asyncio
from polynode import AsyncPolyNode

async def main():
    async with AsyncPolyNode(api_key="pn_live_...") as pn:
        status = await pn.status()
        markets = await pn.markets(count=10)

asyncio.run(main())
```

### Complete V3 API

V3 includes wallets, combos, rewards, credits, identities, markets, builders, profiles, perps, crypto, sports, backtesting, and other current product families. `execute()` gives you access to all 120 current V3 operations through one consistent Python interface.

```python
import asyncio
from polynode import AsyncPolyNode

async def read_v3(address: str):
    async with AsyncPolyNode(api_key="pn_live_...") as pn:
        print(len(pn.v3.operations))  # 120

        combo_activity = await pn.v3.execute(
            "GET /v3/combos/activity",
            query={"limit": 25},
        )
        wallet_rewards = await pn.v3.execute(
            "GET /v3/wallets/{address}/rewards",
            path_params={"address": address},
            query={"limit": 100},
        )
        return combo_activity, wallet_rewards

asyncio.run(read_v3("0xabc..."))
```

The SDK encodes path parameters for you. Read requests that are safe to repeat retry transient failures and honor `Retry-After`; requests that change data are never retried automatically. JSON decimals decode as `Decimal`, and `ApiError` exposes the status, request ID, retry details, and a request URL with credentials removed.

Current presets include `dome`, `fills`, `combos`, `redemptions`, and `deposits`. Current filters include `since()`, `combo_condition_ids()`, `leg_position_ids()`, `event_ids()`, `module_ids()`, `action()`, and `direction()`.

`dome` and `fills` change settlement delivery into a flat, per-fill wire
format. Use one of those presets on a dedicated `PolyNodeWS` connection when
also consuming non-fill events; the server deduplicates delivery per
connection and cannot deliver both wire formats for the same settlement.

### Reconnect and delivery behavior

Why: a reconnect can overlap the last event or exceed the server's retained history. The SDK resubscribes with the latest accepted timestamp, deduplicates the overlap, preserves unknown additive events, and reports replay state. Replay is best effort, not an unbounded gapless guarantee.

```python
pn.ws.on_replay(
    lambda notice: print(
        notice.phase, notice.since, notice.guaranteed, notice.warning
    )
)

sub.on_overflow(
    lambda overflow: print("local iterator queue evicted", overflow.dropped_events)
)
```

### Chainlink TWAP and short-form markets

The TWAP values are lookback windows, not update cadence: 5-minute markets use 30 seconds; 15-minute and 4-hour markets use 60 seconds.

```python
async def stream_short_markets(pn):
    prices = await (
        pn.ws.subscribe("chainlink")
        .feeds(["BTC/USD", "ETH/USD"])
        .twap_windows([30])
        .send()
    )
    print(prices.price_source, prices.twap_windows, prices.warnings)
    prices.on("price_feed", lambda event: print(event.feed, event.price))

    stream = pn.ws.short_form("5m", coins=["btc", "eth"])
    stream.on("rotation", lambda rotation: print([m.slug for m in rotation.markets]))
    stream.on("price_feed", lambda event: print(event.feed, event.price))
    stream.on("settlement", lambda event: print(event.market_slug, event.status))
```

A Chainlink selection is scoped to its WebSocket connection, so combine feeds and windows into one Chainlink subscription per connection. The resolved subscription exposes the server acknowledgement through `price_source`, `twap_windows`, and `warnings`. `short_form()` handles rotation safely with its own socket. At each market boundary it closes that socket, discovers the new slugs, reconnects, and subscribes to the exact settlement and TWAP filters again.

### V3 perps WebSocket

The V3 perps WebSocket streams tickers, best bid/offer, full books, trades, statistics, and klines. The managed client confirms which channels were accepted, reconnects and resubscribes, and emits an explicit gap notice because the perps stream cannot replay missed messages.

```python
import asyncio
from polynode import AsyncPolyNode, PerpsEvent, perps_channels

async def stream_perps():
    async with AsyncPolyNode(api_key="pn_live_...") as pn:
        perps = pn.perps
        hello = await perps.connect()
        ack = await perps.subscribe([
            perps_channels.tickers,
            perps_channels.book("BTC-USD"),
            perps_channels.trades("BTC-USD"),
        ])
        print(hello.max_subscriptions, ack.channels, ack.rejected)

        try:
            async for message in perps:
                if isinstance(message, PerpsEvent) and message.channel == "perps_tickers":
                    # Prices, quantities, funding, and equity values stay exact strings.
                    print(message.data["mark_price"], message.data["funding_rate"])
                elif message.type in ("lag_warning", "reconnect"):
                    print(message)
        finally:
            await perps.disconnect()

asyncio.run(stream_perps())
```

Use `perps_channels.bbo()`, `.book()`, `.trades()`, and `.klines(instrument, "1m" | "1h")` for scoped channels. Each `perps_book` event is a complete replacement snapshot; `perps.book("BTC-USD")` returns the latest complete book. Authentication (`4401`) and connection-cap (`4429`) closes are terminal. Queue eviction is observable through `on_overflow()`.

### WebSocket Streaming

```python
import asyncio
from polynode import AsyncPolyNode

async def main():
    async with AsyncPolyNode(api_key="pn_live_...") as pn:
        sub = await pn.ws.subscribe("settlements").min_size(1000).send()

        async for event in sub:
            print(event.event_type, event.market_title, event.taker_price)

asyncio.run(main())
```

### Orderbook

```python
import asyncio
from polynode import OrderbookEngine

async def main():
    engine = OrderbookEngine(api_key="pn_live_...", integrity=True)
    await engine.subscribe(["token_id_1", "token_id_2"])

    engine.on("ready", lambda: print(f"Tracking {engine.size} books"))
    engine.on("update", lambda u: print(f"{u.asset_id}: {engine.midpoint(u.asset_id)}"))
    engine.on("integrity_error", lambda error: print(error.token, error.code))

asyncio.run(main())
```

With PN1 integrity enabled, the engine validates sequence continuity and deterministic checksums, fails stale or invalid books closed by default, and requests a fresh anchor before making them readable again. Integrity mode requires explicit markets; wildcard subscriptions are unavailable. Set `allow_stale_reads=True` only when your application explicitly prefers availability over verified state.

### Trading

```python
import asyncio
from polynode.trading import PolyNodeTrader, TraderConfig, OrderParams, ExchangeVersion

async def main():
    # CLOB V2 (pUSD collateral) is the current production default.
    trader = PolyNodeTrader(TraderConfig(
        polynode_key="pn_live_...",
        # exchange_version=ExchangeVersion.V2,
        # builder_code=None,  # disables default public Polynode attribution
    ))
    status = await trader.ensure_ready("0xYourPrivateKey...")

    result = await trader.order(OrderParams(
        token_id="...",
        side="BUY",
        price=0.55,
        size=100,
        builder="0x<your_builder_code_bytes32>",  # V2 only; omit for V1
    ))
    print(result)

    trader.close()

asyncio.run(main())
```

For the V2 order flow — required approvals, EIP-712 struct, fee math, and common failure modes — see `polynode/trading/V2_ORDER_FLOW.md` in the installed package.

V2 fees are determined at match time and are not signed into an order, so V2 payloads omit `feeRateBps`, `nonce`, and `taker`. Explicit legacy V1 mode still signs `feeRateBps`; for that path the SDK fetches `/fee-rate` and fails closed if fee, tick-size, or neg-risk metadata is unavailable or malformed.

#### Optional user-owned execution

Set `execution_mode=ExecutionMode.USER_OWNED` when the signing wallet should trade with zero builder attribution and use its own gasless authorization. Builder mode remains the default and existing integrations are unchanged.

```python
import os
from polynode.trading import (
    ExecutionMode,
    PolyNodeTrader,
    TraderConfig,
    UserOwnedClobTransport,
)

trader = PolyNodeTrader(TraderConfig(
    polynode_key=os.environ["POLYNODE_API_KEY"],
    execution_mode=ExecutionMode.USER_OWNED,
    # Optional, explicit regional egress; direct is the default.
    # user_owned_clob_transport=UserOwnedClobTransport.PROXY,
))
ready = await trader.ensure_ready(user_wallet_signer)
print(ready.execution_mode, ready.user_relayer_authorized)
```

`user_wallet_signer` is a caller-controlled `RouterSigner`; the SDK asks it to sign scoped messages and never persists or transmits its private key. A private-key string is also accepted when the caller already manages it inside a trusted process. User-owned mode rejects builder credentials, nonzero builder codes, and credentials owned by another wallet. It supports EOA signers and EOA-controlled Safe or deposit wallets; legacy `POLY_PROXY` and Magic/DID signers are not supported in the first release. Normal CLOB authentication and Polymarket rate limits still apply.

Signed CLOB requests go directly to Polymarket by default. Platforms that need Polynode's regional egress can explicitly set `user_owned_clob_transport=UserOwnedClobTransport.PROXY`; this transport never activates automatically and never falls back between paths. Fee-authenticated orders are not available in user-owned mode, and any positive effective `fee_bps` is rejected before an order is signed or submitted.

For browser-wallet integrations, the typed `begin_user_relayer_authorization()` and `complete_user_relayer_authorization()` functions let a trusted backend request a validated message, send only that message to the user's browser for signing, and complete authorization for the same expected address. Keep the Polynode API key on the backend. Neither primitive writes the wallet signature or returned credential to local storage.

For long-running services, `await trader.authorize_user_owned_execution(signer)` provides the combined convenience flow. Its wallet-owned credential can be kept in a server-side secret manager and supplied later as `TraderConfig.user_relayer_credentials`. Never log it, commit it, or store it in a browser.

`ensure_ready()` is the one-call onboarding path for user-owned Safe and deposit-wallet accounts: it deploys the selected wallet when needed, applies the base trading approvals, verifies both results, and only then reports the account ready. New deposit-wallet integrations must resolve the current address asynchronously:

```python
from polynode.trading import resolve_deposit_wallet_address

funder = await resolve_deposit_wallet_address("0xYourEOA...")
```

The synchronous `derive_deposit_wallet_address()` helper is deprecated and derives only the legacy UUPS address; do not use it to bind a new or live deposit-wallet account. For the same reason, synchronous `link_credentials()` and `import_wallet()` reject deposit wallets in user-owned mode; use `await link_wallet()` or `await ensure_ready()` so the current address is resolved before anything is stored.

`trader.split(SplitParams(...))` and `trader.merge(MergeParams(...))` preserve the synchronous build-only API and return a `TransactionRequest` without signing or submitting anything. In user-owned mode, `await trader.execute_split(SplitParams(...))` and `await trader.execute_merge(MergeParams(...))` execute the corresponding gasless operation for EOA-controlled Safe and deposit-wallet accounts. Supply either an explicit `neg_risk` boolean or a `token_id` for fail-closed execution routing; V2 uses the appropriate collateral adapter automatically. Each executed operation batches only the additional permission it needs; split allowance is limited to the exact split amount.

`await trader.wrap_to_polyusd(...)` and `await trader.unwrap_from_polyusd(...)` also use the account's wallet-specific gasless path for Safe and deposit-wallet accounts. Caller-controlled browser or HSM signers remain supported; these smart-wallet operations do not require handing a private key to the SDK.

## Documentation

Full docs at [docs.polynode.dev](https://docs.polynode.dev)
