Skip to main content
The UpsideMAX Python SDK is the official client for the UpsideMAX perpetuals exchange. It wraps the REST read API (POST /info), the signed write API (POST /exchange), and the realtime WebSocket streams behind two typed client classes. Authentication is EIP-712 wallet signing (secp256k1) — there are no API keys to manage.

upsidemax/upside-python-sdk

Source, examples, and issue tracker on GitHub. MIT licensed.
  • EIP-712 request signing across the Agent and Typed paths — handled for you.
  • Synchronous REST with timeouts and safe retries, plus a threaded WebSocket client that auto-reconnects.
  • Checked inputs: every field is validated before signing, so a malformed request fails locally instead of being silently dropped.
  • Raw-dict responses, typed inputs, and full type hints (ships py.typed).
  • Agent (API-wallet) delegation, TP/SL, leverage / margin, collateral, and portfolio-margin actions.

Installation

Requires Python 3.9+. Runtime dependencies: requests, websocket-client, eth-account, eth-utils. This documentation describes version 0.3.0.

Quick start

An order on an account without usable margin is rejected — check marginAvailableForOrder in user_account first. The Quickstart walks through the same flow with the checks built in.

The two clients

Info — reads & streams

Market data, account and order queries, history, and realtime WebSocket subscriptions. No wallet or signing required.

Exchange — signed writes

Orders, cancels, margin and leverage, TP/SL, collateral, portfolio margin, and agent delegation — every call signed with your wallet key.

Key behaviors

Prices and sizes are raw integer strings. The wire protocol is integer-only: convert a display price with the contract’s priceScale and a display size with its qtyScale, both read from configs — never hardcode a scale. Scales differ per contract and are often 0.
  • Orders and cancels have two receipt shapes. order, cancel, cancel_by_cloid, cancel_all, and modify take a sync flag. With sync=True the call returns once the action has executed, with one statuses[] entry per submitted item. Without it the server answers 202 with {"status": "accepted", "response": {"type": "accepted", "data": {"count": n}}} and the per-order outcome — including any rejection — arrives only on the orderUpdates WebSocket channel. See Receipts.
  • Every other action is synchronous. Margin, leverage, TP/SL, collateral, and agent actions always return their result in the HTTP response.
  • HTTP 200 does not always mean success. Business rejections come back as HTTP 200 — a per-item error inside statuses[], or a non-zero errorCode inside response.data. Always inspect the response.

Timeouts and retries

Every request has a timeout — 5 seconds to connect and 10 seconds to read by default; pass timeout= (seconds, or a (connect, read) tuple) to either client. Failed requests are retried with exponential backoff (max_retries=3, backoff=0.5 seconds; max_retries=0 disables retrying) only where resending is safe: A signed write cannot be resent once it may have reached the server: the resend carries the same nonce and is rejected as a duplicate even if the first attempt executed. So instead of guessing, the SDK raises OutcomeUnknownError.

Error handling

Every SDK error derives from UpsideError: Arguments are checked before anything is signed: a malformed id, size, price, time-in-force, or type raises Python’s own ValueError / TypeError, and nothing is sent.
Reconciling an unknown outcome. Look the order up by its client order id in both places an order can be: user_orders for orders still resting and order_history / user_fills for orders that already filled or were canceled — or watch the orderUpdates channel. orders_by_cloids alone is not enough: it only sees live orders, so an order that filled immediately looks missing. History is indexed shortly after execution; re-check for a few seconds before concluding the order did not land.

Environments

Without a base_url the SDK uses Mainnet; pass constants.DEVNET_API_URL for Devnet:
Exchange signs with the chain ID of the environment its base_url points at. If you reach Mainnet through a different address, pass chain_id=9788 explicitly — a signature over the wrong chain ID is rejected with 401 SIGNATURE_INVALID. Contract IDs, coin IDs, scales, and tick / step sizes are server-assigned and differ per environment — always read them from Info.configs() rather than hardcoding.

Signing

Every /exchange write is authorized by an EIP-712 signature over the fixed domain (Exchange / version 1 / the environment’s chain ID / zero verifying contract). The SDK selects the correct path automatically — the Typed path for registerAccount, approveAgent, revokeAgent, lockCollateral, unlockCollateral, and transferBetweenDeployers, and the Agent path for everything else. Nonces are strictly increasing millisecond timestamps managed per Exchange instance; share one NonceManager between clients that sign with the same key. Browser-extension wallets sign typed structs with their active chain id rather than the environment’s. To match such a signature, pass Exchange(..., signature_chain_id=...); with a raw private key, leave it unset. See Authentication for the full scheme.