Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Polyester Python SDK

Official Python SDK for Polyester APIs, built for trading bots, backend jobs, research notebooks, and automation.

Status: Alpha (0.1.0a34). Proprietary license (not open source). API-key only; no browser login or JWT flows.

Requires Python 3.11+.

Supported surface

Capability Supported
Public market data (spot config, trades, candles) Yes
Order book snapshot + realtime Yes
Market overview (list + subscribe) Yes
Order book heatmap Yes
API-key (Ed25519 signature) auth Yes
Wallet / browser login No
Session MFA enrollment and challenges No
Profile (identity subscribe) Yes
API keys (list/get/subscribe/local keypair generation) Yes
Subaccounts (list/get/members/invites/activity/subscribe) Yes
Address book (list/view/subscribe) Yes
Policies (realtime subscribe) Yes
Guard signer Yes
Balances, holds, equity history Yes
Orders (create, cancel, modify, batch, cancel-all) Yes
User trades Yes
Triggers Yes
Internal transfers Yes
Transfer history Yes
Deposit addresses Yes
Trading / funding withdraws Yes
Zipper deposit-withdraw config Yes
Chain analytics Yes
Lifecycle flows Yes
Polychart / layout / whiteboard Yes
Realtime account and market streams Yes
Reference catalogs + wait-for-ready Yes
Qty / price decimal + scaled-int inputs Yes
Social verification Yes
Account resolve / lookup No

Rows marked No are intentional for API-key SDKs (use the TypeScript browser client for wallet login and session MFA).

Full cross-language comparison: SDK capability matrix.

Rows marked Yes mean that an SDK wrapper exists; deployment authorization still applies. In particular, whiteboard/social-verification and some layout/polychart routes may require a JWT session or may not be mounted. Private streams require an Account ID and the corresponding API-key permission. A successful subscribe call means the token exchange and realtime handshake completed. Treat a structured permission denial as non-transient and update the API-key policy before retrying.

Install

PyPI: https://pypi.org/project/polyester-sdk/

pip install "polyester-sdk==0.1.0a34"

Realtime (Centrifugo) and on-chain Funding helpers are included by default.

For development from a git checkout:

git clone https://github.com/Fabric-Labs/polyester-sdk-python.git
cd polyester-sdk-python
python3.11 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

Quickstart

Create an API key in the Polyester app (API in the sidebar). Copy the key id and private key when shown. The private key is only displayed once. Open the key's Permissions, enable Spot trading, select the markets it may trade, and set a maximum order size appropriate for the strategy. For a subaccount-scoped key, attach an API-key policy that grants ledger reads for balances and private balance streams, plus Spot trading for order mutations. This is separate from the subaccount policy: authorization is the intersection of both policies, so configuring only the subaccount policy is insufficient.

import asyncio

from polyester import AsyncPolyester

async def main() -> None:
    async with AsyncPolyester(
        api_key_id="ak_...",           # from API key creation
        api_private_key="...",       # 64-char hex secret from API key creation
        default_account_id="...",    # Profile → Account ID (see below)
    ) as client:
        overview = await client.market_overview.list(limit=5)
        for market in overview.markets:
            ticks = market.last_price.ticks if market.last_price is not None else None
            print(market.symbol, ticks)

        open_orders = await client.orders.list_open()
        print(f"{len(open_orders.orders)} open orders")

asyncio.run(main())

Credentials

Value Where to find it Constructor parameter
API key id API → create or view key api_key_id
API private key Shown once when the key is created api_private_key
Account ID ProfileAccount ID (e.g. RLxqJGUDg92) default_account_id

Pass all credentials as constructor parameters. The SDK does not read environment variables unless you pass them in yourself (or use from_env() in scripts; see below).

api_private_key accepts the 64-character hex Ed25519 secret from key creation, or raw 32-byte key material.

default_account_id is the Account ID string from your Profile page. Use the value exactly as shown in the app. Do not use an internal numeric id. An account username is optional; Account-ID-based authentication and private channel scoping are valid without one.

default_account_id is optional for public market-data calls. It is required for account-scoped operations such as private realtime channels, bucket transfers, and some ledger writes.

Automatic request signing gives concurrent identical calls distinct authentication tuples. Timestamps can lead the local clock by at most five seconds. The async client applies bounded, cooperative backpressure without blocking the event loop. The low-level synchronous polyester.auth.sign_request helper instead raises PolyesterRateLimitError immediately when capacity is exhausted; honor retry_after. Neither path reuses a signature or drifts outside the API's 10-second freshness window.

Authentication patterns

Recommended: explicit parameters

from polyester import AsyncPolyester

client = AsyncPolyester(
    api_key_id="ak_...",
    api_private_key="...",
    default_account_id="RLxqJGUDg92",
)

If your deployment stores secrets in environment variables, read them in your application and pass them to the constructor:

import os

from polyester import AsyncPolyester

client = AsyncPolyester(
    api_key_id=os.environ["POLYESTER_API_KEY_ID"],
    api_private_key=os.environ["POLYESTER_API_PRIVATE_KEY"],
    default_account_id=os.environ["POLYESTER_ACCOUNT_ID"],
)

The plain AsyncPolyester(...) / Polyester(...) constructors never implicitly read os.environ.

Scripts and local tests only: AsyncPolyester.from_env() and Polyester.from_env() load POLYESTER_API_KEY_ID, POLYESTER_API_PRIVATE_KEY, and POLYESTER_ACCOUNT_ID from the process environment. This is a convenience helper, not the primary integration pattern.

Create and cancel orders

from polyester import AsyncPolyester
from polyester.models import ClientOrderId

async with AsyncPolyester(
    api_key_id="ak_...",
    api_private_key="...",
    default_account_id="RLxqJGUDg92",
    default_sub_account_id="",  # main account; omit subaccount scoping
) as client:
    result = await client.orders.create(
        symbol="BNB-USDT",
        side="buy",
        order_type="limit",
        tif="gtc",
        qty="0.01",
        price="100",
        post_only=True,
        client_order_id="my-bot-001",
    )
    print(result.status, result.order_id)

    await client.orders.cancel(key=ClientOrderId("my-bot-001"))

client_order_id is optional (matches the API). Omit it for one-shot creates. Set a stable non-empty value when you may retry after an ambiguous transport/server failure, and reuse that same id on retry / reconciliation - without it you cannot safely tell whether the first attempt admitted the order. Client order ids accept 1 to 36 ASCII letters, digits, ., _, :, /, and -. Batch create, cancel, and replace accept at most 20 items. Treat a cancel response as an admission acknowledgement and reconcile with list_open before releasing local state.

Create sizing is explicit: set exactly one of base qty or max_quote_debit (a hard all-in quote-debit budget). Decimal/str quote budgets use the pair's catalog quote_quantity_scale. Typed budgets must use QuantityDomain.ORDER_QUOTE and embed that scale; construct with Quantity.from_quote_decimal_str / from_quote_decimal / from_quote_scaled and validate against client.catalogs.quote_quantity_scale_for_symbol.

quote_scale = client.catalogs.quote_quantity_scale_for_symbol("BNB-USDT")
assert quote_scale is not None  # await client.wait_for_catalogs() first
result = await client.orders.create(
    symbol="BNB-USDT",
    side="buy",
    order_type="market",
    max_quote_debit=Quantity.from_quote_decimal_str(
        "25.00", quote_scale, symbol="BNB-USDT"
    ),
)

Use orders.preview_order(...) to check current order admissibility and, when available, resolved base quantity plus protected price bound before submitting. Preview accepts the same public create kwargs and encodes them as an OrderIntent (same wire contract as create). It does not return fee or quote-debit estimates. protected_price_bound is a protective execution boundary, not an expected fill price. Rejections carry stable labels such as BAD_QTY plus field violations; evaluated_at_ms records when admission was evaluated. Preview is not deployed on every API host, so handle an unimplemented/not-found response and do not make Preview a prerequisite for order submission.

Market orders are IOC and enforce a slippage-derived execution boundary. See Market Order Price Protection before overriding market slippage.

Scaled transfer/withdraw AssetAmount inputs must carry their source scale. AssetAmount.from_scaled(..., scale=None) is accepted for composition only and fails closed on encoding unless the request's amount_scale / quantity_scale is explicit.

Use decimal strings or Decimal for human-facing qty / price inputs. Do not pass floats. ticks on Price means Polyester protocol price units (fixed 1e6), not market tick-size alignment (server validates tick size).

For bots (scaled integers)

Stay in integer space; no string round-trip:

from polyester import Price, Quantity

result = await client.orders.create(
    symbol="BNB-USDT",
    side="buy",
    order_type="limit",
    tif="gtc",
    qty=Quantity.from_scaled(1_000_000, scale=8),  # already wire units
    price=Price.from_ticks(100_000_000),            # 100.000000 at 1e6
    post_only=True,
)
# Reads expose the same types: order.price.ticks, order.orig_qty.scaled

Compatible values from fills/books can be passed back into writes when the instrument/domain matches.

orders.batch_replace(...) returns an admission receipt, not final execution. The predecessor ID can be stale after admission; reconcile the successor IDs and per-item phases by polling get_batch_replace_status. Use is_batch_replace_settled(status) only to determine that all items have moved to working, rejected, or terminal, not as a finality signal. Retry an ambiguous batch request with the same request_id.

Your API key needs a policy that allows trading. Spot orders spend trading balance (see below).

Triggers

triggers.list(status=...) filters by lifecycle status. Valid values:

created, armed, running, completed, cancelled, failed, paused

Unknown values raise ValueError (they do not silently return an empty list). Response status uses the same labels (British spelling cancelled).

orders.get(..., include_attached_risk=True) returns policy data on order.attached_risk (take-profit / stop-loss / trailing-stop). Order also exposes post_only.

Balances: funding vs trading

Ledger balances have separate funding and trading buckets per asset.

  • An external deposit can stop in funding or continue to trading, depending on its configured route.
  • Spot orders spend trading balance.
  • Move funds funding → trading in the Polyester UI (Funding → Unified Trading) or on-chain via the funding wallet.

SDK notes:

  • Funding → trading: on-chain TradingGateway.deposit (not an API-key RPC). Either encode calldata or submit a UserOp: pass an owner EOA private key to PolyesterSmartAccount (SDK derives the Polyester Safe; no UI-exported owner key).
  • Funding → external: on-chain FundingAccount.withdrawToChain (same polyester.chain).
  • Funding → another user's funding wallet: on-chain FundingAccount.UAssetTransfer via wallet/smart-account signing in the Polyester app (not an API-key RPC).
  • Trading → funding: prepare and persist an exact API-key signature with client.trading_withdraws.prepare_api_key_to_funding(...), then call submit_prepared(...). One-call create_api_key_to_funding(...) is also available.
  • Trading → trading (another account): client.internal_transfers.create(...).
from polyester.chain import (
    POLYESTER_TESTNET_ENVIRONMENT,
    PolyesterSmartAccount,
    encode_trading_gateway_deposit,
    encode_funding_withdraw_to_chain,
    encode_withdraw_destination,
    quote_zipper_fee,
)

account = PolyesterSmartAccount(owner_private_key="0x…")  # caller-supplied EOA

# Funding → Trading
deposit = encode_trading_gateway_deposit(
    trading_gateway=POLYESTER_TESTNET_ENVIRONMENT.contracts.trading_gateway_address,
    u_asset_id="0x…",
    quantity_scaled=10**18,  # 1 USDT at 18 decimals
)
account.send_calls([deposit])

# Funding → external (quote Zipper fee first)
fee = quote_zipper_fee(
    chain_id=6,  # BSC testnet Zipper id
    z_token="0x…",
    zipper_endpoint=POLYESTER_TESTNET_ENVIRONMENT.contracts.zipper_endpoint_address,
)
withdraw = encode_funding_withdraw_to_chain(
    funding_account=POLYESTER_TESTNET_ENVIRONMENT.contracts.funding_account_address,
    chain_id=6,
    z_token="0x…",
    withdraw_destination=encode_withdraw_destination(address="0x…", is_case_sensitive=False),
    z_amount=5 * 10**18,
    max_fee=fee.fee + fee.fee // 10,
)
account.send_calls([withdraw])

Whitelist toggles / destination entries / GuardRegistry signer setup are also encoded under polyester.chain (encode_add_allowed_external_destinations, …).

Pass default_account_id (your Profile Account ID) on the client for bucket transfers and other account-scoped ledger operations.

Format u128 wire amounts with the public helper (18-decimal scale):

from polyester import format_ledger_u128

print(format_ledger_u128(balance.funding), format_ledger_u128(balance.trading))

Public market data

Public endpoints do not require an API key. Authenticated endpoints use the credentials above.

candles = await client.market_data.get_candles(symbol="BTC-USDT", timeframe="1m", limit=50)
current = await client.market_data.get_current_candle(symbol="BTC-USDT", timeframe="1m")
if current is not None:
    print(current.close, current.is_closed)
trades = await client.market_data.get_trades(symbol="BTC-USDT", limit=20)

subscription = await client.market_data.subscribe_trades(symbol="BNB-USDT")
subscription.set_on_error(lambda error: print(f"realtime interruption: {error}"))
async with subscription:
    async for trade in subscription:
        print(trade.price.ticks if trade.price else None, trade.qty.scaled if trade.qty else None)
        break

get_candles() returns row-oriented candles newest-first (an incomplete open candle, when requested, is prepended). get_candles_columns() normalizes its column response oldest-first. Sort explicitly by ts_sec before feeding a chronological indicator.

Merged market overview stream (snapshot + live updates). The create call waits for the WebSocket handshake and initial snapshot:

sub = await client.market_overview.create_subscription(
    on_error=lambda error: print(f"managed overview failed: {error}")
)
async with sub:
    async for markets in sub:
        print(len(markets), "rows")
        break

Realtime delivery contract

  • Realtime is binary-only. The client negotiates the centrifuge-protobuf WebSocket subprotocol and consumes protobuf publications from :proto channels. ConnectRPC's optional JSON wire mode does not apply to realtime. Inbound WebSocket messages are capped at 4 MiB.
  • Subscription queues are bounded. If the consumer falls behind, the SDK raises PolyesterRealtimeOverflowError and faults the subscription; it does not silently drop updates.
  • Reconnects use capped exponential backoff with per-subscription jitter. set_on_error(...) observes background feed interruptions; async iteration still raises terminal failures.
  • Orderbook sequence gaps trigger a REST snapshot refresh. Use on_sequence_gap / on_reconnect / on_snapshot_refresh on orderbook.create_subscription(...) for recovery observability.
  • Managed snapshot-then-stream subscriptions disable transport auto-reconnect so they can rebuild REST state between reconnect attempts. Buffered publications survive a failed snapshot retry and are merged exactly once after recovery; cancellation closes the replacement socket. Managed create methods await the handshake and initial snapshot before returning.
  • Private order stream quantities are raw scaled values. Their Quantity.scale may be None; resolve the hydrated catalog scale with client.catalogs.base_quantity_scale_for_symbol_id(order.symbol_id) (or by symbol), then call quantity.format(scale). Never guess or inject scale 8.

Sync client

The sync Polyester client exposes the same service tree and constructor parameters:

from polyester import Polyester

with Polyester(
    api_key_id="ak_...",
    api_private_key="...",
    default_account_id="RLxqJGUDg92",
) as client:
    balances = client.balances.list()

Realtime subscriptions are available via subscribe_sync helpers on the sync client. Private API-key policy snapshots use client.policies.subscribe_api_policies_sync(...) (async: await client.policies.subscribe_api_policies(...)).

Testing (contributors)

CI (no network):

python -m pytest tests/unit tests/hardening -q

Live selection (A7): public smoke vs credentialed suites are selectable via markers.

# Public smoke (read-only smoke; no mutation/funded)
python -m pytest -m "public_smoke" -q

# Credentialed live integration (requires API-key env)
python -m pytest -m "credentialed and not mutation and not funded" -q

# Local L2 hardening only (no network)
python -m pytest tests/hardening -q

Live devnet tests use a local .env file in the test harness only. Fixtures load values from env and pass them as explicit constructor parameters; the same pattern application code should use.

cp .env.example .env
# fill in POLYESTER_API_KEY_ID, POLYESTER_API_PRIVATE_KEY, POLYESTER_ACCOUNT_ID

pip install -e ".[dev]"
python -m pytest tests/unit tests/hardening -q
./scripts/test_all.sh   # optional: unit + live tiers
./scripts/smoke_realtime.sh   # realtime unit + live heartbeat before release

Use python -m pytest (not bare pytest) so tests run in the same venv as pip install. Set POLYESTER_TEST_MUTATION=1 for state-changing tests. Funded mutations require both POLYESTER_TEST_MUTATION=1 and POLYESTER_TEST_FUNDED=1. For a release-certification run, set POLYESTER_TEST_STRICT_LIVE=1; any skipped test then fails instead of making an incomplete live run appear green. Missing or malformed credentials must fail under strict live (not soft-skip as green). The session printer scopes executed/skipped/failed counts to @pytest.mark.integration tests and enforces POLYESTER_TEST_MIN_EXECUTED (default 5) when credentials are present.

Legacy stress tests that use non-dry-run cancel_all require POLYESTER_TEST_ACCOUNT_WIDE_CLEANUP=1. Set it only for a dedicated test account; those tests may cancel every open order in their selected symbol.

CI requires every public Connect RPC in gen to be wrapped or listed in sdk-coverage.toml. Contributors: python scripts/check_sdk_coverage.py. CI refreshes sdk-capabilities.json and the README capability table on the same branch when they drift (same-repo PRs / pushes to main).

Pre-release checklist (realtime changes):

cd polyester-sdk-python
python -m venv /tmp/polyester-pypi-test && source /tmp/polyester-pypi-test/bin/activate
pip install -e ".[dev]"
python -m pytest tests/unit -q
./scripts/smoke_realtime.sh

cd ../polyester-examples-python
pip install -e "../polyester-sdk-python"
python -m pytest -q
python3 examples/04_public_realtime_trades.py
python3 examples/05_public_orderbook_stream.py

Then bump the version, update CHANGELOG.md, build, and publish to PyPI. Install from the new wheel (not editable) and rerun smoke_realtime.sh once to confirm the published artifact.

Changelog

See CHANGELOG.md.

Transport

Connect RPC over HTTP via generated clients in src/polyester/gen/. Wire format defaults to binary protobuf; pass wire_format="json" for debugging.

Some RPCs may return HTTP 404 on devnet. The SDK raises PolyesterRouteNotFoundError with a clearer message than [unimplemented]: Not Found.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

polyester_sdk-0.1.0a34.tar.gz (351.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

polyester_sdk-0.1.0a34-py3-none-any.whl (457.3 kB view details)

Uploaded Python 3

File details

Details for the file polyester_sdk-0.1.0a34.tar.gz.

File metadata

  • Download URL: polyester_sdk-0.1.0a34.tar.gz
  • Upload date:
  • Size: 351.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.9

File hashes

Hashes for polyester_sdk-0.1.0a34.tar.gz
Algorithm Hash digest
SHA256 c629409b628f3800ba9be709b9de41cd2fc4e3439816da7155b8d55c74bc8785
MD5 6f22cf430e81fd34e3ec999eb8e82cc1
BLAKE2b-256 f4de108f2e0df288097b13373b82c4dd636b1207b1af36d1d1d6efc1f8aa07fb

See more details on using hashes here.

File details

Details for the file polyester_sdk-0.1.0a34-py3-none-any.whl.

File metadata

File hashes

Hashes for polyester_sdk-0.1.0a34-py3-none-any.whl
Algorithm Hash digest
SHA256 9ed07f45e3a9bd7db203427a2562d2b1cfdc278d926e02a0a1cdfc3b023607fb
MD5 d1a7e4cf9bf03742f2dd8ff993e84606
BLAKE2b-256 6b368c31558a507ea9f521fd765322ac230607fd6dde96984deda1e0c85645cb

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0a34 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page