# Agent Wallets Source: https://docs.upsidemax.xyz/agents/api-wallets Authorize a dedicated API wallet to trade for a master account while keeping the master private key offline, with expiry, monitoring, rotation, and revocation controls. An **agent wallet** is a separate signing wallet authorized to submit selected trading actions on behalf of a master account. It lets an automated strategy place and manage orders without keeping the master account's private key online. Agent wallets are designed for delegated execution, not custody. Keep the master key offline, give each strategy or environment its own agent identity, and place deterministic validation between any AI-generated intent and the signer. An approved agent can submit real state-changing requests. Treat its private key as a live credential, limit its lifetime and operational scope in your application, and maintain an independent revocation path. ## Capabilities and limits | Property | Behavior | | -------------------- | --------------------------------------------------------------------------------------------------------------- | | Supported execution | Agents can submit orders, cancellations, modifications, and TP/SL requests on behalf of the master account | | Master-key isolation | The master key is used to authorize or revoke an agent, then can remain offline during normal automated trading | | Anonymous slots | Up to 1 anonymous agent per master account | | Named slots | Up to 3 named agents per master account | | Expiry | `validUntil` is a Unix millisecond timestamp; `0` means no automatic expiry | | Renewal | Re-approving an already authorized address refreshes its expiry without consuming another slot | | Name replacement | Approving a named agent with an existing name replaces the previous entry for that name | | Administration | Only the master account can approve or revoke agents | | Inventory query | `userAgents` returns both active and expired agent records for auditing and cleanup | ## Recommended lifecycle Generate a new wallet for one strategy, service, or environment. Store its private key in a KMS, HSM, or secrets manager and expose only a narrow signing interface to the trading service. ```python theme={null} from eth_account import Account agent = Account.create() agent_address = agent.address # Store agent.key in a secure secret store. # Never print it, send it to an LLM, or commit it to source control. print(agent_address) ``` Submit [`approveAgent`](/exchange/approve-agent), signed by the master account. Prefer a descriptive `agentName` and a finite `validUntil` for long-running automation. ```json theme={null} { "action": { "type": "approveAgent", "agentAddress": "0xabc0000000000000000000000000000000000001", "agentName": "market-maker-prod", "validUntil": 1893456000000 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1782259200123 } ``` `agentName` and `validUntil` are business-optional but **must appear explicitly in the signed JSON** — send `""` for an anonymous agent and `0` for no expiry. Omitting either returns `401 SIGNATURE_INVALID`. See [`approveAgent`](/exchange/approve-agent) for details. Query [`userAgents`](/info/user-agents) with the master account ID and confirm the returned address, name, expiry, and slot type before enabling the signer. ```json theme={null} { "type": "userAgents", "accountId": "5" } ``` The strategy or model should produce an unsigned intent. A deterministic policy layer validates the action, applies risk controls, assigns a unique nonce, and sends the canonical payload to the agent signer. Track open orders and fills through the [Info API](/info/overview) and private [WebSocket channels](/websocket/overview); poll `userAccount` for positions and balances, which are not yet streamed over WebSocket. Stop submitting new writes when authorization is near expiry, state is stale, or reconciliation falls behind. Approve the replacement wallet, verify it, switch the signer, and then call [`revokeAgent`](/exchange/revoke-agent) for the old address. Maintain a separate operator-controlled kill switch that can revoke the active agent without relying on the strategy process. ## Agent execution envelope Every state-changing operation uses `POST /exchange` and follows the standard signing process documented in [Authentication](/guide/authentication). The nonce must be unique for the **agent signing address**, not merely for the master account. An agent signs with its **own** private key — no extra routing fields. The server recovers the agent address from the signature, maps it to its master account, and applies the action under the master. You do **not** send `vaultAddress` or any master identifier for agent trading; routing comes only from the recovered signer, and trade actions ignore `vaultAddress` (a mistyped value is simply dropped). ```json theme={null} { "action": { "type": "order", "orders": [ { "a": 1, "b": true, "p": "50", "s": "1", "r": false, "t": { "limit": { "tif": "Gtc" } } } ], "grouping": "na" }, "signature": { "r": "0x...", "s": "0x...", "v": 27 }, "nonce": 1782259200456 } ``` Build and sign the exact action shape defined by the relevant Exchange API page. Do not let a model invent action fields, infer numeric types, or sign arbitrary JSON. ## Separate reasoning from signing A secure agent stack should have distinct responsibilities: Reads documentation and current state, then proposes a structured action with a rationale. It has no access to private keys. Enforces allowed actions, contract allowlists, precision, maximum size, leverage, reduce-only constraints, price bands, and account-level exposure limits. Accepts only validated canonical actions, assigns or verifies a unique nonce, signs with the agent key, and returns the signature envelope. Confirms orders, cancellations, fills, positions, and authorization state from server responses and WebSocket events. The model should never call the signer with free-form text. Define a strict proposal schema such as: ```json theme={null} { "intent": "place_order", "contractId": 1, "side": "buy", "price": "50", "size": "1", "timeInForce": "Gtc", "reduceOnly": false, "reason": "Spread and inventory conditions satisfy strategy rules" } ``` Your application then maps the validated proposal to the compact API action fields. ## Expiry and rotation policy For long-lived automation, avoid permanent authorization unless there is a strong operational reason. A practical policy is: 1. Use a named agent for each environment and strategy, such as `maker-prod` or `risk-staging`. 2. Set a finite `validUntil` and alert well before expiry. 3. Create and approve a replacement wallet before the current authorization expires. 4. Verify the replacement through `userAgents`. 5. Switch the signer and run a low-risk health check. 6. Revoke the previous address and confirm it is no longer used by any process. Because `userAgents` includes expired entries, your monitoring should distinguish `validUntil = 0`, future timestamps, and expired timestamps rather than assuming every returned record is active. ## Kill switch A kill switch should operate independently of the model and trading strategy: * Stop the order-generation loop. * Disable or isolate the signing service. * Cancel open orders where operationally appropriate. * Revoke the agent with `revokeAgent`, signed by the master account. * Confirm authorization state through `userAgents`. * Reconcile late responses and in-flight requests before declaring the incident closed. Revocation takes effect when it is recorded by the exchange. A request already in flight may still be accepted if it arrives before the revocation is processed, so continue reconciliation after triggering the kill switch. ## Common failure modes | Symptom | Likely cause | Response | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `SIGNATURE_INVALID` | Wrong private key, wrong signing path, missing required typed fields (`agentName`/`validUntil` on `approveAgent`), incorrect canonical JSON (Agent path), double hashing, or invalid `v` | Rebuild the signed digest exactly as documented and compare the recovered address | | Duplicate or rejected nonce | The signing address reused a nonce or concurrent workers raced | Maintain one monotonic nonce allocator per agent address | | Agent not authorized | Authorization expired, was revoked, or targets a different master account | Query `userAgents`, verify the target account, and re-approve only after operator review | | Named quota full | Three distinct named slots are already occupied | Revoke or intentionally replace an existing named agent | | Address belongs to another master | The wallet is already registered under a different master account | Generate a fresh dedicated wallet rather than sharing one agent address | | State drift after submission | REST or WebSocket reconciliation is delayed or disconnected | Pause new writes, refresh account and order state, then resume only after consistency checks pass | ## Related API pages Authorize an agent address, name it, and optionally set an expiry. Remove an agent authorization and free its quota slot. Audit active and expired agent records for a master account. Sign `POST /exchange` requests with EIP-712 over secp256k1 — Agent and typed paths. Generate unique replay-protection values for each signing address. Review the shared `POST /exchange` envelope. # LLMs.txt Source: https://docs.upsidemax.xyz/agents/llms-txt Use UpsideMAX machine-readable Markdown documentation with coding assistants, custom agents, and retrieval pipelines. UpsideMAX publishes machine-readable documentation files that coding assistants and custom agents can consume without parsing the rendered website. Use them to discover the API surface, retrieve implementation details, generate integration code, and ground agent responses in the current documentation. The machine-readable files describe the API; they do not grant access or bypass signing. Read requests still use `POST /info`, while state-changing requests require a valid signed envelope sent to `POST /exchange`. ## Available files A compact index of documentation pages with titles, descriptions, and links. Start here when an agent needs to discover the right source before loading more context. The full published documentation rendered as one Markdown document. Use it for broad code-generation tasks, offline indexing, or retrieval pipelines. ```text theme={null} https://docs.upsidemax.xyz/llms.txt https://docs.upsidemax.xyz/llms-full.txt ``` ## Which file should you use? | Task | Recommended source | Why | | -------------------------------------------- | -------------------------------- | ------------------------------------------------------------- | | Find the page for an endpoint or concept | `llms.txt` | Fast, compact discovery with minimal context usage | | Answer a focused integration question | `llms.txt`, then the linked page | Keeps the context narrow and easier to verify | | Generate a client spanning several API areas | `llms-full.txt` | Provides read, write, signing, and WebSocket context together | | Build a documentation search or RAG index | `llms-full.txt` | One normalized Markdown corpus is easy to chunk and embed | | Keep a long-running agent context fresh | `llms.txt` plus selected pages | Avoids repeatedly loading the full corpus | Prefer a two-stage retrieval flow: use `llms.txt` to select the relevant pages, then load only those pages. Reserve `llms-full.txt` for tasks that genuinely require cross-document context. ## Recommended retrieval workflow Load `llms.txt` and identify the pages related to the user's task, such as authentication, order placement, account state, or WebSocket subscriptions. Fetch the selected documentation pages and keep their URLs or paths alongside the extracted text so the agent can cite its source. Ask the model for typed data or code rather than an unstructured instruction. For trading actions, require a JSON proposal that can be validated before signing. Apply strict schemas, precision rules, risk limits, allowed-action lists, and nonce management in deterministic application code. Re-fetch the index at application startup or on a controlled schedule, then update only the changed pages in your local cache or retrieval index. ## Use with coding assistants ### Claude Code or similar CLI agents Fetch the full document when the task spans multiple sections: ```text theme={null} /fetch https://docs.upsidemax.xyz/llms-full.txt ``` For a focused task, fetch the index first and then the specific page it points to. ### Cursor, Windsurf, and IDE assistants Add one of the URLs as a documentation source in the project's context settings. Use `llms.txt` for everyday lookup and `llms-full.txt` for repository-wide client generation or refactoring. A useful project rule is: ```text theme={null} Before implementing or changing UpsideMAX integration code, consult the machine-readable documentation. Do not infer request fields or response shapes from naming alone. Link each non-obvious implementation choice to its source page. ``` ### Custom agents Fetch the index with `curl`: ```bash theme={null} curl -fsSL https://docs.upsidemax.xyz/llms.txt ``` Download the full corpus: ```bash theme={null} curl -fsSL https://docs.upsidemax.xyz/llms-full.txt \ -o upside-llms-full.txt ``` A minimal Python loader: ```python theme={null} import requests docs = requests.get("https://docs.upsidemax.xyz/llms.txt", timeout=20).text print(docs[:500]) ``` ## Prompt recipes ### Endpoint discovery ```text theme={null} Using the UpsideMAX documentation index, identify the smallest set of pages needed to implement this task. Return the page URLs first, then summarize the required endpoint, request fields, response fields, signing requirements, and failure cases. Do not write code until the sources are identified. ``` ### Code generation ```text theme={null} Generate a deployment-ready implementation from the cited UpsideMAX pages. Use exact field names and preserve documented numeric types. Separate payload construction, validation, signing, transport, and response reconciliation. Never include a real private key or secret in examples. ``` ### Payload review ```text theme={null} Compare this proposed request against the UpsideMAX documentation. Report: 1. unknown or missing fields, 2. type or precision mismatches, 3. signature and nonce requirements, 4. unsafe assumptions, 5. the corrected payload. Cite the documentation page supporting each correction. ``` ## Building a retrieval index When indexing `llms-full.txt` for retrieval-augmented generation: * Split primarily at page titles and section headings rather than arbitrary character counts. * Store each chunk with its page URL, heading path, and document version or fetch time. * Keep code blocks with the paragraph that explains them. * Give authentication, nonce, error handling, and action-specific pages higher retrieval priority for write operations. * Return source links with every generated answer so developers can verify the recommendation. * Treat retrieved documentation as context, not executable authority; validate all generated writes independently. ## High-value documentation paths Sign `POST /exchange` requests with EIP-712 over secp256k1 — Agent and typed paths. Uniqueness and replay-protection requirements for every signing address. Market, account, agent, and order queries through `POST /info`. State-changing actions and the shared `POST /exchange` envelope. Real-time market and private account streams for synchronized agent state. Authorization, expiry, monitoring, rotation, and revocation for automated signers. Do not place private keys, seed phrases, signing-service credentials, or unrestricted live-trading permissions in an LLM context. The model should produce an intent or unsigned payload; a separate trusted service should validate and sign it. # Agents Overview Source: https://docs.upsidemax.xyz/agents/overview Connect coding assistants and automated trading agents to UpsideMAX using machine-readable docs, signed write requests, unsigned reads, and real-time WebSocket streams. UpsideMAX supports two complementary agent workflows: * **Documentation agents** use machine-readable documentation to answer integration questions, generate code, and navigate the API surface. * **Trading agents** use a dedicated agent wallet to read market and account state, sign approved actions, and react to real-time WebSocket events without exposing the master account's private key. An AI model should never be treated as the final authority for order parameters, balances, or risk limits. Validate generated payloads in code and enforce deterministic checks before signing or submitting any write request. ## Recommended architecture Start with [`/llms.txt`](https://docs.upsidemax.xyz/llms.txt) to identify the relevant pages and endpoints without loading the entire documentation set. Fetch individual pages for focused tasks, or use [`/llms-full.txt`](https://docs.upsidemax.xyz/llms-full.txt) when the agent needs broad API context for code generation or retrieval. Use [`POST /info`](/info/overview) for market configuration, prices, order books, orders, balances, positions, and authorized agent records. Read requests do not require request signing. Send state-changing operations through [`POST /exchange`](/exchange/overview). Keep the master key offline and authorize a separate API wallet with [`approveAgent`](/exchange/approve-agent) for automated execution. Subscribe to the [WebSocket API](/websocket/overview) for order-book updates, trades, candles, order changes, open orders, and fills instead of repeatedly polling REST endpoints. **Note:** WebSocket does not yet stream **position** or **balance/collateral** changes — poll [`POST /info`](/info/overview) (`userAccount`) for positions and balances. Native WebSocket streams for them are planned. ## Choose the right integration path Install the open-source skills bundle so Claude Code or Codex can register, trade, and stream on Devnet from plain-language requests — zero setup. Give coding assistants and custom agents a concise documentation index or the full API reference in Markdown. Delegate trading actions to a separate wallet, set an expiry, monitor authorization, and revoke access when needed. Retrieve configuration, live market state, account state, and order data without signing. Maintain a synchronized local view of market and account events for low-latency agent workflows. ## Agent control loop A live trading agent should separate reasoning from execution: 1. **Observe** — read configuration and current state from REST, then keep it fresh through WebSocket subscriptions. 2. **Propose** — let the model or strategy engine produce a structured intent, such as side, contract, size, price, and risk rationale. 3. **Validate** — enforce deterministic rules for contract IDs, numeric precision, maximum position size, leverage, reduce-only behavior, available margin, and allowed action types. 4. **Sign** — sign only the validated action with the authorized agent wallet and a unique nonce. 5. **Submit** — send the envelope to `POST /exchange` and inspect both transport-level and business-level errors. 6. **Reconcile** — confirm the resulting order or fill through `POST /info` and private WebSocket channels. Positions and balances are currently available only via `POST /info` (`userAccount`) — WebSocket does not push them yet. Never expose a master or agent private key to an LLM prompt, chat transcript, application log, analytics event, or error-reporting service. Keep signing in a separate, deterministic component backed by a secure secret store. ## Starter system prompt Use the following as a base prompt for an API-aware coding agent: ```text theme={null} You are integrating with the UpsideMAX API. Documentation index: https://docs.upsidemax.xyz/llms.txt Full documentation: https://docs.upsidemax.xyz/llms-full.txt Rules: - Use POST /info for reads and POST /exchange for writes. - Do not invent fields, action types, endpoints, or response properties. - Preserve numeric strings exactly where the API expects strings. - For writes, use EIP-712 signing (secp256k1) with a unique nonce; see the Authentication page for the Agent and typed paths. - On Devnet, `registerAccount` requires a valid single-use `inviteCode` at the envelope top level (not part of the signature); obtain it from the UpsideMAX team. - WebSocket streams market data, order updates, open orders, and fills only. It does NOT yet push position or balance/collateral changes — poll POST /info (userAccount) for positions and balances. Native WS streams for them are planned. - Never request, reveal, print, or log a private key. - Return proposed actions as structured JSON for deterministic validation before signing. - Cite the documentation page used for every non-obvious implementation decision. ``` ## Go-live checklist * Pin explicit risk limits outside the model prompt. * Reject unknown fields and unsupported action types with a strict schema. * Use a named agent wallet with an expiry for each environment or strategy. * Maintain a monotonic nonce per signing address. * Reconcile every submitted action against order and fill updates. * Add a kill switch that revokes the agent and stops the signer independently of the model. * Test on Devnet with small limits before increasing exposure. # Agent Skills Plugin Source: https://docs.upsidemax.xyz/agents/skills Install the open-source UpsideMAX skills plugin so your AI coding assistant (Claude Code, Codex, or Cursor) can register, trade, and stream on UpsideMAX Devnet from plain-language requests — no setup. The **UpsideMAX skills plugin** is an open-source bundle that teaches an AI coding assistant — [Claude Code](https://claude.com/claude-code), Codex, or Cursor — how to drive UpsideMAX for you. Ask in plain language ("let me try it", "buy BTC", "show my positions", "stream trades") and the assistant runs the real, signed API calls against Devnet. It's the fastest way to experience the exchange without writing any code first. Everything runs on **Devnet** (`https://dev.upsidemax.xyz`). Wallets are generated at runtime and funded by the auto-airdrop — no real funds, no Mainnet risk, nothing to configure. Open-source. Clone it, read the code, or contribute. MIT licensed. ## What's in the plugin Five skills. Start with **`upside-test`** — it's the guided, one-command experience layer over the others. | Skill | What it does | | ------------------- | --------------------------------------------------------------------------------------------------------------------- | | `upside-test` | **Start here.** Try the whole product in single commands — register, trade, positions, live market — with zero setup. | | `upside-onboarding` | Create an account, receive the test airdrop, verify balances. | | `upside-trading` | Market data, place / modify / cancel orders, leverage. | | `upside-websocket` | Subscribe to live order book, trades, candles, and account streams. | | `upside-advanced` | Agent-wallet delegation, TP/SL, and decoding numeric error codes. | ## Install & start Two steps in every tab: **install** from the repo, then the **one line that starts the experience**. All tabs install the same `SKILL.md` files — pick yours. **1. Install** — in an open Claude Code session: ``` /plugin marketplace add upsidemax/upside-agent-skills /plugin install upside-agent-skills@upside-agent-skills /reload-plugins ``` **2. ▶ Start** — run the skill: ``` /upside-agent-skills:upside-test ``` It opens the guided **numbered menu** — pick a number, or just say **"Let me try UpsideMAX"**. **1. Install** — in a Codex terminal (or run `/plugins` inside a session and install it from the marketplace tab): ```bash theme={null} codex plugin marketplace add upsidemax/upside-agent-skills codex plugin add upside-agent-skills@upside-agent-skills ``` **2. ▶ Start** — in the Codex chat, type `/` and choose **`upside-test`**, or just say **"Let me try UpsideMAX"**. It opens the guided numbered menu. If your Codex build predates the plugin marketplace, use the **Terminal** section below — it needs no plugin system. **1. Install** — Cursor imports skills through its UI (no CLI): **Customize → Rules → Add Rule → Remote Rule (GitHub)**, and paste `https://github.com/upsidemax/upside-agent-skills`. (Or drop them into the skills folder: `git clone https://github.com/upsidemax/upside-agent-skills && cp -R upside-agent-skills/skills/* ~/.cursor/skills/`, then reload Cursor.) **2. ▶ Start** — in Agent chat, type `/` and choose **`upside-test`**, or just say **"Let me try UpsideMAX"**. It opens the guided numbered menu. Before registering you'll need an **alpha test invitation code** — the assistant asks for it (or set `UPSIDE_INVITE_CODE=`); request one from the UpsideMAX team. Python 3.9+ is required, and the skill installs its own Python dependencies on first run. ### Prefer the terminal? The `upside-test` launcher is also a plain script — it works the same whether you run it through **Claude Code, Codex, Cursor, or no assistant at all**. `curl` / `tar` ship with macOS, Linux, and Windows 10+. ```bash theme={null} curl -L https://github.com/upsidemax/upside-agent-skills/archive/refs/heads/main.tar.gz | tar xz cd upside-agent-skills-main export UPSIDE_INVITE_CODE= # request one from the UpsideMAX team python3 skills/upside-test/scripts/play.py # numbered menu; add `full` to run the demo ``` Registers a test account, funds it, and lets you trade — the full experience from your shell. (`git clone`, or a [ZIP download](https://github.com/upsidemax/upside-agent-skills/archive/refs/heads/main.zip), work too.) ## Using it The skill shows a **numbered menu** — reply with a number to run a flow. Or skip the menu and just say what you want; the assistant maps it to the right flow: | You say | The assistant does | | ---------------------- | ------------------------------------------------------------------------ | | "Let me try UpsideMAX" | Runs the whole flow: account → test funds → a trade at the current price | | "Buy 1 BTC" | Places a market order and reports whether it filled | | "Show my positions" | Prints your open positions and balances | | "Watch live trades" | Opens a live price feed | | "What's leverage?" | Explains the concept in plain language | Replies come back in your language. Registration needs an alpha test invitation code — the assistant asks for it (request one from the UpsideMAX team). Prefer to see the code behind a step? Ask the assistant to "show the request" — the test drive can print the exact signed `POST /exchange` envelope it sent, so you can lift it straight into your own integration. From there, the [Authentication](/guide/authentication) and [Exchange API](/exchange/overview) pages cover the full signing scheme and every action. ## Good to know * **Devnet only.** The plugin targets Devnet by default; Mainnet access is arranged with the UpsideMAX team. * **Keys stay local.** Wallets are generated at runtime and stored in a local session file — you're never asked to paste a private key into chat. * **It's a starting point.** Use it to learn the flows quickly, then build your own integration against the REST and WebSocket APIs documented in the rest of these docs. # Approve Agent Source: https://docs.upsidemax.xyz/exchange/approve-agent Approve an agent wallet to sign trade operations on behalf of your master account, keeping your master private key offline during automated trading. The `approveAgent` action authorizes an **agent** — a separate hot wallet — to sign trading operations on your behalf. Agents can submit orders, cancellations, modifies, and TP/SL requests without requiring your master account's private key to be present. This lets you keep your master key in cold storage while running automated strategies with a dedicated API wallet. Each master account supports up to **1 anonymous agent** and **3 named agents**. Re-approving an already-authorized address refreshes its expiry without consuming an additional quota slot. If you approve a named agent with a name that matches an existing named agent, the new entry overwrites the old one. Only a master account can approve or revoke agents. Agent wallets cannot manage other agents. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request All requests must include an ECDSA signature and a millisecond-precision nonce. This request must be signed by the **master account**. ```json theme={null} { "action": { "type": "approveAgent", "agentAddress": "0xabc0000000000000000000000000000000000001", "agentName": "bot1", "validUntil": 0 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859500000 } ``` ### Action fields Fixed value: `"approveAgent"`. The Ethereum address of the wallet to authorize as an agent. Must be a valid checksummed or lowercase hex address in the format `0x` followed by 40 hex characters. A human-readable label. Pass an **empty string** `""` for the anonymous slot, or a name for one of the 3 named quota slots (re-using a name overwrites that entry). **Must be present in the signed JSON** — omitting it yields `401 SIGNATURE_INVALID`. Expiry as Unix milliseconds; `0` = permanent (no expiry). **Must be present in the signed JSON** (send `0` for permanent) — omitting it yields `401 SIGNATURE_INVALID`. ## Response ### Success ```json theme={null} { "status": "ok", "response": { "type": "approveAgent", "data": { "agentAddress": "0xabc0000000000000000000000000000000000001" } } } ``` ### Response fields The address of the agent that was approved, echoed back for confirmation. ## Error reference Rejections return `status: "ok"` with an `errorCode` and `errorMessage` in `data`. | Cause | Description | | -------------------------------------- | ----------------------------------------------------------------------------- | | Invalid agent address | The provided `agentAddress` is not a valid Ethereum address. | | Address already used by another master | The wallet is already registered as an agent for a different master account. | | Named quota full | You already have 3 named agents and are trying to add a fourth distinct name. | | Signer not registered | The signing key is not recognized as a valid master account. | To view your currently authorized agents, query `userAgents` via POST `/info`. To revoke an agent, use [revokeAgent](/exchange/revoke-agent). # Cancel Order Source: https://docs.upsidemax.xyz/exchange/cancel Cancel one or more resting orders using the exchange-assigned order IDs (oid) returned when the orders were placed. Supports batches of up to 10. The `cancel` action removes resting orders from the order book using the exchange-assigned order IDs returned in the `resting.oid` field when you originally placed them. You can cancel up to 10 orders in a single signed request, mixing contracts freely within the batch. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange Content-Type: application/json ``` ## Request Body ```json theme={null} { "action": { "type": "cancel", "cancels": [ { "a": 1, "o": 3 } ] }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572961403 } ``` ## Action Fields Fixed value: `"cancel"`. Array of cancel objects. Maximum **10** per request. Each entry must identify the contract and the exchange order ID to cancel. ## Cancel Object Fields **asset** — The integer contract ID for the order you want to cancel. Must match the contract the order was placed on. **orderId** — The exchange-assigned order ID from the `resting.oid` field returned when the order was placed. ## Responses **Two status codes are possible.** The order and cancel family — `order`, `cancel`, `cancelByCloid`, `cancelAll`, and `modify` — may answer with **HTTP 200** carrying the result shown below, or **HTTP 202** acknowledging the request for asynchronous processing: ```json theme={null} {"status": "accepted", "response": {"type": "accepted", "data": {"count": 1}}} ``` On a 202 the per-item outcome arrives on the [`orderUpdates`](/websocket/order-updates) channel instead. Handle both — do not assume only one will occur. The response contains one status entry per cancel object in the same index order as your request. All cancellations were accepted. Each successful entry is the plain string `"success"`. ```json theme={null} { "status": "ok", "requestId": "req-144115188075855907", "response": { "type": "cancel", "data": { "statuses": ["success"] } } } ``` The provided order ID does not match any active order. The entry is an error object instead of the string `"success"`. ```json theme={null} { "status": "ok", "response": { "type": "cancel", "data": { "statuses": [{ "error": "orderId not found" }] } } } ``` ### Response Fields One entry per cancel object in request order. Each entry is either: * The string `"success"` — the order was found and removed from the book. * An object `{"error": ""}` — the cancellation failed; the reason string explains why. A top-level `"status": "ok"` does **not** mean every individual cancellation succeeded. Always inspect each element of `statuses` to detect per-order failures. # Cancel All Source: https://docs.upsidemax.xyz/exchange/cancel-all Cancel every open limit and conditional order for a specific contract in one request. The response reports how many orders of each type were removed. The `cancelAll` action removes all open orders — both limit and conditional (take-profit / stop-loss) — for a specified contract in a single signed request. Use this when you need to flatten your order book exposure for a contract quickly without tracking individual order IDs. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange Content-Type: application/json ``` ## Request Body ```json theme={null} { "action": { "type": "cancelAll", "a": 1 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778844423100 } ``` ## Action Fields Fixed value: `"cancelAll"`. **asset** — The integer contract ID for which all open orders should be cancelled. Only orders on this contract are affected; orders on other contracts remain untouched. ## Response **Two status codes are possible.** The order and cancel family — `order`, `cancel`, `cancelByCloid`, `cancelAll`, and `modify` — may answer with **HTTP 200** carrying the result shown below, or **HTTP 202** acknowledging the request for asynchronous processing: ```json theme={null} {"status": "accepted", "response": {"type": "accepted", "data": {"count": 1}}} ``` On a 202 the per-item outcome arrives on the [`orderUpdates`](/websocket/order-updates) channel instead. Handle both — do not assume only one will occur. ```json theme={null} { "status": "ok", "response": { "type": "cancelAll", "data": { "limitCancelled": 2, "conditionalCancelled": 0 } } } ``` ### Response Fields The number of resting limit orders that were cancelled for the specified contract. The number of conditional orders (take-profit and stop-loss) that were cancelled for the specified contract. If there are no open orders for the contract, the call still succeeds and both counts will be `0`. This makes `cancelAll` safe to call defensively at the start of a session or strategy reset. `cancelAll` is scoped to a **single contract** per request. To cancel all orders across multiple contracts, send one `cancelAll` request per contract, each with its own signed nonce. # Cancel By Cloid Source: https://docs.upsidemax.xyz/exchange/cancel-by-cloid Cancel resting orders using the client order IDs you assigned at placement. Useful when you track orders by your own IDs without storing exchange oids. The `cancelByCloid` action lets you cancel orders using the client order ID (`c` field) you assigned when placing each order. This is useful when your system manages its own order identifiers and you want to avoid storing the exchange-assigned `oid` alongside them. You can include multiple cancels in a single signed request. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange Content-Type: application/json ``` ## Request Body ```json theme={null} { "action": { "type": "cancelByCloid", "cancels": [ { "a": 1, "cloid": "1778763737044" } ] }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778763737200 } ``` ## Action Fields Fixed value: `"cancelByCloid"`. Array of cancel-by-cloid objects. Each entry identifies the contract and the client order ID of the order to cancel. ## Cancel Object Fields **asset** — The integer contract ID for the order you want to cancel. Must match the contract the order was placed on. **clientOrderId** — The client order ID you set in the `c` field when placing the order (an int64 decimal string, e.g. `"1778763737044"`). This value must match exactly. ## Responses **Two status codes are possible.** The order and cancel family — `order`, `cancel`, `cancelByCloid`, `cancelAll`, and `modify` — may answer with **HTTP 200** carrying the result shown below, or **HTTP 202** acknowledging the request for asynchronous processing: ```json theme={null} {"status": "accepted", "response": {"type": "accepted", "data": {"count": 1}}} ``` On a 202 the per-item outcome arrives on the [`orderUpdates`](/websocket/order-updates) channel instead. Handle both — do not assume only one will occur. The order was found by its client order ID and removed from the book. ```json theme={null} { "status": "ok", "response": { "type": "cancelByCloid", "data": { "statuses": ["success"] } } } ``` No active order matches the provided `cloid` for the given contract. ```json theme={null} { "status": "ok", "response": { "type": "cancelByCloid", "data": { "statuses": [{ "error": "clOrdId not found" }] } } } ``` ### Response Fields One entry per cancel object in request order. Each entry is either: * The string `"success"` — the order was located by client order ID and cancelled. * An object `{"error": ""}` — the cancellation failed; inspect the reason string for details. To use `cancelByCloid`, you must have set the `c` field on the original `order` request. If you did not provide a client order ID at placement, use the standard `cancel` action with the exchange `oid` instead. # Cancel Conditional Source: https://docs.upsidemax.xyz/exchange/cancel-conditional Cancel one conditional order by its order ID. Works for TP/SL and standalone triggers. Returns an error if not found or owned by a different user. The `cancelConditional` action cancels a single conditional order by its order ID. It works for both TP/SL orders created via [`tpSl`](/exchange/tp-sl) and standalone trigger orders. Use this action when you need to cancel a specific leg of a TP/SL setup; to cancel all TP/SL orders for a position at once, use [`cancelTpSl`](/exchange/cancel-tp-sl) instead. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request All requests must include an ECDSA signature and a millisecond-precision nonce. ```json theme={null} { "action": { "type": "cancelConditional", "oid": 734796988633055604 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778858800000 } ``` ### Action fields Fixed value: `"cancelConditional"`. The conditional order ID to cancel. Must be greater than `0`. You receive this ID in the `tpOrderId` or `slOrderId` field of the [`tpSl`](/exchange/tp-sl) response. ## Response ### Success ```json theme={null} { "status": "ok", "response": { "type": "cancelConditional", "data": { "cancelledCount": 1 } } } ``` ### Response fields The number of orders cancelled. Always `1` on success for this action. ## Error reference A rejection is returned with HTTP 200 and `status: "ok"` — the failure surfaces as an `errorCode` inside `response.data`, not as a top-level `status: "error"`. | Error | Cause | | ----------------------------- | ---------------------------------------------------------------------------------------------- | | `OrderNotFound` (`errorCode`) | No conditional order with the given `oid` exists or it has already been filled/cancelled. | | `ScopeMismatch` (`errorCode`) | The order exists but belongs to a different user account. You can only cancel your own orders. | # Cancel TP/SL Source: https://docs.upsidemax.xyz/exchange/cancel-tp-sl Cancel all take-profit and stop-loss trigger orders for a specific position in one request. Returns a count of cancelled orders; zero is not an error. The `cancelTpSl` action cancels all TP/SL trigger orders associated with a specific position, identified by contract ID and position side. This is the fastest way to remove all trigger orders for a position without having to look up and cancel individual order IDs. This action is idempotent: if there are no active TP/SL orders for the specified position, the request succeeds and returns `cancelledCount: 0` rather than an error. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request All requests must include an ECDSA signature and a millisecond-precision nonce. ```json theme={null} { "action": { "type": "cancelTpSl", "a": 1, "positionSide": 0 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778858700000 } ``` ### Action fields Fixed value: `"cancelTpSl"`. The contract ID whose TP/SL orders you want to cancel. Scopes the cancellation by position side: `0` = cancel all TP/SL orders for the entire contract, `1` = cancel LONG position TP/SL only, `2` = cancel SHORT position TP/SL only. Defaults to `0`. ## Response ```json theme={null} { "status": "ok", "response": { "type": "cancelTpSl", "data": { "cancelledCount": 2 } } } ``` ### Response fields The number of TP/SL trigger orders that were cancelled. A value of `0` means no orders were found for the specified position — this is not an error condition. To cancel a single specific TP or SL order by its ID, use [cancelConditional](/exchange/cancel-conditional) instead. # Market Deployer Source: https://docs.upsidemax.xyz/exchange/enroll-user-to-market-deployer Enroll your account in a market deployer before trading its contracts or transferring collateral in multi-market-deployer setups on UpsideMAX . A **market deployer** is an isolated trading environment with its own smart contract set and collateral pool. When you register your account, UpsideMAX automatically enrolls you in the default (bootstrap) market deployer. If you want to trade contracts belonging to a different market deployer, or transfer collateral to one, you must explicitly enroll in it first using the `enrollUserToMarketDeployer` action. This action is **idempotent**: submitting it when you are already enrolled does not produce an error — the server simply returns `enrolled: false` to indicate no new enrollment was created. You can safely call it as a precondition check before any market-deployer-specific operation. You must have a registered account before calling this action. If the signing wallet has not been registered, the server returns HTTP 400. ## Prerequisites * A registered account (see [`registerAccount`](/exchange/register-account)) * The numeric ID of the market deployer you want to join ## Request ```text theme={null} POST https://dev.upsidemax.xyz/exchange Content-Type: application/json ``` ```json theme={null} { "action": { "type": "enrollUserToMarketDeployer", "marketDeployerId": 2 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572816871 } ``` ### Action Fields Must be the fixed string `"enrollUserToMarketDeployer"`. The numeric identifier of the market deployer you want to enroll in. Contact the market deployer operator or query the Info API to discover available IDs. ## Response ```json theme={null} { "status": "ok", "requestId": "req-144115188075855890", "response": { "type": "enrollUserToMarketDeployer", "data": { "marketDeployerId": 2, "enrolled": true } } } ``` ### Response Fields The market deployer the account is now enrolled in, echoing the requested ID. `true` if this is the first time your account has enrolled in the specified market deployer. `false` if you were already enrolled — this is not an error; the operation is idempotent. A non-zero value indicates a business-level rejection. `0` or absent means success. Check this field even when `status` is `"ok"`. A human-readable description of the rejection when `errorCode` is non-zero. For example: `"unknown marketDeployer"` when the requested ID does not exist. `"status": "ok"` confirms the request was received and processed by the server. It does **not** guarantee enrollment succeeded. Always inspect `errorCode` and `errorMessage` inside `response.data` to confirm the business outcome — for example, passing an unknown `marketDeployerId` returns `status: "ok"` with a non-zero `errorCode`. ## Error Reference | Condition | Indicated By | Description | | ----------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------- | | Unknown market deployer | `errorCode != 0`, `errorMessage: "unknown marketDeployer"` | The `marketDeployerId` does not correspond to any registered deployer. | | Unregistered account | HTTP 400 | The signing wallet has not called `registerAccount` yet. | | Already enrolled | `enrolled: false` | Not an error — enrollment is idempotent. | Do not confuse a business rejection (`errorCode != 0` inside the response body) with an HTTP error. The server returns HTTP 200 for both successful enrollments and unknown-deployer rejections. Your client must always read `errorCode` to determine the true outcome. ## Next Steps Check that `enrolled: true` (or `enrolled: false` if you expected to already be enrolled) and that `errorCode` is `0` or absent. Use [`lockCollateral`](/exchange/lock-collateral) to deposit funds into the market deployer's collateral pool before placing orders. With collateral locked, you can now place orders on contracts belonging to this market deployer using the [`order`](/exchange/order) action. # Lock Collateral Source: https://docs.upsidemax.xyz/exchange/lock-collateral Move funds from your chain-level balance into a market deployer cross margin pool, making them available as margin for trading that deployer's contracts. When you want to trade contracts under a specific market deployer, you first need to move funds from your chain-level balance into that deployer's cross margin pool. The `lockCollateral` action performs this transfer — your total funds remain constant, since this is a reallocation rather than a deposit or mint. Once locked, those funds are available as margin for any contracts offered by that market deployer. You must be enrolled in the target market deployer before calling `lockCollateral`. If you haven't enrolled yet, see [`enrollUserToMarketDeployer`](/exchange/enroll-user-to-market-deployer). ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request Every request must carry an ECDSA signature and a nonce to prevent replay attacks. ```json theme={null} { "action": { "type": "lockCollateral", "marketDeployerId": 1, "coinId": 1, "amount": "1000" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778858900000 } ``` ### Action parameters The ID of the market deployer you want to fund. You must already be enrolled in this deployer. The coin or currency ID to transfer. Must match a coin supported by the target market deployer. The amount to transfer, expressed as a raw integer string (e.g. `"1000"`). Must be greater than zero. Decimals are not accepted — use the coin's smallest unit. ## Response A successful transfer returns the updated balances for both your chain-level ledger and the market deployer's cross margin pool. ```json theme={null} { "status": "ok", "response": { "type": "lockCollateral", "data": { "coinId": 1, "amount": 1000, "ledgerAfter": 5000, "marketDeployerAfter": 3000 } } } ``` The coin ID that was transferred, confirming which asset was moved. The amount that was transferred into the market deployer's cross margin pool. Your chain-level balance for this coin after the transfer. Your cross margin balance within the target market deployer after the transfer. ## Errors If the request cannot be fulfilled, the exchange still returns **HTTP 200** with `status: "ok"` — the rejection is reported inside `response.data`, where a server-assigned integer `errorCode` and a human-readable `errorMessage` are populated while the numeric amount and balance fields are set to `0`. Common causes include: * **Insufficient chain-level balance** — the requested `amount` exceeds your available chain-level funds for the given coin. * **Not enrolled in the market deployer** — you must enroll before locking collateral into a market deployer. * **Amount ≤ 0** — the `amount` field must be a positive integer string. ```json theme={null} { "status": "ok", "response": { "type": "lockCollateral", "data": { "coinId": 0, "amount": 0, "ledgerAfter": 0, "marketDeployerAfter": 0, "errorCode": 1, "errorMessage": "insufficient chain-level balance" } } } ``` To move funds in the opposite direction — from a market deployer back to your chain-level balance — use [`unlockCollateral`](/exchange/unlock-collateral). # Lock Into Share Group Source: https://docs.upsidemax.xyz/exchange/lock-into-share-group Move funds from your chain-level balance into a share group margin pool directly, bypassing the per-market-deployer step for PORTFOLIO mode trading. Rather than routing funds through a market deployer first, `lockIntoShareGroup` lets you move collateral directly from your chain-level balance into a share group's shared margin pool in a single step. This is the most direct way to fund PORTFOLIO mode trading when you already have chain-level funds available and want to skip the intermediate per-market-deployer allocation. Your account must be in **PORTFOLIO mode** before you can fund share groups. Switch modes first with [`setMarginShareType`](/exchange/set-margin-share-type) using `marginShareType: 1`. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request Every request must carry an ECDSA signature and a nonce to prevent replay attacks. ```json theme={null} { "action": { "type": "lockIntoShareGroup", "groupId": 3, "coinId": 1, "amount": "1000" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859400000 } ``` ### Action parameters The ID of the target share group. Funds are credited directly to this group's shared margin pool. The coin or currency ID to transfer from your chain-level balance into the share group. The amount to transfer, expressed as a raw integer string (e.g. `"1000"`). Must be greater than zero and must not exceed your available chain-level balance for the given coin. Decimals are not accepted — use the coin's smallest unit. ## Response A successful transfer returns the updated balances for both the source (your chain-level ledger, represented as `fromBalanceAfter`) and the destination share group. The response type is `shareGroupFund`, consistent with other share group fund operations. ```json theme={null} { "status": "ok", "response": { "type": "shareGroupFund", "data": { "coinId": 1, "amount": 1000, "fromBalanceAfter": 4000, "toBalanceAfter": 1000 } } } ``` The coin ID that was transferred. The amount moved from your chain-level balance into the share group. Your chain-level balance for this coin after the transfer. The share group's margin pool balance after the transfer. ## Errors If the request cannot be fulfilled, the exchange still returns **HTTP 200** with `status: "ok"` — the rejection is reported inside `response.data`, where a server-assigned integer `errorCode` and a human-readable `errorMessage` are populated while the numeric amount and balance fields are set to `0`. Common causes include: * **Insufficient chain-level balance** — the requested `amount` exceeds your available chain-level funds for the given coin. * **Account not in PORTFOLIO mode** — share groups are only active when your margin share type is `1`. * **Invalid `groupId`** — the specified share group must exist and be accessible to your account. * **Amount ≤ 0** — the `amount` field must be a positive integer string. ```json theme={null} { "status": "ok", "response": { "type": "shareGroupFund", "data": { "coinId": 0, "amount": 0, "fromBalanceAfter": 0, "toBalanceAfter": 0, "errorCode": 1, "errorMessage": "insufficient chain-level balance" } } } ``` To withdraw funds from a share group back to your chain-level balance, use [`unlockFromShareGroup`](/exchange/unlock-from-share-group). To move funds between a share group and a market deployer, use [`transferMdToShareGroup`](/exchange/transfer-md-to-share-group) or [`transferShareGroupToMd`](/exchange/transfer-share-group-to-md). # Modify Source: https://docs.upsidemax.xyz/exchange/modify Modify a resting order's price, quantity, time-in-force, or client order ID. Locate the order by exchange order ID or client order ID. The `modify` action updates a resting (OPEN status) order without requiring you to cancel and replace it. You can change the price, total size, time-in-force (Gtc or Alo only), and client order ID. Direction (`b`), contract (`a`), and `reduceOnly` (`r`) cannot be changed — cancel the order and place a new one if you need to alter those fields. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange Content-Type: application/json ``` ## Priority Rules How you locate and modify the order determines whether it retains its queue position: * **Same price, reduce size only** — the modification is applied in-place. The order keeps its existing FIFO queue priority **and its order ID**. * **Change price, increase size, or change TIF/cloid** — the order is removed and re-queued at the back of the new price level, losing its queue priority. **The order ID is still preserved.** An increase in size passes an initial-margin pre-check before it is re-queued. If margin is insufficient, the order is **rolled back to its original parameters** and the response carries an `errorCode`. If the new price would immediately cross the book and match against a resting opposite-side order, the modification request is rejected. Cancel the order and place a new one to execute at a marketable price. ## Request Body Use `oid` to identify the order by the exchange-assigned ID returned in `resting.oid`. ```json theme={null} { "action": { "type": "modify", "a": 1, "oid": 12345, "p": "150", "s": "8", "tif": "Gtc" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778858100000 } ``` Use `cloid` to find the order by its client-assigned ID, and optionally update the client order ID at the same time with `c`. ```json theme={null} { "action": { "type": "modify", "a": 1, "cloid": "1778763737044", "p": "151", "c": "1778858200000" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778858200100 } ``` ## Action Fields Fixed value: `"modify"`. **asset** — The integer contract ID of the order to modify. **orderId** — Exchange-assigned order ID from `resting.oid`. Takes priority over `cloid` when both are provided. At least one of `oid` or `cloid` must be present. **clientOrderId (locate)** — Client order ID used to find the order when `oid` is not provided. Must match the `c` field set at placement. **price** — New limit price as a decimal string. Omit to keep the existing price unchanged. **size** — New total target size as a decimal string. This is the **total** quantity including any already-filled amount, not just the remaining leaves size. Omit to keep the existing size unchanged. **timeInForce** — New time-in-force for the order. Only `"Gtc"` (good-til-cancel) or `"Alo"` (add-liquidity-only / post-only) are accepted. Omit to keep the existing TIF unchanged. **clientOrderId (update)** — New client order ID to assign to the order after modification. Omit to keep the existing client order ID unchanged. You must provide at least one of `oid` or `cloid` to identify the order. All other fields (`p`, `s`, `tif`, `c`) are optional — include only the fields you want to change. ## Responses **Two status codes are possible.** The order and cancel family — `order`, `cancel`, `cancelByCloid`, `cancelAll`, and `modify` — may answer with **HTTP 200** carrying the result shown below, or **HTTP 202** acknowledging the request for asynchronous processing: ```json theme={null} {"status": "accepted", "response": {"type": "accepted", "data": {"count": 1}}} ``` On a 202 the per-item outcome arrives on the [`orderUpdates`](/websocket/order-updates) channel instead. Handle both — do not assume only one will occur. The order was modified. The response reflects the new state of the order. ```json theme={null} { "status": "ok", "response": { "type": "modify", "data": { "orderId": 12345, "limitPrice": 150, "totalSize": 8, "leavesSize": 5, "clientOrderId": 1778858200000 } } } ``` The modification was rejected. Check `errorCode` and `errorMessage` for the reason. ```json theme={null} { "status": "ok", "response": { "type": "modify", "data": { "orderId": 0, "limitPrice": 0, "totalSize": 0, "leavesSize": 0, "clientOrderId": 0, "errorCode": 7, "errorMessage": "orderId not found" } } } ``` ### Response Fields The unchanged exchange-assigned order ID of the modified order. The new limit price after the modification is applied. The new total size of the order (including already-filled quantity). The remaining unfilled quantity still resting in the book after the modification. The updated client order ID. Reflects the new value if `c` was provided, or the previous value if it was not. Absent or `0` indicates success. Any non-zero value indicates a rejection; read `errorMessage` for details. On rejection, all numeric fields are `0`. Human-readable rejection reason. Only present when `errorCode` is non-zero. ## Known Errors | errorMessage | Cause | | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `orderId not found` | The `oid` or `cloid` does not match any active order | | `clOrdId not found` | Located by `cloid`, but no matching active order was found | | `order already canceled` | The target order was already cancelled before this request arrived | | `order not open` | The order is in a terminal state (fully filled or cancelled) | | `new size must exceed already-filled amount` | The requested `s` is less than or equal to the quantity already filled | | `modify supports resting TIF only (GTC / POST_ONLY)` | `tif` was set to `"Ioc"` or another non-resting value | | `modified price is marketable (would cross the book); cancel and place a new order instead` | The new `p` would immediately match against a resting opposite-side order | | `newClientOrderId already in use by an active order` | The `c` value collides with another active order's client order ID | | `insufficient margin for modified order` | Increasing `s` would require more margin than is available; the change is automatically rolled back | # Place Order Source: https://docs.upsidemax.xyz/exchange/order Place limit (GTC, IOC, ALO), market, and trigger orders on UpsideMAX. Submit up to 10 orders in one signed request using compact single-letter keys. The `order` action places one or more orders against a contract. A single request can carry up to 10 order objects under one ECDSA signature. Order objects use compact single-letter keys to keep payloads small. Three order shapes are available, selected by the `t` field: **limit** (GTC, IOC, or ALO), **market**, and **trigger** (stop-loss, take-profit, or breakout entry). A limit or market entry order can also carry inline take-profit and stop-loss levels, so a position and its exits are established in one signed request. **Every order requires a price.** `p` is mandatory for market orders as well as limit orders — the matching price is the price you submit. The server does not compute a marketable price for you. A missing or non-positive `p` returns `400 BAD_REQUEST`. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange Content-Type: application/json ``` ## Request Rests in the order book until filled or cancelled (GTC), or follows the IOC / ALO policy you choose. `p` is the resting price. ```json theme={null} { "action": { "type": "order", "orders": [ { "a": 1, "b": true, "p": "100", "s": "10", "r": false, "t": { "limit": { "tif": "Gtc" } } } ], "grouping": "na" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572951477 } ``` Executes immediately against available liquidity. `p` is the **execution price** you are willing to cross to — compute it as `mark price ± your slippage allowance` and submit it. ```json theme={null} { "action": { "type": "order", "orders": [ { "a": 1, "b": true, "p": "100", "s": "10", "r": false, "t": { "market": {} } } ], "grouping": "na" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572951478 } ``` Rests off-book until the mark price crosses `triggerPx`, then places the order described by `a`, `b`, `s`, `r`, and `p`. ```json theme={null} { "action": { "type": "order", "orders": [ { "a": 1, "b": false, "p": "95", "s": "10", "r": true, "t": { "trigger": { "triggerPx": "96", "isMarket": true, "tpsl": "sl" } } } ], "grouping": "na" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572951479 } ``` A single limit or market entry that also arms a take-profit and a stop-loss. The `tp*` / `sl*` fields are part of the signed payload. ```json theme={null} { "action": { "type": "order", "orders": [ { "a": 1, "b": true, "p": "100", "s": "10", "r": false, "t": { "limit": { "tif": "Gtc" } }, "tpPrice": "120", "tpLimitPrice": "119", "tpSize": "0", "tpTriggerType": 0, "tpOrderType": 1, "slPrice": "90", "slLimitPrice": "89", "slSize": "0", "slTriggerType": 0, "slOrderType": 2 } ], "grouping": "na" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572951480 } ``` Route a builder fee to a registered address. ```json theme={null} { "action": { "type": "order", "orders": [ { "a": 1, "b": true, "p": "100", "s": "10", "r": false, "t": { "limit": { "tif": "Gtc" } }, "builderAddress": "0xbbbb...builder", "builderFee": 10 } ], "grouping": "na" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572951481 } ``` ## Action fields Fixed value: `"order"`. Array of order objects. Maximum **10** per request; more returns `BATCH_TOO_LARGE`. Compatibility field. The server does not validate it and it does not affect placement, but send the conventional value `"na"`. ## Order object fields Each element of the `orders` array uses compact single-letter keys. **asset** — The integer contract ID identifying which perpetual to trade. **isBuy** — `true` to buy/go long; `false` to sell/go short. **price** — Order price as a raw integer string, required for **every** order type. * **Limit order** — the resting price. * **Market order** — the execution price you are willing to cross to, computed client-side as `mark price ± slippage allowance`. * **Trigger order** — the resting price after the trigger fires when `isMarket` is `false`, or the execution price when it is `true`. Must be greater than zero and aligned to the contract's `tickSize`. A missing, zero, or misaligned price is rejected. **size** — Order quantity as a raw integer string. Must be positive. **reduceOnly** — `true` restricts the order to reducing an existing position. Omitting the field is equivalent to `false`. **orderType** — Selects limit, market, or trigger behavior. See [Order types](#order-types). **clientOrderId** — An int64 decimal string you assign for your own tracking (for example `"1778572951477"`). Use it later with [`cancelByCloid`](/exchange/cancel-by-cloid) to cancel without storing the exchange-assigned `oid`. **builder** — Optional builder-fee recipient address (`"0x"` + 40 hex). Omit for no builder. An unregistered address does **not** reject the order, but `builderFee` is silently zeroed. **builderFee** — Builder fee rate in basis points. Ignored when `builderAddress` is omitted. In a batched request only the **first** order's builder fields take effect — the engine uses `orders[0]`'s `builderAddress` / `builderFee` as the single shared builder for the whole batch, the same one-per-request rule as `vaultAddress`. You never send a position side. The server derives which position an order acts on from the contract, the direction `b`, and `r` — a reduce-only order closes the position opposite to its own direction. ## Order types | Type | Format | Description | | --------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | Limit GTC | `{"limit":{"tif":"Gtc"}}` | **Good-til-cancel** — rests in the book until filled or explicitly cancelled | | Limit IOC | `{"limit":{"tif":"Ioc"}}` | **Immediate-or-cancel** — fills what it can instantly, cancels the remainder | | Limit ALO | `{"limit":{"tif":"Alo"}}` | **Add-liquidity-only (post-only)** — rejected if it would immediately cross the spread | | Market | `{"market":{}}` | Executes immediately (IOC) at the price you submit in `p` | | Trigger | `{"trigger":{"triggerPx":"","isMarket":,"tpsl":"tp"\|"sl"}}` | Rests off-book until the mark price crosses `triggerPx`, then places the order | ### Trigger orders A trigger order consumes no book depth while it waits. When the **mark price** crosses `triggerPx`, the server places the order described by the remaining fields. Trigger price as a raw integer string, greater than zero. Evaluated against the mark price. `true` fires a market IOC order; `false` rests a GTC limit order. **Both require a positive top-level `p`** — the resting price when `false`, the execution price when `true`. `"tp"` or `"sl"`. Sets the direction of the trigger comparison, matching the convention used by [`tpSl`](/exchange/tp-sl). Constraints and behavior: * **Single order only.** Trigger orders are valid when `orders` has exactly one element. A batch containing one is rejected with `400 BAD_REQUEST`. * The trigger price is always evaluated against the **mark price**; no other price source can be selected. * A trigger order cannot also carry inline `tp*` / `sl*` fields. * **The response differs from an ordinary order** — see [Trigger order response](#trigger-order-response). * A resting trigger order appears in [`userOrders`](/info/user-orders) with `isConditional: true`, an `orderType` of `TPM` / `TPL` / `SM` / `SL`, and its `triggerPrice`. On the [`orderUpdates`](/websocket/order-updates) channel it carries the compact `cond: true` flag. * Cancel it with [`cancelConditional`](/exchange/cancel-conditional) or [`cancelTpSl`](/exchange/cancel-tp-sl); there is no dedicated cancel action. ## Inline take-profit and stop-loss A limit or market entry order can arm its own exits. These fields are part of the signed payload, so include them when you compute the signature. The `sl*` fields mirror the `tp*` fields exactly. Take-profit **trigger** price. `"0"` or omitted means no take-profit. Execution price used once the take-profit triggers, greater than zero. With a limit `tpOrderType` this is the resting price; with a market `tpOrderType` it is the price to cross to. A market take-profit still requires this price. Quantity to close on trigger. `"0"` closes the entire position. Price source for the trigger: `0` for mark price, `1` for oracle price. No other value is accepted. Order type placed when the take-profit triggers: `1` for limit, `2` for market. **Required whenever `tpPrice` is set** and must be omitted or `0` when it is not. Stop-loss trigger price. Mirrors `tpPrice`. Execution price once the stop-loss triggers. Mirrors `tpLimitPrice`. Quantity to close on trigger. Mirrors `tpSize`. Price source for the stop-loss trigger. Mirrors `tpTriggerType`. Order type placed when the stop-loss triggers. Mirrors `tpOrderType`. Inline TP/SL applies to **single-order requests only**. In a batch of two or more, every order's `tp*` / `sl*` fields are ignored. A **reduce-only order must not carry them** — the whole order is rejected if it does. Once the entry order is **completely filled**, the armed levels become conditional orders bound to the resulting position. They appear in [`userOrders`](/info/user-orders) with `isConditional: true` and `isPositionTpsl: true`, with `tpslParentOrderId` pointing at the entry order. **A partial fill does not arm them.** ## Responses Order submission returns either **HTTP 200** with per-order results, or **HTTP 202** acknowledging the batch for asynchronous processing. **Clients must handle both** — do not assume one or the other. The batch was accepted for asynchronous processing. No order IDs or fill results are returned inline. ```json theme={null} { "status": "accepted", "response": { "type": "accepted", "data": { "count": 1 } } } ``` Read each order's outcome from the [`orderUpdates`](/websocket/order-updates) channel, correlating by your `c` (client order ID). The same shape is used by the cancel actions. The limit order was accepted and is waiting in the book. ```json theme={null} { "status": "ok", "response": { "type": "order", "data": { "statuses": [{ "resting": { "oid": 1 } }] } } } ``` The order matched existing liquidity immediately. ```json theme={null} { "status": "ok", "response": { "type": "order", "data": { "statuses": [{ "filled": { "totalSz": "5", "avgPx": "100", "oid": 2 } }] } } } ``` The order was refused. The top-level status is still `"ok"`. ```json theme={null} { "status": "ok", "response": { "type": "order", "data": { "statuses": [{ "error": "size must be positive" }] } } } ``` ### Response fields `"ok"` for a synchronous result, `"accepted"` when the batch was taken for asynchronous processing. Malformed, unauthenticated, or pre-validation failures instead return a top-level `status: "error"` with a machine `code`. Present on an acknowledgement. Number of orders accepted for processing. Present on a synchronous result. One entry per submitted order, in request order. Each entry is one of: * **`resting`** — `{ "oid": }`, the order is in the book. * **`filled`** — `{ "oid": , "totalSz": "", "avgPx": "" }`. * **`error`** — a string explaining why that order was rejected. A top-level `"status": "ok"` does **not** mean every order succeeded. Inspect each element of `statuses` — a rejected order appears there as an `error` entry alongside successful siblings. ### Trigger order response A trigger order settles through the conditional-order path, so its **200 response uses a different shape**. Parse the receipt according to whether the order was a trigger order; do not apply the `statuses[]` shape to it. ```json theme={null} { "status": "ok", "response": { "type": "tpSl", "data": { "tpOrderId": 5, "slOrderId": 0 } } } ``` With `tpsl: "tp"` the new order's ID is in `tpOrderId` and `slOrderId` is `0`; with `tpsl: "sl"` the reverse. A 202 acknowledgement is identical to an ordinary order's. ## Retrieving order state When you receive a 202, and whenever you need lifecycle transitions, read order state from: * [`userOrders`](/info/user-orders) — active orders and their IDs. * [`orderUpdates`](/websocket/order-updates) and [`userFills`](/websocket/user-fills) — pushed as resting, fill, and cancel events occur. * [`orderHistory`](/info/order-history) — orders that have already terminated. ## Errors | Error | Cause | | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `size must be positive` | Quantity was zero or negative; rejected during pre-validation with HTTP 400. A quantity judged invalid later, during matching, reads `invalid size` instead. | | `price exceeds tick range (maxTicks x tickSize)` | The price is outside the contract's permitted tick range. The message is a fixed string and does not interpolate your values — read `maxTicks` and `tickSize` from [`configs`](/info/configs). | | `share margin group is FROZEN (new open orders rejected; reduceOnly close allowed)` | The account is in PORTFOLIO margin mode and the contract's share group is frozen. Only reduce-only closes are accepted while frozen. | | `trigger orders must be sent as a single order` | A trigger order was included in a batch of two or more. | For accounts in **PORTFOLIO** margin mode, margin is drawn from the shared group pool automatically whenever the contract belongs to a share group. You send no group-related fields — the system resolves it from your `marginShareType` and the contract's group membership. # Exchange API Overview Source: https://docs.upsidemax.xyz/exchange/overview All state-changing operations on UpsideMAX route through POST /exchange and are dispatched by the action.type field in your request body. Every write operation on UpsideMAX — placing orders, cancelling positions, adjusting margin, managing collateral, and configuring your account — is submitted through a single endpoint: `POST /exchange`. The server inspects the `action.type` field in the request body to determine which operation to execute, then recovers your wallet address from the accompanying ECDSA signature to authorize it. This unified design means you only need one integration point for all state-changing calls. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange Content-Type: application/json ``` ## Request Envelope Every request shares the same top-level envelope structure: ```json theme={null} { "action": { "type": "", ...actionFields }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572816871 } ``` | Field | Type | Description | | ----------- | ------- | ----------------------------------------------------------------------------------------------------- | | `action` | object | The operation payload. Must include `type` plus any action-specific fields. | | `signature` | object | ECDSA signature with `r`, `s`, and `v` components. The server recovers your wallet address from this. | | `nonce` | integer | Current Unix timestamp in **milliseconds**. Prevents replay attacks. | Some actions accept additional envelope-level fields (such as `inviteCode` for `registerAccount`). These sit alongside `action`, `signature`, and `nonce` — not inside `action` — and are **not** included in the signed payload. ## Authentication All requests require a valid ECDSA signature. The server recovers your wallet address from the `(r, s, v)` tuple and uses it to authorize the operation — no API key header is needed. See the [Authentication guide](/guide/authentication) for details on what to sign and how to construct the signature. ## Action Groups Register your wallet and enroll in market deployers. * [`registerAccount`](/exchange/register-account) * [`enrollUserToMarketDeployer`](/exchange/enroll-user-to-market-deployer) Place, modify, and cancel perpetual orders. * [`order`](/exchange/order) * [`cancel`](/exchange/cancel) * [`cancelByCloid`](/exchange/cancel-by-cloid) * [`cancelAll`](/exchange/cancel-all) * [`modify`](/exchange/modify) * [`updateSlippageSetting`](/exchange/update-slippage-setting) Adjust margin mode, leverage, and isolated margin balances. * [`updateIsolatedMargin`](/exchange/update-isolated-margin) * [`updateLeverage`](/exchange/update-leverage) * [`updateMarginMode`](/exchange/update-margin-mode) Set and cancel take-profit, stop-loss, and other conditional orders. * [`tpSl`](/exchange/tp-sl) * [`cancelTpSl`](/exchange/cancel-tp-sl) * [`cancelConditional`](/exchange/cancel-conditional) Lock, unlock, and transfer collateral across deployers and share groups. * [`lockCollateral`](/exchange/lock-collateral) * [`unlockCollateral`](/exchange/unlock-collateral) * [`transferBetweenDeployers`](/exchange/transfer-between-deployers) * [`setMarginShareType`](/exchange/set-margin-share-type) * [`transferMdToShareGroup`](/exchange/transfer-md-to-share-group) * [`transferShareGroupToMd`](/exchange/transfer-share-group-to-md) * [`lockIntoShareGroup`](/exchange/lock-into-share-group) * [`unlockFromShareGroup`](/exchange/unlock-from-share-group) Delegate signing to agent wallets. * [`approveAgent`](/exchange/approve-agent) * [`revokeAgent`](/exchange/revoke-agent) ## All Available Actions ### Account | Action | Description | | ------------------------------------------------------------------------ | --------------------------------------------------------------- | | [`registerAccount`](/exchange/register-account) | Register your wallet address and receive a unique `accountId`. | | [`enrollUserToMarketDeployer`](/exchange/enroll-user-to-market-deployer) | Enroll in an additional market deployer to trade its contracts. | ### Orders | Action | Description | | ------------------------------------------------------------ | ---------------------------------------------------------------------------- | | [`order`](/exchange/order) | Place a new limit or market order. | | [`cancel`](/exchange/cancel) | Cancel an open order by its server-assigned order ID. | | [`cancelByCloid`](/exchange/cancel-by-cloid) | Cancel an open order using your client-assigned order ID (cloid). | | [`cancelAll`](/exchange/cancel-all) | Cancel all open orders for a single contract (contract `a` required). | | [`modify`](/exchange/modify) | Modify the price or size of an existing open order. | | [`updateSlippageSetting`](/exchange/update-slippage-setting) | Set the market-order slippage cap for your account within a market deployer. | ### Margin & Leverage | Action | Description | | ---------------------------------------------------------- | --------------------------------------------------------- | | [`updateIsolatedMargin`](/exchange/update-isolated-margin) | Add or remove margin from an isolated-margin position. | | [`updateLeverage`](/exchange/update-leverage) | Change the leverage multiplier for a contract. | | [`updateMarginMode`](/exchange/update-margin-mode) | Switch a contract between cross and isolated margin mode. | ### Conditional Orders | Action | Description | | --------------------------------------------------- | ---------------------------------------------------------- | | [`tpSl`](/exchange/tp-sl) | Attach a take-profit and/or stop-loss to an open position. | | [`cancelTpSl`](/exchange/cancel-tp-sl) | Remove the take-profit and/or stop-loss from a position. | | [`cancelConditional`](/exchange/cancel-conditional) | Cancel a specific conditional order by its ID. | ### Collateral | Action | Description | | ------------------------------------------------------------------ | ----------------------------------------------------------------- | | [`lockCollateral`](/exchange/lock-collateral) | Lock collateral into a market deployer. | | [`unlockCollateral`](/exchange/unlock-collateral) | Unlock collateral from a market deployer. | | [`transferBetweenDeployers`](/exchange/transfer-between-deployers) | Move collateral from one market deployer to another. | | [`setMarginShareType`](/exchange/set-margin-share-type) | Configure the margin share type for an account. | | [`transferMdToShareGroup`](/exchange/transfer-md-to-share-group) | Transfer collateral from a market deployer into a share group. | | [`transferShareGroupToMd`](/exchange/transfer-share-group-to-md) | Transfer collateral from a share group back to a market deployer. | | [`lockIntoShareGroup`](/exchange/lock-into-share-group) | Lock collateral directly into a share group. | | [`unlockFromShareGroup`](/exchange/unlock-from-share-group) | Unlock collateral from a share group. | ### API Wallet | Action | Description | | ----------------------------------------- | ---------------------------------------------------------- | | [`approveAgent`](/exchange/approve-agent) | Delegate signing authority to an agent wallet address. | | [`revokeAgent`](/exchange/revoke-agent) | Remove signing authority from a previously approved agent. | ## Response Shape A successful request always returns HTTP 200 with `"status": "ok"`: ```json theme={null} { "status": "ok", "requestId": "req-144115188075855882", "response": { "type": "", ...actionSpecificFields } } ``` `"status": "ok"` indicates the request was received and processed — it does **not** guarantee the business operation succeeded. Some actions embed an `errorCode` / `errorMessage` inside `response`. Always check those fields after a successful HTTP 200. # Register Account Source: https://docs.upsidemax.xyz/exchange/register-account Register your wallet address with UpsideMAX to receive a unique accountId required for all subsequent trading and collateral operations. Before you can place orders or manage collateral on UpsideMAX , you must register your wallet address using the `registerAccount` action. A successful registration returns a unique `accountId` — a decimal integer that identifies your account across every subsequent API call. You only need to register once per wallet address. In gated environments — including Devnet — registration requires a valid alpha test invitation code. Pass `inviteCode` at the **envelope top level** — alongside `action`, `signature`, and `nonce` — not inside the `action` object. The alpha test invitation code is **not** included in the signed payload. ## Request ```text theme={null} POST https://dev.upsidemax.xyz/exchange Content-Type: application/json ``` ```json theme={null} { "action": { "type": "registerAccount", "address": "0x7b94aeea275c43ab537a8cd55f7551688c6521ad" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572816871, "inviteCode": "A3K9Q2" } ``` ### Action Fields Must be the fixed string `"registerAccount"`. The wallet address you are registering. Must be `0x`-prefixed and contain exactly 40 lowercase hexadecimal characters (EIP-55 checksummed addresses are also accepted). ### Envelope-Level Fields A 6-character alphanumeric alpha test invitation code. **Required** in gated environments; silently ignored in open environments. Do **not** include this field inside `action` — it must sit at the top level of the request body alongside `action`, `signature`, and `nonce`. It is not part of the signature. ## Response ```json theme={null} { "status": "ok", "requestId": "req-144115188075855882", "response": { "type": "registerAccount", "accountId": "3" } } ``` Your new account's unique identifier, encoded as a decimal string representing an int64. Store this value — you will need it for collateral operations, order placement, and any action that references your account. Even though `accountId` is returned as a string, it is a 64-bit integer. Use a big-integer or string type in your client to avoid precision loss in JavaScript environments. **Test funds are airdropped automatically.** On Devnet, a successful registration triggers an automatic airdrop of **10,000 USDC** of test collateral to your new account, typically within about 10 seconds. USDC is the only asset airdropped. These are Devnet-only tokens with no real value — no deposit or manual step is needed. Poll [`userAccount`](/info/user-account) until the balance appears, then lock collateral and start trading. ## Error Reference | Error | HTTP Status | Cause | | ------------------------ | ----------- | ------------------------------------------------------------------------------------ | | `ACCOUNT_ALREADY_EXISTS` | 400 | The provided `address` is already registered. Each wallet can only hold one account. | | `INVITE_CODE_INVALID` | 403 | The `inviteCode` was not found or has already been used. | | `inviteCode required` | 400 | The environment requires an alpha test invitation code but none was provided. | If you attempt to register the same wallet address twice, the server returns `ACCOUNT_ALREADY_EXISTS`. To retrieve an existing account's `accountId`, query the Info API instead of re-registering. ## Next Steps Store the returned `accountId` securely in your application config or database. All collateral and position operations reference it. You are automatically enrolled in the default market deployer on registration. To trade contracts in additional environments, call [`enrollUserToMarketDeployer`](/exchange/enroll-user-to-market-deployer). Deposit collateral into your chosen market deployer with [`lockCollateral`](/exchange/lock-collateral) before placing any orders. Once collateral is in place, use the [`order`](/exchange/order) action to open a perpetual position. # Revoke Agent Source: https://docs.upsidemax.xyz/exchange/revoke-agent Revoke a previously authorized agent wallet so it can no longer sign operations for your master account. The agent is invalidated immediately upon success. The `revokeAgent` action immediately invalidates a previously authorized agent wallet. Once revoked, the agent can no longer submit orders, cancellations, or any other signed operations on behalf of your master account. The quota slot previously occupied by the agent is freed and can be reused. Only the master account that originally approved the agent can revoke it. To view all currently active agents for your account, query [`userAgents`](/info/user-agents) via POST `/info`. Revocation takes effect immediately. Any in-flight requests signed by the agent that have not yet been processed by the exchange may still be accepted if they arrive before the revocation is recorded. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request All requests must include an ECDSA signature and a millisecond-precision nonce. This request must be signed by the **master account**. ```json theme={null} { "action": { "type": "revokeAgent", "agentAddress": "0xabc0000000000000000000000000000000000001" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859600000 } ``` ### Action fields Fixed value: `"revokeAgent"`. The Ethereum address of the agent wallet to revoke. Must match an agent previously approved by your master account. ## Response ### Success ```json theme={null} { "status": "ok", "response": { "type": "revokeAgent", "data": { "agentAddress": "0xabc0000000000000000000000000000000000001" } } } ``` ### Response fields The address of the agent that was revoked, echoed back for confirmation. ## Error reference Rejections return `status: "ok"` with an `errorCode` and `errorMessage` in `data`. | Cause | Description | | ------------------- | -------------------------------------------------------------------------------------- | | Agent not found | No agent with the given address is registered under your master account. | | Not owned by signer | The agent exists but was approved by a different master account; you cannot revoke it. | To authorize a new agent after revoking one, use [approveAgent](/exchange/approve-agent). # Set Margin Share Type Source: https://docs.upsidemax.xyz/exchange/set-margin-share-type Switch your account between UNIFIED mode (isolated per-market-deployer margin) and PORTFOLIO mode (shared group margin pool) to control how margin is allocated. UpsideMAX supports two margin sharing modes that determine how margin is allocated across your contracts. Use `setMarginShareType` to switch between them. The mode you choose affects every market deployer in your account, so ensure you have no open positions or active orders before switching — the exchange will reject the request otherwise. * **UNIFIED (0)** — the default mode. Each market deployer maintains its own isolated margin pool. A loss in one market deployer cannot draw down margin from another. * **PORTFOLIO (1)** — opt-in portfolio margin mode. Contracts that belong to the same share group draw from a single shared margin pool, enabling cross-margining. This can significantly reduce your overall margin requirements when you hold offsetting positions. You must have **no open positions and no active orders** before changing your margin share type. Close all positions and cancel all orders first, then resubmit this request. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request The example below switches your account to PORTFOLIO mode. To revert to UNIFIED, set `marginShareType` to `0`. ```json theme={null} { "action": { "type": "setMarginShareType", "marginShareType": 1 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859100000 } ``` ### Action parameters The margin mode to apply to your account: * `0` — **UNIFIED**: each market deployer has its own isolated margin pool (default). * `1` — **PORTFOLIO**: contracts within a share group share one margin pool. Any value other than `0` or `1` is rejected. ## Response On success, the exchange returns a governance acknowledgement. The `entityId` is always `0` and serves only as a confirmation token. ```json theme={null} { "status": "ok", "response": { "type": "governance", "data": { "entityId": 0 } } } ``` Always `0` on a successful `setMarginShareType` call. No further data is returned. ## Errors If the request cannot be fulfilled, the exchange still returns **HTTP 200** with `status: "ok"` — the rejection is reported inside `response.data`, where a server-assigned integer `errorCode` and a human-readable `errorMessage` are populated while the numeric data fields are set to `0`. Common causes include: * **Open positions exist** — all positions must be closed before switching modes. * **Active orders exist** — all orders must be cancelled before switching modes. * **Invalid `marginShareType` value** — only `0` and `1` are valid. ```json theme={null} { "status": "ok", "response": { "type": "governance", "data": { "entityId": 0, "errorCode": 1, "errorMessage": "cannot change margin share type while open positions exist" } } } ``` After switching to PORTFOLIO mode, use [`transferMdToShareGroup`](/exchange/transfer-md-to-share-group) or [`lockIntoShareGroup`](/exchange/lock-into-share-group) to fund your share groups before placing orders. # TP/SL Source: https://docs.upsidemax.xyz/exchange/tp-sl Create TP/SL trigger orders for an existing position. These orders don't consume margin and fire automatically when the trigger price is reached. The `tpSl` action creates take-profit (TP) and stop-loss (SL) trigger orders for an existing position. Unlike regular orders, TP/SL orders do not consume margin when created — they sit dormant and automatically place a real closing order when the market reaches the trigger price. You can set a TP only, an SL only, or both in a single request. For position-level TP/SL (`isPositionTpsl: true`), the closing direction is determined automatically from the position side — you don't need to specify it. The position must already exist before you create position-level TP/SL orders. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request All requests must include an ECDSA signature and a millisecond-precision nonce. ```json theme={null} { "action": { "type": "tpSl", "a": 1, "positionSide": 0, "isPositionTpsl": true, "tpPrice": "90000", "slPrice": "80000", "tpLimitPrice": "90000", "slLimitPrice": "80000", "tpSize": "0", "slSize": "0", "tpTriggerType": 0, "slTriggerType": 0, "tpOrderType": 1, "slOrderType": 1 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778858600000 } ``` ### Action fields Fixed value: `"tpSl"`. The contract ID the TP/SL orders apply to. Identifies the position side: `0` = ONE\_WAY, `1` = LONG, `2` = SHORT. Defaults to `0`. When `true`, creates position-level TP/SL orders that automatically close the position when triggered. When `false`, creates standalone trigger orders. Defaults to `false`. The side of the order placed when the trigger fires: `"B"` = buy, `"S"` = sell. Used only for standalone trigger orders (`isPositionTpsl` = `false`); for position-level TP/SL the closing direction is inferred automatically. When `true`, the triggered order is marked reduce-only and will only execute if it reduces an open position. Defaults to `false`. Trigger price for the take-profit leg. Set to `"0"` to skip the TP leg. At least one of `tpPrice` or `slPrice` must be greater than `"0"`. Trigger price for the stop-loss leg. Set to `"0"` to skip the SL leg. At least one of `tpPrice` or `slPrice` must be greater than `"0"`. Execution price used once the TP triggers. Must be greater than zero. With `tpOrderType` set to limit it is the resting price of a GTC order; with market it is the price the IOC order may cross to, computed as `mark price ± your slippage allowance`. Execution price used once the SL triggers. Must be greater than zero, with the same meaning as `tpLimitPrice`. A market-type TP/SL still requires a positive execution price. `"0"` is **not** accepted as shorthand for "close at market" — supply the price you are willing to cross to. The position size to close when the TP triggers. `"0"` = close the entire position. The position size to close when the SL triggers. `"0"` = close the entire position. The price feed used to evaluate the TP trigger: `0` = mark price, `1` = oracle price. Defaults to `0`. **Only these two values are accepted** — any other value is rejected with `invalid tp/sl trigger type`. The price feed used to evaluate the SL trigger: `0` = mark price, `1` = oracle price. Defaults to `0`. Same restriction as `tpTriggerType`. Type of order placed when the TP triggers: `1` = limit, `2` = market. **Required whenever `tpPrice` is greater than `"0"`**, and must be omitted or `0` when the TP leg is unset. Type of order placed when the SL triggers: `1` = limit, `2` = market. Required whenever `slPrice` is set, with the same rule as `tpOrderType`. ## Response ### Success ```json theme={null} { "status": "ok", "response": { "type": "tpSl", "data": { "tpOrderId": 123, "slOrderId": 124 } } } ``` ### Rejection A rejected request still returns HTTP 200 with `status: "ok"`, carrying `errorCode` and `errorMessage` inside `response.data`. Common causes are no open position, both `tpPrice` and `slPrice` at `"0"`, not being enrolled in the market deployer, and an unknown contract. ### Response fields The order ID assigned to the take-profit trigger order. Returns `0` if the TP leg was not set (i.e., `tpPrice` was `"0"`). The order ID assigned to the stop-loss trigger order. Returns `0` if the SL leg was not set (i.e., `slPrice` was `"0"`). Save the returned `tpOrderId` and `slOrderId` values if you need to cancel individual legs later using [cancelConditional](/exchange/cancel-conditional). To cancel all TP/SL orders for a position at once, use [cancelTpSl](/exchange/cancel-tp-sl). # Transfer Between Deployers Source: https://docs.upsidemax.xyz/exchange/transfer-between-deployers Transfer cross margin funds directly between two market deployers within your account, leaving your chain-level balance completely unaffected. If you maintain positions across multiple market deployers (MDs), you can rebalance margin between them directly using `transferBetweenDeployers`. The transfer moves funds from the source deployer's cross margin pool to the destination deployer's cross margin pool. Your chain-level balance is not affected in any way. Both deployers must be ones you are already enrolled in, and the transfer is subject to the same withdrawable amount constraints as [`unlockCollateral`](/exchange/unlock-collateral) on the source side. You must be enrolled in **both** the source and destination market deployers before calling this action. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request Every request must carry an ECDSA signature and a nonce to prevent replay attacks. ```json theme={null} { "action": { "type": "transferBetweenDeployers", "fromMarketDeployerId": 1, "toMarketDeployerId": 2, "coinId": 1, "amount": "1000" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859000000 } ``` ### Action parameters The ID of the source market deployer. The transfer amount is deducted from this deployer's cross margin pool and is subject to its withdrawable amount limit. The ID of the destination market deployer. The transfer amount is credited to this deployer's cross margin pool. The coin or currency ID to transfer. Must be a coin supported by both market deployers. The amount to transfer, expressed as a raw integer string (e.g. `"1000"`). Must be greater than zero and must not exceed the withdrawable amount on the source deployer. Decimals are not accepted — use the coin's smallest unit. ## Response A successful transfer returns the updated cross margin balances for both market deployers. ```json theme={null} { "status": "ok", "response": { "type": "transferBetweenDeployers", "data": { "coinId": 1, "amount": 1000, "fromBalanceAfter": 4000, "toBalanceAfter": 3000 } } } ``` The coin ID that was transferred. The amount moved from the source to the destination market deployer. The cross margin balance of the source market deployer after the transfer. The cross margin balance of the destination market deployer after the transfer. ## Errors If the request cannot be fulfilled, the exchange still returns **HTTP 200** with `status: "ok"` — the rejection is reported inside `response.data`, where a server-assigned integer `errorCode` and a human-readable `errorMessage` are populated while the numeric amount and balance fields are set to `0`. Common causes include: * **Amount exceeds withdrawable balance on source** — position IM and frozen order margin reduce how much can be moved from the source deployer. * **Not enrolled in one or both deployers** — you must be enrolled in both the source and destination MDs. * **Same source and destination** — `fromMarketDeployerId` and `toMarketDeployerId` must differ. * **Amount ≤ 0** — the `amount` field must be a positive integer string. ```json theme={null} { "status": "ok", "response": { "type": "transferBetweenDeployers", "data": { "coinId": 0, "amount": 0, "fromBalanceAfter": 0, "toBalanceAfter": 0, "errorCode": 1, "errorMessage": "requested amount exceeds withdrawable balance on source deployer" } } } ``` If you need to move funds involving your chain-level balance instead, use [`lockCollateral`](/exchange/lock-collateral) or [`unlockCollateral`](/exchange/unlock-collateral). # Transfer Market Deployer To Share Group Source: https://docs.upsidemax.xyz/exchange/transfer-md-to-share-group Transfer margin from a market deployer cross margin pool into a portfolio share group pool to fund PORTFOLIO mode margin sharing across contracts. In PORTFOLIO mode, contracts within a share group draw margin from a shared pool rather than individual per-market-deployer balances. Use `transferMdToShareGroup` to fund that pool by moving collateral from a market deployer's cross margin pool into a specific share group. The transfer is subject to the same withdrawable amount constraints that apply to the source market deployer — position IM and order-frozen margin reduce how much you can move. Your account must be in **PORTFOLIO mode** to use share groups. Switch modes first with [`setMarginShareType`](/exchange/set-margin-share-type). ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request Every request must carry an ECDSA signature and a nonce to prevent replay attacks. ```json theme={null} { "action": { "type": "transferMdToShareGroup", "marketDeployerId": 1, "groupId": 3, "coinId": 1, "amount": "1000" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859200000 } ``` ### Action parameters The ID of the source market deployer. Funds are deducted from this deployer's cross margin pool and are subject to its withdrawable amount limit. The ID of the target share group. Funds are credited to this group's shared margin pool. The coin or currency ID to transfer. The amount to transfer, expressed as a raw integer string (e.g. `"1000"`). Must be greater than zero and must not exceed the withdrawable amount on the source deployer. Decimals are not accepted — use the coin's smallest unit. ## Response A successful transfer returns the updated balances for both the source market deployer and the destination share group. ```json theme={null} { "status": "ok", "response": { "type": "shareGroupFund", "data": { "coinId": 1, "amount": 1000, "fromBalanceAfter": 4000, "toBalanceAfter": 1000 } } } ``` The coin ID that was transferred. The amount moved from the market deployer into the share group. The cross margin balance of the source market deployer after the transfer. The share group's margin pool balance after the transfer. ## Errors If the request cannot be fulfilled, the exchange still returns **HTTP 200** with `status: "ok"` — the rejection is reported inside `response.data`, where a server-assigned integer `errorCode` and a human-readable `errorMessage` are populated while the numeric amount and balance fields are set to `0`. Common causes include: * **Amount exceeds withdrawable balance on the market deployer** — position IM and frozen order margin on the source deployer constrain how much can be moved. * **Account not in PORTFOLIO mode** — share groups are only active when your margin share type is `1`. * **Invalid `groupId`** — the specified share group must exist and be accessible to your account. * **Amount ≤ 0** — the `amount` field must be a positive integer string. ```json theme={null} { "status": "ok", "response": { "type": "shareGroupFund", "data": { "coinId": 0, "amount": 0, "fromBalanceAfter": 0, "toBalanceAfter": 0, "errorCode": 1, "errorMessage": "account is not in portfolio margin mode" } } } ``` To move funds in the opposite direction — from a share group back to a market deployer — use [`transferShareGroupToMd`](/exchange/transfer-share-group-to-md). To fund a share group directly from your chain-level balance, use [`lockIntoShareGroup`](/exchange/lock-into-share-group). # Transfer Share Group To Market Deployer Source: https://docs.upsidemax.xyz/exchange/transfer-share-group-to-md Transfer available margin from a portfolio share group pool back into a market deployer cross margin pool to rebalance between margin modes. When you need to rebalance funds between your PORTFOLIO share groups and per-market-deployer cross margin pools, use `transferShareGroupToMd`. This action moves collateral from a share group's shared margin pool back into a specific market deployer's cross margin pool. The transfer is constrained by the withdrawable amount available in the source share group — meaning position IM and order-frozen margin held against contracts in that group reduce how much you can move. Your account must be in **PORTFOLIO mode** to interact with share groups. If you want to revert fully to UNIFIED mode, first drain all share groups back to their market deployers, then call [`setMarginShareType`](/exchange/set-margin-share-type) with `marginShareType: 0`. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request Every request must carry an ECDSA signature and a nonce to prevent replay attacks. ```json theme={null} { "action": { "type": "transferShareGroupToMd", "groupId": 3, "marketDeployerId": 1, "coinId": 1, "amount": "1000" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859300000 } ``` ### Action parameters The ID of the source share group. Funds are deducted from this group's shared margin pool, subject to its withdrawable amount limit. The ID of the destination market deployer. Funds are credited to this deployer's cross margin pool. The coin or currency ID to transfer. The amount to transfer, expressed as a raw integer string (e.g. `"1000"`). Must be greater than zero and must not exceed the withdrawable amount available in the source share group. Decimals are not accepted — use the coin's smallest unit. ## Response A successful transfer returns the updated balances for both the source share group and the destination market deployer. The response type is `shareGroupFund`, consistent with other share group fund operations. ```json theme={null} { "status": "ok", "response": { "type": "shareGroupFund", "data": { "coinId": 1, "amount": 1000, "fromBalanceAfter": 0, "toBalanceAfter": 5000 } } } ``` The coin ID that was transferred. The amount moved from the share group into the market deployer. The share group's margin pool balance after the transfer. The cross margin balance of the destination market deployer after the transfer. ## Errors If the request cannot be fulfilled, the exchange still returns **HTTP 200** with `status: "ok"` — the rejection is reported inside `response.data`, where a server-assigned integer `errorCode` and a human-readable `errorMessage` are populated while the numeric amount and balance fields are set to `0`. Common causes include: * **Amount exceeds withdrawable balance in the share group** — position IM and frozen order margin on contracts within the group constrain how much can be moved. * **Account not in PORTFOLIO mode** — share groups are only active when your margin share type is `1`. * **Invalid `groupId`** — the specified share group must exist and be accessible to your account. * **Amount ≤ 0** — the `amount` field must be a positive integer string. ```json theme={null} { "status": "ok", "response": { "type": "shareGroupFund", "data": { "coinId": 0, "amount": 0, "fromBalanceAfter": 0, "toBalanceAfter": 0, "errorCode": 1, "errorMessage": "requested amount exceeds withdrawable balance in share group" } } } ``` To move funds in the opposite direction — from a market deployer into a share group — use [`transferMdToShareGroup`](/exchange/transfer-md-to-share-group). To withdraw from a share group directly to your chain-level balance, use [`unlockFromShareGroup`](/exchange/unlock-from-share-group). # Unlock Collateral Source: https://docs.upsidemax.xyz/exchange/unlock-collateral Move available margin from a market deployer cross margin pool back to your chain-level balance, subject to position and order margin requirements. When you want to move funds out of a market deployer and back to your chain-level balance, use the `unlockCollateral` action. You can only withdraw up to the **withdrawable amount** — defined as your total equity in the market deployer minus the initial margin (IM) held against open positions and any margin frozen by active orders. Unrealized profits are included in equity but cannot be withdrawn until realized. You cannot withdraw funds that are currently committed as initial margin for open positions or frozen for pending orders. Close positions and cancel orders first if you need to free up additional margin. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request Every request must carry an ECDSA signature and a nonce to prevent replay attacks. ```json theme={null} { "action": { "type": "unlockCollateral", "marketDeployerId": 1, "coinId": 1, "amount": "1000" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859100000 } ``` ### Action parameters The ID of the market deployer from which you want to withdraw funds. The coin or currency ID to transfer back to your chain-level balance. The amount to withdraw, expressed as a raw integer string (e.g. `"1000"`). Must be greater than zero and must not exceed the withdrawable amount. Decimals are not accepted — use the coin's smallest unit. ## Response A successful withdrawal returns the updated balances for both the market deployer's cross margin pool and your chain-level ledger. ```json theme={null} { "status": "ok", "response": { "type": "unlockCollateral", "data": { "coinId": 1, "amount": 1000, "marketDeployerAfter": 2000, "ledgerAfter": 6000 } } } ``` The coin ID that was transferred, confirming which asset was moved. The amount that was withdrawn from the market deployer's cross margin pool. Your cross margin balance within the market deployer after the withdrawal. Your chain-level balance for this coin after the withdrawal. ## Errors If the request cannot be fulfilled, the exchange still returns **HTTP 200** with `status: "ok"` — the rejection is reported inside `response.data`, where a server-assigned integer `errorCode` and a human-readable `errorMessage` are populated while the numeric amount and balance fields are set to `0`. Common causes include: * **Amount exceeds withdrawable balance** — you cannot withdraw more than your equity minus position IM and order-frozen margin. * **Amount ≤ 0** — the `amount` field must be a positive integer string. * **Not enrolled in the market deployer** — the specified market deployer must be one you are enrolled in. ```json theme={null} { "status": "ok", "response": { "type": "unlockCollateral", "data": { "coinId": 0, "amount": 0, "marketDeployerAfter": 0, "ledgerAfter": 0, "errorCode": 1, "errorMessage": "requested amount exceeds withdrawable balance" } } } ``` To move funds in the opposite direction — from your chain-level balance into a market deployer — use [`lockCollateral`](/exchange/lock-collateral). # Unlock From Share Group Source: https://docs.upsidemax.xyz/exchange/unlock-from-share-group Withdraw available funds from a portfolio share group margin pool to your chain-level balance, subject to group equity and margin requirement constraints. When you want to pull funds out of a PORTFOLIO share group and return them to your chain-level balance, use `unlockFromShareGroup`. Like other withdrawal actions, you can only move up to the **withdrawable amount** — the share group's total equity minus the initial margin (IM) held against open positions and any margin frozen by active orders within that group. Unrealized profits are included in equity but cannot be withdrawn until realized. You cannot withdraw funds committed as initial margin for open positions or frozen for pending orders within the share group. Close positions and cancel orders in the group first to free up additional margin. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request Every request must carry an ECDSA signature and a nonce to prevent replay attacks. ```json theme={null} { "action": { "type": "unlockFromShareGroup", "groupId": 3, "coinId": 1, "amount": "1000" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859500000 } ``` ### Action parameters The ID of the source share group. Funds are deducted from this group's shared margin pool, subject to its withdrawable amount limit. The coin or currency ID to withdraw back to your chain-level balance. The amount to withdraw, expressed as a raw integer string (e.g. `"1000"`). Must be greater than zero and must not exceed the withdrawable amount available in the share group. Decimals are not accepted — use the coin's smallest unit. ## Response A successful withdrawal returns the updated balances for both the source share group and your chain-level ledger. The response type is `shareGroupFund`, consistent with other share group fund operations. ```json theme={null} { "status": "ok", "response": { "type": "shareGroupFund", "data": { "coinId": 1, "amount": 1000, "fromBalanceAfter": 0, "toBalanceAfter": 5000 } } } ``` The coin ID that was withdrawn. The amount moved from the share group back to your chain-level balance. The share group's margin pool balance after the withdrawal. Your chain-level balance for this coin after the withdrawal. ## Errors If the request cannot be fulfilled, the exchange still returns **HTTP 200** with `status: "ok"` — the rejection is reported inside `response.data`, where a server-assigned integer `errorCode` and a human-readable `errorMessage` are populated while the numeric amount and balance fields are set to `0`. Common causes include: * **Amount exceeds withdrawable balance** — position IM and order-frozen margin within the group constrain how much can be withdrawn. * **Account not in PORTFOLIO mode** — share groups are only active when your margin share type is `1`. * **Invalid `groupId`** — the specified share group must exist and be accessible to your account. * **Amount ≤ 0** — the `amount` field must be a positive integer string. ```json theme={null} { "status": "ok", "response": { "type": "shareGroupFund", "data": { "coinId": 0, "amount": 0, "fromBalanceAfter": 0, "toBalanceAfter": 0, "errorCode": 1, "errorMessage": "requested amount exceeds withdrawable balance in share group" } } } ``` To deposit funds into a share group from your chain-level balance, use [`lockIntoShareGroup`](/exchange/lock-into-share-group). To move funds from a share group into a market deployer instead, use [`transferShareGroupToMd`](/exchange/transfer-share-group-to-md). # Update Isolated Margin Source: https://docs.upsidemax.xyz/exchange/update-isolated-margin Adjust the margin allocated to an isolated position without closing it, moving funds between your cross margin pool and the isolated position. The `updateIsolatedMargin` action lets you add or remove collateral from an existing isolated position without closing or modifying the position itself. Funds are simply moved between your cross margin pool and the isolated position — your total account balance stays constant. This action uses full field names (`asset`, `isBuy`, `ntli`) rather than the compact single-character keys used by order and cancel actions. ## How margin transfer works **Adding margin** (`ntli > 0`): the specified amount is deducted from your cross available margin and credited to the isolated position's margin balance. The operation is limited by how much cross margin is currently available. **Removing margin** (`ntli < 0`): the specified amount is deducted from the isolated position's margin balance and returned to your cross pool. After the removal, the remaining isolated equity must still be sufficient to cover the position's initial margin requirement — you cannot remove margin to the point where your isolated position would be under-margined. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request All requests must include an ECDSA signature and a millisecond-precision nonce. ```json theme={null} { "action": { "type": "updateIsolatedMargin", "asset": 1, "isBuy": true, "ntli": 5000 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778858500000 } ``` ### Action fields Fixed value: `"updateIsolatedMargin"`. The contract ID of the position you want to adjust margin for. Identifies the position side in **hedge mode**: `true` = LONG position, `false` = SHORT position. Omit this field when your account is in ONE\_WAY position mode. Net transfer amount expressed in the collateral's minimum units. Positive values add margin to the isolated position; negative values remove margin. Must not be `0`. ## Response ### Success ```json theme={null} { "status": "ok", "response": { "type": "updateIsolatedMargin", "data": { "isoBefore": 10000, "isoAfter": 15000 } } } ``` ### Rejection ```json theme={null} { "status": "ok", "response": { "type": "updateIsolatedMargin", "data": { "isoBefore": 0, "isoAfter": 0, "errorCode": 4, "errorMessage": "no isolated position to adjust" } } } ``` Rejections still return HTTP 200 with `status: "ok"`. Check for the presence of `errorCode` in `data` to detect failures. ### Response fields The isolated position margin balance before the operation. Returns `0` on rejection. The isolated position margin balance after the operation. Returns `0` on rejection. Present only on rejection. Identifies the failure reason. Present only on rejection. Human-readable description of the failure. ## Error reference | `errorMessage` | Cause | | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `action.ntli must be non-zero` | `ntli` was set to `0`. | | `no isolated position to adjust` | No isolated position exists for the given `asset` and `isBuy` combination — or the position size is `0`, or the position is cross-margined. | | `no isolated collateral slot` | The isolated collateral slot could not be located for this position (abnormal state). | | `insufficient cross balance` | Your cross margin pool doesn't have enough available margin to fund the addition. | | `amount exceeds withdrawable cross margin (equity - position IM - order frozen)` | The addition would leave the cross account under-margined. | | `not enough isolated margin to remove` | The removal amount exceeds the current isolated margin balance. | | `removal would push effective leverage above position leverage (isolated equity below initial margin)` | After removal, the remaining isolated equity would fall below the initial margin requirement. | | `mark price not ready; cannot evaluate isolated margin removal` | The mark price is unavailable while removing margin; retry shortly. | | `mark price not ready for held position contractId=` | While adding margin, the mark price of another position the account holds isn't ready, so withdrawable margin can't be computed. | # Update Leverage Source: https://docs.upsidemax.xyz/exchange/update-leverage Adjust leverage for a specific contract. Reducing leverage triggers a margin check against existing positions to ensure requirements are met. The `updateLeverage` action sets the leverage multiplier for a specific contract. Leverage controls the initial margin (IM) requirement for new and existing positions on that contract — higher leverage means a smaller margin requirement per unit of notional value. Increasing leverage is always permitted. Decreasing leverage triggers a validation check: the exchange verifies that your existing position's margin can cover the higher IM implied by the lower leverage. If it cannot, the request is rejected and your leverage remains unchanged. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request All requests must include an ECDSA signature and a millisecond-precision nonce. ```json theme={null} { "action": { "type": "updateLeverage", "a": 1, "leverage": 20 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778858800000 } ``` ### Action fields Fixed value: `"updateLeverage"`. The contract ID to update leverage for. The new leverage value. Must be a positive integer and must not exceed the `tier0.maxLeverage` configured for the contract. Use the `configs` query (POST `/info`) to retrieve the `tier0.maxLeverage` for each contract before calling this action. ## Response ### Success ```json theme={null} { "status": "ok", "response": { "type": "updateLeverage", "data": { "leverageBefore": 10, "leverageAfter": 20 } } } ``` ### Rejection ```json theme={null} { "status": "ok", "response": { "type": "updateLeverage", "data": { "leverageBefore": 0, "leverageAfter": 0, "errorCode": 3, "errorMessage": "leverage exceeds tier0.maxLeverage=50" } } } ``` Rejections still return HTTP 200 with `status: "ok"`. Check for the presence of `errorCode` in `data` to detect failures. ### Response fields The leverage setting for the contract before the update. Returns `0` on rejection. The leverage setting for the contract after the update. Returns `0` on rejection. Present only on rejection. Identifies the failure reason. Present only on rejection. Human-readable description of the failure. ## Error reference | `errorMessage` | Cause | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `invalid leverage` | `leverage` was `0` or negative. | | `leverage exceeds tier0.maxLeverage=` | The requested leverage exceeds the maximum allowed for this contract, where `` is the configured limit. | | `leverage downgrade IM insufficient` | Decreasing leverage would raise the initial margin requirement beyond your available margin for existing positions. | # Update Margin Mode Source: https://docs.upsidemax.xyz/exchange/update-margin-mode Switch a contract between cross margin (shared pool) and isolated margin (per-position allocation), subject to having no open positions or orders. The `updateMarginMode` action switches the margin mode for a specific contract between **cross** and **isolated**. In cross margin mode, your positions share a common collateral pool and can draw on your full available balance. In isolated margin mode, each position is allocated a fixed amount of collateral that you control explicitly. Before switching modes, the contract must have no open positions and no active orders. If either condition is not met, the exchange rejects the request — close all positions and cancel all orders on the contract first. This action uses full field names (`asset`, `isCross`, `isHedge`) rather than the compact single-character keys used by order and cancel actions. This action is idempotent: setting the same margin mode that is already active returns a success response without modifying any state. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange ``` ## Request All requests must include an ECDSA signature and a millisecond-precision nonce. ```json theme={null} { "action": { "type": "updateMarginMode", "asset": 1, "isCross": false, "isHedge": false }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778859200000 } ``` ### Action fields Fixed value: `"updateMarginMode"`. The contract ID to update the margin mode for. `true` = switch to **cross** margin mode. `false` = switch to **isolated** margin mode. Optional. `false` selects **ONE\_WAY** (one-way) position mode. **Omit to keep the current position mode unchanged.** **HEDGE (two-way) position mode is currently unavailable.** Only ONE\_WAY is accepted, and any request passing `isHedge: true` is rejected. Isolated margin (`isCross: false`) works directly under ONE\_WAY — you do not need HEDGE for it. ## Response ### Success ```json theme={null} { "status": "ok", "response": { "type": "updateMarginMode", "data": { "marginModeBefore": 1, "marginModeAfter": 2, "positionModeBefore": 0, "positionModeAfter": 1 } } } ``` ### Rejection ```json theme={null} { "status": "ok", "response": { "type": "updateMarginMode", "data": { "marginModeBefore": 0, "marginModeAfter": 0, "positionModeBefore": 0, "positionModeAfter": 0, "errorCode": 11, "errorMessage": "cannot switch marginMode with open position" } } } ``` Rejections still return HTTP 200 with `status: "ok"`. Check for the presence of `errorCode` in `data` to detect failures. ### Response fields The margin mode for the contract before the update: `1` = cross, `2` = isolated. Returns `0` on rejection. The margin mode for the contract after the update: `1` = cross, `2` = isolated. Returns `0` on rejection. The position mode before the update: `0` = ONE\_WAY, `1` = HEDGE. Returns `0` on rejection. The position mode after the update: `0` = ONE\_WAY, `1` = HEDGE. Equals `positionModeBefore` when `isHedge` is omitted (unchanged). Returns `0` on rejection. Present only on rejection. Identifies the failure reason. Present only on rejection. Human-readable description of the failure. ## Error reference | `errorMessage` | Cause | | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `HEDGE position mode is temporarily disabled in this version (ONE_WAY only)` | You passed `isHedge: true`. HEDGE is currently unavailable; use ONE\_WAY. | | `invalid marginMode (expected 0/1/2)` | The margin mode value is not recognized — it must be `0`, `1`, or `2`. The message is a fixed string and does not interpolate your value. | | `cannot switch marginMode with open position` | The contract has at least one open position. Close all positions before switching. | | `cannot switch marginMode with open orders` | The contract has at least one active order. Cancel all orders before switching. | | `cannot switch positionMode with open position` | You passed `isHedge` but the contract still has an open position. Close it before switching position mode. | | `cannot switch positionMode with open orders` | You passed `isHedge` but the contract still has active orders. Cancel them before switching position mode. | | `MD deployer/liquidator account must stay CROSS (R17)` | Your account type — market deployer or liquidator — is restricted to cross margin and cannot switch to isolated. (`MD` in the server's message is short for market deployer.) | # Update Slippage Setting Source: https://docs.upsidemax.xyz/exchange/update-slippage-setting Set the market-order slippage cap for your account within a market deployer, in basis points. The cap bounds how far a market order may execute from the mark price. The `updateSlippageSetting` action sets the **market-order slippage cap** for your account within a market deployer, expressed in basis points. Market orders are priced from the mark price plus or minus this allowance, so the cap bounds how far a market order may execute from the current mark. The setting is scoped to the pair **(account, market deployer)** — it is not per-contract, and one call covers every contract under that deployer. The default is **1000 bps (10%)**. Your `accountId` is recovered from the signature. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/exchange Content-Type: application/json ``` ## Request ```json theme={null} { "action": { "type": "updateSlippageSetting", "marketDeployerId": 1, "marketSlippageBps": 500 }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778858900000 } ``` ### Action fields Fixed value: `"updateSlippageSetting"`. The market deployer this setting applies to. You must already be enrolled in it. The market-order slippage cap in basis points. Valid range is `(0, 10000]`, where `10000` equals 100%. Values outside this range are rejected. Defaults to `1000` (10%) when never set. ## Response ### Success ```json theme={null} { "status": "ok", "requestId": "req-144115188075856500", "response": { "type": "updateSlippageSetting", "data": { "marketSlippageBpsBefore": 1000, "marketSlippageBpsAfter": 500 } } } ``` ### Rejection Rejections still return HTTP 200 with `status: "ok"`. A rejected request carries a non-zero `errorCode` and an `errorMessage` **inside `response.data`**, and both `marketSlippageBpsBefore` and `marketSlippageBpsAfter` are `0`. A non-zero `errorCode` means the request was refused and **the slippage setting is unchanged**. Check for it on every call — `status: "ok"` alone does not mean the setting was applied. ### Response fields The cap in effect before this call. Returns `0` on rejection. The cap in effect after this call. Returns `0` on rejection. Present only on rejection. Identifies the failure reason. Present only on rejection. Human-readable description of the failure. ## Errors | Condition | Result | | ------------------------------------------ | ----------------------------------------------------------------------------- | | `marketSlippageBps` outside `(0, 10000]` | `400 BAD_REQUEST`, or a rejection carrying `errorCode` inside `response.data` | | Account not enrolled in `marketDeployerId` | Rejection carrying `errorCode` inside `response.data` | ## Reading the current value The cap in effect is returned as `marketSlippageBps` by the [`userAccount`](/info/user-account) query — the same field this action writes. Read it back there rather than caching the value locally. # Requests Source: https://docs.upsidemax.xyz/guide/authentication Learn how to sign POST /exchange requests using EIP-712 structured-data signatures over secp256k1. The server recovers your identity from the signature — no API keys needed. Every `POST /exchange` request must include a valid **EIP-712** structured-data signature (secp256k1 ECDSA) over the action payload. The server recovers your Ethereum address from the signature at request time — there are no API keys, bearer tokens, or sessions to manage. If the recovered address matches a registered account, the operation proceeds. If it doesn't, the server returns `SIGNATURE_INVALID`. ## Domain All signatures use a fixed EIP-712 domain: | Field | Value | | ------------------- | -------------------------------------------- | | `name` | `"Exchange"` | | `version` | `"1"` | | `chainId` | `9767` | | `verifyingContract` | `0x0000000000000000000000000000000000000000` | ## Two Signing Paths The signing struct is chosen by `action.type`: Trading and programmatic actions — `order`, `cancel`, `cancelByCloid`, `modify`, and most others. The entire canonical action is folded into a single `actionHash` carried by a generic `Agent(string source, bytes32 actionHash)` struct. Funds and permission actions — `registerAccount`, `approveAgent`, `revokeAgent`, `lockCollateral`, `unlockCollateral`, `transferBetweenDeployers`. Each has a field-level EIP-712 struct so a wallet can render human-readable values for review. Both paths share the same domain, digest formula, and request envelope. Only the `hashStruct` differs. If an action type is not in the typed list above, use the Agent path. ## Agent Path Used for **every action except the six [typed-path](#typed-path) ones** — all trading and programmatic calls, such as `order`, `cancel`, and `modify`, plus the margin, leverage, TP/SL, and share-group actions. As a rule of thumb: if `action.type` isn't in the typed list below, sign it via the Agent path. Construct the action as a plain JSON object. Every action has a `type` field that identifies the operation. ```json theme={null} { "type": "order", "orders": [ { "a": 1, "b": true, "p": "50", "s": "10", "r": false, "t": { "limit": { "tif": "Gtc" } } } ], "grouping": "na" } ``` Serialize the action to canonical JSON: **sort keys alphabetically**, use compact separators (`,` and `:`) with **no whitespace**, and exclude `undefined`/`null` fields. In Python this is exactly `json.dumps(action, sort_keys=True, separators=(",", ":"))`. ```text theme={null} Input: { "type": "order", "grouping": "na", "orders": [...] } Output: {"grouping":"na","orders":[...],"type":"order"} ``` Key ordering is crucial. The server re-serializes the action using the same rules; any mismatch produces a different hash and `SIGNATURE_INVALID`. Concatenate the canonical JSON bytes with the nonce encoded as a **big-endian 8-byte** value, then keccak256: ```python theme={null} from eth_utils import keccak canonical = json.dumps(action, sort_keys=True, separators=(",", ":")).encode() action_hash = keccak(canonical + int(nonce).to_bytes(8, "big")) ``` Wrap the `actionHash` in the generic `Agent` struct with `source = "b"`: ```python theme={null} hash_struct = keccak( keccak(b"Agent(string source,bytes32 actionHash)") + keccak(b"b") # keccak256 of the source string "b" + action_hash ) ``` See [Digest & signature](#digest--signature) below — this step is identical for both paths. ## Typed Path Used for `registerAccount`, `approveAgent`, `revokeAgent`, `lockCollateral`, `unlockCollateral`, and `transferBetweenDeployers`. Each action encodes its fields directly into a struct — there is **no** canonical JSON step. The `hashStruct` is `keccak256( typeHash() ‖ enc(field1) ‖ … ‖ enc(nonce) )`, where each field is encoded as: | Solidity type | Encoding | | ------------- | --------------------------- | | `string` | `keccak256(utf8Bytes)` | | `address` | 20 bytes, left-padded to 32 | | `uintN` | big-endian, 32 bytes | Struct definitions (the `nonce` is always the final `uint64` field): | Action | Struct | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `registerAccount` | `RegisterAccount(address address,uint64 nonce)` | | `approveAgent` | `ApproveAgent(address agentAddress,string agentName,uint64 validUntil,uint64 nonce)` | | `revokeAgent` | `RevokeAgent(address agentAddress,uint64 nonce)` | | `lockCollateral` | `LockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)` | | `unlockCollateral` | `UnlockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)` | | `transferBetweenDeployers` | `TransferBetweenDeployers(uint32 fromMarketDeployerId,uint32 toMarketDeployerId,uint32 coinId,string amount,uint64 nonce)` | ```python theme={null} # Example: registerAccount hash_struct = keccak( keccak(b"RegisterAccount(address address,uint64 nonce)") + (b"\x00" * 12 + bytes.fromhex(address[2:])) # address → 32 bytes + int(nonce).to_bytes(32, "big") # uint64 nonce → 32 bytes ) ``` ## Digest & Signature Both paths finish identically. Build the domain separator, form the EIP-712 digest, and sign it directly. ```text theme={null} domainSeparator = keccak256( typeHash(EIP712Domain) ‖ keccak256("Exchange") ‖ keccak256("1") ‖ uint256(9767) ‖ uint256(0) ) ``` where `typeHash(EIP712Domain) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`. ```text theme={null} digest = keccak256( 0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct ) // 32 bytes ``` Use **Ethereum keccak256**, not NIST SHA-3. The `digest` is already the final hash — sign it directly (`prehash = false`). Never hash it again. Sign the 32-byte digest with secp256k1 (RFC 6979 deterministic `k`). Set `v = 27 + recovery_bit`. ```python theme={null} from eth_account import Account sig = Account._sign_hash(digest, PRIVATE_KEY) r = "0x" + sig.r.to_bytes(32, "big").hex() s = "0x" + sig.s.to_bytes(32, "big").hex() v = sig.v if sig.v >= 27 else sig.v + 27 # 27 or 28 ``` ## Signature Format Include the signature in the `signature` field of the request envelope: ```json theme={null} { "r": "0x<64 lowercase hex characters>", "s": "0x<64 lowercase hex characters>", "v": 27 } ``` Both `r` and `s` are 32-byte values as `0x`-prefixed lowercase hex strings (66 characters including the prefix). `v` is `27` or `28`. ## Request Envelope The full `POST /exchange` body is the same for both paths: ```json theme={null} { "action": { "type": "order", "orders": [{ "a": 1, "b": true, "p": "50", "s": "10", "r": false, "t": { "limit": { "tif": "Gtc" } } }], "grouping": "na" }, "signature": { "r": "0x3b2a...", "s": "0x7cf1...", "v": 28 }, "nonce": 1778572951477 } ``` `vaultAddress` is an optional top-level field for **vault-proxy operations only** (not currently enabled on Devnet). **Delegated agent trading does not use it** — an agent signs with its own key, and the server routes to the master account from the recovered signer address, so trade actions ignore `vaultAddress` entirely. ## Browser wallet signing The typed path builds its digest over a domain whose `chainId` is **9767**. Signing with a raw private key — an SDK, `eth_account`, or a viem local account — needs nothing extra: keep the default. Browser extension wallets are different. When signing through `eth_signTypedData_v4`, they require `domain.chainId` to equal the wallet's currently active chain and refuse to sign otherwise. To support them, send the chain ID you actually signed with as an optional top-level envelope field: ```json theme={null} { "action": { "type": "registerAccount", "address": "0xabc...123" }, "signature": { "r": "0x...", "s": "0x...", "v": 28 }, "nonce": 1778572951477, "signatureChainId": "0x2627" } ``` Hex string with a `0x` prefix — the `domain.chainId` your client used to sign the typed struct. Defaults to `0x2627` (9767) when omitted. Rules: * **Typed path only.** The Agent path — `order`, `cancel`, and the rest — always uses 9767 and ignores this field. * **Not part of the signature.** It sits at the top level beside `action`, `signature`, and `nonce`, and never enters the canonical JSON. The server rebuilds the domain with it before verifying; a value that disagrees with what you actually signed recovers the wrong address and returns `401 SIGNATURE_INVALID`. * Omitting it makes the server rebuild the domain with 9767, so existing clients need no change. * A malformed value — missing the `0x` prefix, containing non-hex characters, or longer than 64 bits — returns `400 BAD_REQUEST`. ## Complete Signing Helper A reusable helper that implements both paths. This is the reference implementation — copy, paste, and run. ```python Python theme={null} import json, time, requests from eth_account import Account from eth_utils import keccak BASE_URL = "https://dev.upsidemax.xyz" # EIP-712 domain: Exchange / v1 / chainId 9767 / verifyingContract 0x0 _CHAIN_ID, _NAME, _VER, _SOURCE = 9767, "Exchange", "1", "b" def _u(v): return int(v).to_bytes(32, "big") def _addr(s): return b"\x00" * 12 + bytes.fromhex(s[2:] if s[:2].lower() == "0x" else s) def _s(v): return keccak(str(v).encode()) _DOMAIN = keccak( keccak(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + keccak(_NAME.encode()) + keccak(_VER.encode()) + _u(_CHAIN_ID) + b"\x00" * 32) # Funds / permission actions each sign a field-level typed struct; every other action # uses the Agent path. Each struct's field names match the action's JSON keys, so the # encoding is derived straight from the type string — no per-action code to maintain. _TYPED = { "registerAccount": "RegisterAccount(address address,uint64 nonce)", "approveAgent": "ApproveAgent(address agentAddress,string agentName,uint64 validUntil,uint64 nonce)", "revokeAgent": "RevokeAgent(address agentAddress,uint64 nonce)", "lockCollateral": "LockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)", "unlockCollateral": "UnlockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)", "transferBetweenDeployers": "TransferBetweenDeployers(uint32 fromMarketDeployerId,uint32 toMarketDeployerId,uint32 coinId,string amount,uint64 nonce)", } # Optional typed fields MUST still appear in the wire JSON — the server reads them by # name to recompute the digest. Inject their defaults before signing so the signed # struct and the sent JSON match; otherwise the server recovers a different address → 401. _TYPED_DEFAULTS = {"approveAgent": {"agentName": "", "validUntil": 0}} def _field(sol_type, value): # EIP-712 field encoding if sol_type == "address": return _addr(value) # 20 bytes, left-padded to 32 if sol_type == "string": return _s(value) # keccak256(utf8) return _u(value) # uintN -> 32-byte big-endian def _typed_struct(type_str, action, nonce): fields = [f.split() for f in type_str[type_str.index("(") + 1 : -1].split(",")] enc = [_field(t, nonce if name == "nonce" else action[name]) for t, name in fields] return keccak(keccak(type_str.encode()) + b"".join(enc)) def eip712_digest(action, nonce): type_str = _TYPED.get(action["type"]) if type_str: # typed path — field-level EIP-712 encoding for k, v in _TYPED_DEFAULTS.get(action["type"], {}).items(): action.setdefault(k, v) # fill optional fields so the wire JSON matches the digest struct = _typed_struct(type_str, action, nonce) else: # Agent path — canonical JSON folded into an actionHash canonical = json.dumps(action, sort_keys=True, separators=(",", ":")).encode() action_hash = keccak(canonical + int(nonce).to_bytes(8, "big")) struct = keccak(keccak(b"Agent(string source,bytes32 actionHash)") + keccak(_SOURCE.encode()) + action_hash) return keccak(b"\x19\x01" + _DOMAIN + struct) _last_nonce = 0 def _next_nonce(): # strictly increasing — avoids sub-ms nonce collisions global _last_nonce _last_nonce = max(_last_nonce + 1, int(time.time() * 1000)) return _last_nonce def sign_and_send(action, private_key): """Sign an action with EIP-712 and POST it to /exchange.""" nonce = _next_nonce() sig = Account._sign_hash(eip712_digest(action, nonce), private_key) envelope = { "action": action, "signature": { "r": "0x" + sig.r.to_bytes(32, "big").hex(), "s": "0x" + sig.s.to_bytes(32, "big").hex(), "v": sig.v if sig.v >= 27 else sig.v + 27, }, "nonce": nonce, } resp = requests.post(f"{BASE_URL}/exchange", json=envelope, timeout=15) return resp.json() ``` ```typescript TypeScript theme={null} // npm i ethers import { keccak256, getBytes, concat, toUtf8Bytes, zeroPadValue, toBeArray, hexlify, SigningKey, } from "ethers"; const BASE_URL = "https://dev.upsidemax.xyz"; // EIP-712 domain: Exchange / v1 / chainId 9767 / verifyingContract 0x0 const CHAIN_ID = 9767n, NAME = "Exchange", VER = "1", SOURCE = "b"; const kb = (d: Uint8Array) => getBytes(keccak256(d)); const u256 = (v: bigint | number) => getBytes(zeroPadValue(toBeArray(BigInt(v)), 32)); const be8 = (v: bigint | number) => getBytes(zeroPadValue(toBeArray(BigInt(v)), 8)); const a32 = (s: string) => getBytes(zeroPadValue(s, 32)); const hs = (s: string) => kb(toUtf8Bytes(s)); const cat = (a: Uint8Array[]) => getBytes(concat(a)); const DOMAIN = kb(cat([ hs("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), hs(NAME), hs(VER), u256(CHAIN_ID), u256(0), ])); // Funds / permission actions each sign a field-level typed struct; every other action // uses the Agent path. Each struct's field names match the action's JSON keys, so the // encoding is derived straight from the type string — no per-action code to maintain. const TYPED: Record = { registerAccount: "RegisterAccount(address address,uint64 nonce)", approveAgent: "ApproveAgent(address agentAddress,string agentName,uint64 validUntil,uint64 nonce)", revokeAgent: "RevokeAgent(address agentAddress,uint64 nonce)", lockCollateral: "LockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)", unlockCollateral: "UnlockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)", transferBetweenDeployers: "TransferBetweenDeployers(uint32 fromMarketDeployerId,uint32 toMarketDeployerId,uint32 coinId,string amount,uint64 nonce)", }; // Optional typed fields must still appear in the wire JSON — inject defaults before signing. const TYPED_DEFAULTS: Record> = { approveAgent: { agentName: "", validUntil: 0 } }; const encodeField = (solType: string, value: any): Uint8Array => // EIP-712 field encoding solType === "address" ? a32(value) // 20 bytes, left-padded to 32 : solType === "string" ? hs(String(value)) // keccak256(utf8) : u256(value); // uintN -> 32-byte big-endian function typedStruct(typeStr: string, action: any, nonce: bigint): Uint8Array { const fields = typeStr.slice(typeStr.indexOf("(") + 1, -1).split(",").map(f => f.split(" ")); const enc = fields.map(([t, name]) => encodeField(t, name === "nonce" ? nonce : action[name])); return kb(cat([hs(typeStr), ...enc])); } // Canonical JSON: keys sorted at every level, compact separators (Agent path only). const canon = (o: any): string => Array.isArray(o) ? "[" + o.map(canon).join(",") + "]" : o && typeof o === "object" ? "{" + Object.keys(o).sort().map(k => JSON.stringify(k) + ":" + canon(o[k])).join(",") + "}" : JSON.stringify(o); function eip712Digest(action: any, nonce: bigint): Uint8Array { const typeStr = TYPED[action.type]; let struct: Uint8Array; if (typeStr) { // typed path — field-level EIP-712 encoding for (const [k, v] of Object.entries(TYPED_DEFAULTS[action.type] ?? {})) if (action[k] === undefined) action[k] = v; // fill optional fields so wire JSON matches the digest struct = typedStruct(typeStr, action, nonce); } else { // Agent path — canonical JSON folded into an actionHash const actionHash = kb(cat([toUtf8Bytes(canon(action)), be8(nonce)])); struct = kb(cat([hs("Agent(string source,bytes32 actionHash)"), hs(SOURCE), actionHash])); } return kb(cat([Uint8Array.from([0x19, 0x01]), DOMAIN, struct])); } let lastNonce = 0; const nextNonce = () => (lastNonce = Math.max(lastNonce + 1, Date.now())); // strictly increasing export async function signAndSend(action: any, privateKey: string) { const nonce = BigInt(nextNonce()); const sig = new SigningKey(privateKey).sign(hexlify(eip712Digest(action, nonce))); const envelope = { action, signature: { r: sig.r, s: sig.s, v: sig.v }, // v is 27 or 28 nonce: Number(nonce), }; const resp = await fetch(`${BASE_URL}/exchange`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(envelope), }); return resp.json(); } ``` ## Common Errors | Error | Typical Cause | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SIGNATURE_INVALID` | Recovered address doesn't match the registered account. Common causes: unsorted JSON keys or whitespace in canonical JSON (Agent path), wrong domain constants, wrong nonce byte-width (8 bytes in the Agent `actionHash` vs 32 bytes as a typed field), double-hashing the digest, or the wrong private key. | | `SIGNATURE_INVALID` (typed field omitted) | A typed action left an optional field out of the wire JSON (e.g. `approveAgent` without `agentName` / `validUntil`). The server reads every declared field by name, so each must be present — send `""` / `0`. The helper's `_TYPED_DEFAULTS` injects these before signing. | | `SIGNATURE_INVALID` (wrong `v`) | `v` was not normalized to the `27`/`28` range. Use `v = sig.v if sig.v >= 27 else sig.v + 27`. | | `SIGNATURE_INVALID` (wrong path) | A funds/permission action was signed via the Agent path (or vice-versa). Match the action to the correct path above. | When you receive `SIGNATURE_INVALID`, the error message includes the recovered address and the expected address, which helps pinpoint which key you actually signed with. # Data Types Source: https://docs.upsidemax.xyz/guide/data-types Reference for numeric strings, timestamps, address format, contract IDs, client order IDs, and the short key convention used in order payloads. UpsideMAX uses a small set of consistent conventions for data representation throughout the API. Understanding these conventions upfront will save you debugging time — in particular, note that prices and sizes are **strings**, not numbers, and that raw integer values in responses must be scaled before display. ## Numeric Values: Strings to Avoid Float Precision Loss All prices, quantities, and monetary amounts are represented as **decimal strings** rather than JSON numbers. This avoids the floating-point precision loss that occurs when JSON parsers store large or fractional numbers as IEEE 754 doubles. ```json theme={null} { "p": "85000", "s": "10" } ``` Always treat these fields as strings when reading or writing. Pass them through to your exchange layer without converting to float. ## Raw Integers and Scale Factors Numeric values in API responses are **raw integers**. To obtain the human-readable display value, divide by 10 raised to the scale exponent for that field. Use the `configs` endpoint to retrieve `priceScale` and `qtyScale` for each contract. | Raw value | Scale | Display value | | ---------- | ---------------- | ------------- | | `"850000"` | `priceScale = 2` | `8500.00` | | `"1000"` | `qtyScale = 1` | `100.0` | ```python theme={null} def to_display(raw: str, scale: int) -> float: return int(raw) / (10 ** scale) price_display = to_display("850000", 2) # priceScale = 2 → 8500.0 ``` Each contract's `priceScale` and `qtyScale` are published by the `configs` endpoint (on Devnet, contracts use `priceScale = 2` and `qtyScale = 4`). Always read scale values from `configs` rather than hardcoding them — they differ per contract and can change when new contracts are listed. ## Timestamps All timestamps are **int64 Unix milliseconds** — the number of milliseconds elapsed since 1970-01-01T00:00:00Z. Timestamps are represented as JSON integers (not strings). ```json theme={null} { "time": 1754450974231 } ``` ```python theme={null} import datetime dt = datetime.datetime.fromtimestamp(1754450974231 / 1000, datetime.timezone.utc) # → 2025-08-05 18:29:34.231000+00:00 ``` ## Ethereum Addresses Addresses follow the standard Ethereum format: `0x` prefix followed by 40 lowercase hexadecimal characters (42 characters total). ```text theme={null} "address": "0x7b94aeea275c43ab537a8cd55f7551688c6521ad" ``` Always send addresses in lowercase. The server compares addresses case-insensitively, but using lowercase consistently avoids subtle bugs in signature verification and logging. ## Contract (Asset) IDs Each perpetual contract has an integer ID stored in the `a` field of order, cancel, and modify actions. On Devnet the available contracts are BTC-USDC, ETH-USDC, and SOL-USDC. Call `configs` to enumerate all contracts and their IDs in any environment — IDs are assigned by the server, so never hardcode them. ```json theme={null} { "a": 1 } // asset = a contract ID from configs ``` ## Client Order IDs (cloid) A client order ID is an **int64 decimal string** that you assign when placing an order. It must be a unique positive integer. You can use it later to cancel or query orders without needing the server-assigned `oid`. ```json theme={null} { "c": "1778763737044" } ``` A Unix millisecond timestamp works well as a client order ID for most use cases — it is unique, monotonically increasing, and easy to generate without a counter. ## Short Key Convention (Order Payloads) Order-related actions use single-letter compact keys to reduce payload size. The mapping is: | Short key | Full name | Type | Description | | --------- | --------------- | ------- | ------------------------------------------------- | | `a` | `asset` | integer | Contract ID | | `b` | `isBuy` | boolean | `true` = buy, `false` = sell | | `p` | `price` | string | Limit price | | `s` | `size` | string | Order quantity | | `r` | `reduceOnly` | boolean | If `true`, order can only reduce an open position | | `t` | `orderType` | object | Order type: `{"limit": {"tif": "Gtc"}}` | | `c` | `clientOrderId` | string | Optional client-assigned int64 ID | Example order object using short keys: ```json theme={null} { "a": 1, "b": true, "p": "50", "s": "10", "r": false, "t": { "limit": { "tif": "Gtc" } }, "c": "1778763737044" } ``` ## Full Key Convention (Margin and Mode Actions) Margin and account mode actions use full field names rather than single-letter abbreviations: | Field | Type | Description | | --------- | ------- | ------------------------------------------------ | | `asset` | integer | Contract ID | | `isBuy` | boolean | Side of the position to adjust | | `isCross` | boolean | `true` = cross margin, `false` = isolated margin | | `ntli` | string | New total leverage or isolated margin amount | Example leverage update using full keys: ```json theme={null} { "type": "updateLeverage", "asset": 1, "isCross": true, "leverage": 10 } ``` # Error Codes Source: https://docs.upsidemax.xyz/guide/error-codes Reference for HTTP-level error codes and business-level error messages returned by the UpsideMAX API, with common troubleshooting guidance. UpsideMAX returns errors at two levels: **HTTP-level errors** that appear in the top-level `status: "error"` response, and **business-level errors** that appear inside `response.data.statuses[]` even when `status` is `"ok"`. Both levels are documented here. ## HTTP-Level Error Codes These errors are returned with a non-200 HTTP status code when the server rejects the request before or during processing. The response body contains `status: "error"`, a machine-readable `code`, and a human-readable `message`. | Error Code | HTTP Status | Description | | ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BAD_REQUEST` | 400 | Malformed request — missing required fields, invalid JSON syntax, or unexpected field types | | `UNKNOWN_ACTION` | 400 | The `action.type` value is not recognized by the server | | `SIGNATURE_INVALID` | 401 | ECDSA signature recovery failed, or the recovered address does not match. Usual causes: a typed action omitted an optional field from the wire JSON so your digest differs from the server's; the Agent path's canonical JSON carried whitespace or unsorted keys; `v` was not `27` / `28`; or the domain was wrong (`name` must be `Exchange`, `version` `1`, and `chainId` `9767` unless the envelope carries a matching `signatureChainId`) | | `ACCOUNT_ALREADY_EXISTS` | 409 | The address is already registered; `registerAccount` cannot be called twice for the same address | | `INVITE_CODE_INVALID` | 403 | The alpha test invitation code is invalid (`NOT_FOUND`) or has already been used (`ALREADY_USED`) | | `BATCH_TOO_LARGE` | 400 | The `orders[]` or `cancels[]` array contains more than 10 items | | `RATE_LIMITED` | 429 | Too many requests from this IP or account; back off and retry | | `DOWNSTREAM_TIMEOUT` | 504 | The backend matching engine did not respond within 5 seconds; the operation was not applied | | `INTERNAL_ERROR` | 500 | Unexpected server-side error; contact UpsideMAX support with the `requestId` | ## Business-Level Errors These errors appear inside `response.data.statuses[]` when the request envelope is valid and authenticated, but the matching engine or risk engine rejects one or more operations within the batch. The outer `status` is `"ok"` and the HTTP status is 200. | Message | Cause | | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `signer has no registered account` | The signing address has not been registered. Call `registerAccount` before placing orders. | | `orderId not found` | The order has already been cancelled, fully filled, or was never created with that ID. | | `size must be positive` | The order quantity is zero or negative. | | `price exceeds tick range (maxTicks x tickSize)` | The order price is outside the contract's permitted tick range. The message is a fixed string and does not interpolate your values — read `maxTicks` and `tickSize` from [`configs`](/info/configs). | | `action.orders size N exceeds max 10` | The `orders[]` array contains more than 10 items. Split the batch into multiple requests. | | `timeout` | The backend processing timed out. The operation was not applied. It is safe to retry. | | `share margin group is FROZEN` | The account is in portfolio margin mode and the share group is frozen. Only `reduceOnly` orders are accepted while the group is frozen. | ## Troubleshooting This is the most common error when integrating for the first time. Work through this checklist: 1. **Right signing path** — Funds/permission actions (`registerAccount`, `approveAgent`, `revokeAgent`, `lockCollateral`, `unlockCollateral`, `transferBetweenDeployers`) use the EIP-712 **typed** struct; everything else uses the **Agent** path. Signing an action via the wrong path always fails. See [Authentication](/guide/authentication). 2. **Typed path — include every struct field in the wire JSON** — For a typed action, every field the struct declares must be present in the request JSON, even the business-optional ones. `approveAgent` in particular must carry `agentName` (`""` when unset) and `validUntil` (`0` for no expiry); if you omit them, the server reads the JSON as-sent and computes a different digest than the one you signed, so recovery fails. Inject the defaults into the action **before** signing. See [Authentication](/guide/authentication). 3. **Canonical JSON (Agent path)** — Keys must be sorted alphabetically with compact separators and no whitespace: `json.dumps(action, sort_keys=True, separators=(",", ":"))`. A single extra space produces a different `actionHash`. 4. **Correct domain & nonce encoding** — Use the fixed domain (`name="Exchange"`, `version="1"`, `chainId=9767`, `verifyingContract=0x0`). The nonce is a big-endian **8-byte** suffix inside the Agent `actionHash`, but a **32-byte** `uint64` field in a typed struct — mixing these up is a common cause. 5. **No double-hashing** — The EIP-712 `digest` is already the final hash. Pass the 32-byte digest directly to the signing function (`prehash = false`); do not hash again. 6. **Correct `v` value** — Ensure `v = sig.v if sig.v >= 27 else sig.v + 27`. Some libraries return `v` as 0 or 1; you must add 27. 7. **Right private key** — The `SIGNATURE_INVALID` message includes the recovered address. If it doesn't match your expected address, you are signing with a different private key than the one whose address you registered. This is a business-level rejection, not an HTTP error. The request was correctly signed and delivered to the matching engine, but the engine rejected the order. Check `response.data.statuses[0].error` for the rejection reason. Common causes: * **`size must be positive`** — You passed `"s": "0"` or a negative string. * **`price exceeds tick range (maxTicks x tickSize)`** — The price is outside the contract's permitted tick range; read `maxTicks` and `tickSize` from `configs`. * **`signer has no registered account`** — You need to call `registerAccount` first. * **`orderId not found`** — The `oid` you tried to cancel doesn't exist or was already filled. Always iterate over the entire `statuses` array when placing batch orders — some items in the batch may succeed while others fail. A `DOWNSTREAM_TIMEOUT` (HTTP 504) means the backend chain node did not return a result within the server's 5-second window. The request's outcome is **unknown** — reconcile before retrying rather than assuming it was dropped. **What to do:** 1. Wait 1–2 seconds. 2. Query `POST /info` with [`userOrders`](/info/user-orders) — and [`orderHistory`](/info/order-history) if the order may already have terminated — to establish the actual state. 3. Only if the order is genuinely absent, retry with a fresh nonce. 4. If you continue to see timeouts, check the UpsideMAX status page or contact support with your `requestId`. This means the address has already been registered in this environment. You do not need to register again — proceed directly to placing orders. If you are generating a new keypair each time in your test scripts, each new address will need its own `registerAccount` call. Consider persisting your test wallet's private key between runs. Some environments require a valid alpha test invitation code to register. Make sure you: 1. Place the `inviteCode` field at the **top level** of the envelope — not inside the `action` object. 2. Do **not** include `inviteCode` inside the signed message (it is outside `action`). 3. Check whether the code was already used (`ALREADY_USED`) or is simply wrong/expired (`NOT_FOUND`). Contact the UpsideMAX team to obtain a valid alpha test invitation code for restricted environments. # Developer Hub Source: https://docs.upsidemax.xyz/guide/introduction UpsideMAX API provides signed writes, unsigned reads, and real-time WebSocket streams for developers. UpsideMAX is a perpetuals exchange built for programmatic trading. Instead of a traditional REST API with per-resource URLs, every write operation flows through a single `POST /exchange` endpoint — routed by the `action.type` field in the request body. Reads flow through `POST /info`, routed by the `type` field. Real-time market and account updates are delivered over WebSocket. This action-based design keeps the interface minimal and consistent across all trading operations. ## API Channels The UpsideMAX API exposes three channels: | Channel | Endpoint | Auth Required | | ------------- | ---------------- | --------------------- | | **Write** | `POST /exchange` | Yes — ECDSA signature | | **Read** | `POST /info` | No | | **Streaming** | WebSocket | No | ### Action-Based Routing Unlike REST APIs that use distinct URLs per resource, UpsideMAX routes all requests by a type field in the JSON body: * **`POST /exchange`** — All state-changing operations (place order, cancel, register account, etc.) share this single URL. The server dispatches by `action.type`. * **`POST /info`** — All read queries share this URL. The server dispatches by the top-level `type` field. This means you always `POST` to the same URL and change only the JSON payload to perform different operations. ## Environments Devnet is for testing only. Funds and accounts are not shared across environments. The Mainnet URL is not yet public — contact the UpsideMAX team to get access. | Environment | REST Base URL | WebSocket URL | | | ----------- | ---------------------------- | ---------------------------- | - | | **Devnet** | `https://dev.upsidemax.xyz` | `wss://dev.upsidemax.xyz/ws` | | | **Mainnet** | TBD — contact UpsideMAX team | TBD — contact UpsideMAX team | | **Registration on Devnet requires an alpha test invitation code.** `registerAccount` must include a valid single-use `inviteCode` at the envelope top level (not part of the signature), or registration is rejected with `inviteCode required`. Request one from the UpsideMAX team. After a successful registration, Devnet automatically airdrops **10,000 USDC** of test funds to your account within about 10 seconds — Devnet-only tokens with no real value. USDC is the only asset airdropped. ## Devnet Constraints * **Contracts:** BTC-USDC, ETH-USDC, SOL-USDC (perpetuals, `base`-`quote`). `configs` may also list internal staging contracts — filter by `name` / `quoteCoinId`, never hardcode IDs. * **Quote / settlement coin:** USDC * **Base coins:** BTC, ETH, SOL * **Prices & sizes (`p`, `s`):** Raw integer strings — scale each with the contract's `priceScale` / `qtyScale` from `configs` (currently `priceScale = 2`, `qtyScale = 4`, `tickSize = 1`) Contract and coin IDs are assigned by the server — always enumerate them from the [`configs`](/info/configs) endpoint rather than hardcoding. `configs` is the authoritative source for tick sizes, scales, and leverage tiers per contract. ## Explore the API Register an account, place a limit order, and cancel it — all in under 50 lines of Python. Learn how to sign `POST /exchange` requests using ECDSA secp256k1. Explore all write actions: orders, cancels, account registration, and more. Query orders, balances, market data, and configuration via `POST /info`. Subscribe to real-time order book updates, trades, and account events. Reference for HTTP-level and business-level error codes with troubleshooting tips. # Nonce Source: https://docs.upsidemax.xyz/guide/nonce Understand the nonce field in POST /exchange requests — how to generate it, uniqueness rules, and how it protects against replay attacks. Every `POST /exchange` request includes a `nonce` — an integer that the server uses to prevent replay attacks. Because the nonce is embedded in the signed message, an attacker who intercepts a valid signed request cannot resubmit it: the server recognises and rejects any nonce it has already seen from that signer. ## What Is the Nonce? The `nonce` is an **int64** (64-bit signed integer) included at the top level of every `POST /exchange` envelope alongside `action` and `signature`. It is part of the signed message, so any tampering with the nonce value will invalidate the signature. ```json theme={null} { "action": { "type": "order", "..." }, "signature": { "r": "0x...", "s": "0x...", "v": 27 }, "nonce": 1778572951477 } ``` ## Recommended Value Use the **current Unix timestamp in milliseconds**. This is a natural choice because it is: * **Monotonically increasing** — subsequent requests naturally have higher nonces. * **Unique** — millisecond resolution makes accidental collisions unlikely in normal usage. * **Self-documenting** — you can decode the approximate time of any request from its nonce. Millisecond timestamps are sufficient for most use cases. If you need to fire more than one request per millisecond, increment the nonce by 1 for each additional request within the same millisecond. ## Code Examples ```python Python theme={null} import time nonce = int(time.time() * 1000) # e.g. 1778572951477 ``` ```typescript TypeScript theme={null} const nonce = Date.now(); // e.g. 1778572951477 ``` ```go Go theme={null} import "time" nonce := time.Now().UnixMilli() // e.g. 1778572951477 ``` ## Rules | Rule | Details | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Must be unique per signer** | The server rejects any nonce that has already been used by the same signing address. | | **Included in the signed message** | The nonce is bound into the EIP-712 signature — as a big-endian 8-byte suffix in the Agent-path `actionHash`, or as the final `uint64` field of a typed struct — making it tamper-evident. | | **int64** | Valid range: 1 – 9,223,372,036,854,775,807. A Unix millisecond timestamp fits comfortably within this range for centuries. | ## Rapid Succession Requests If you need to submit multiple requests faster than one per millisecond, read the current timestamp once and increment: ```python theme={null} import time base_nonce = int(time.time() * 1000) for i, action in enumerate(batch_of_actions): nonce = base_nonce + i # guaranteed unique # ... sign and send with this nonce ``` Do not reuse a nonce under any circumstances — even after a failed request. The server may have partially processed the request before returning an error, and reusing the nonce will cause the retry to be rejected. # Quick Start Source: https://docs.upsidemax.xyz/guide/quickstart One runnable script (Python or TypeScript) for your first UpsideMAX trade on Devnet — register with an alpha test invitation code, receive the test-fund airdrop, lock collateral, then place and cancel a limit order. One script takes you through a full trade on Devnet. It registers your wallet (with an alpha test invitation code), waits for the automatic test-fund airdrop, locks collateral as margin, then places and cancels a limit order. Every `POST /exchange` call is signed with EIP-712; read queries to `POST /info` need no signature. Devnet offers **BTC-USDC**, **ETH-USDC**, and **SOL-USDC** USDC-settled perpetuals. `configs` may also list internal staging contracts, so pick contracts by `name` / `quoteCoinId` — don't assume `contractId` 1/2/3 map to them. ## Prerequisites ```bash Python theme={null} pip install eth-account eth-utils requests # Python 3.8+ ``` ```bash TypeScript theme={null} npm i ethers # Node 18+ (for global fetch) ``` **Get an alpha test invitation code first.** Devnet registration requires a valid single-use `inviteCode` — request one from the UpsideMAX team and set it as `INVITE_CODE` below. After you register, Devnet airdrops **10,000 USDC** of Devnet-only funds (USDC only) to your account within \~10 seconds. ## The script Set `INVITE_CODE`, then run. Only `registerAccount` and `lockCollateral` use the EIP-712 typed path here — the full typed set is in [Authentication](/guide/authentication). ```python Python theme={null} import json, time, requests from eth_account import Account from eth_utils import keccak BASE_URL = "https://dev.upsidemax.xyz" INVITE_CODE = "" # from the UpsideMAX team (Devnet only) USDC, MARKET_DEPLOYER, CONTRACT = 1, 1, 1 # coinId / market deployer / contract, from configs wallet = Account.create() PRIVATE_KEY, ADDRESS = wallet.key, wallet.address.lower() # --- EIP-712 signer (domain: Exchange / v1 / chainId 9767 / verifyingContract 0x0) --- _u = lambda v: int(v).to_bytes(32, "big") _DOMAIN = keccak(keccak(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + keccak(b"Exchange") + keccak(b"1") + _u(9767) + b"\x00" * 32) _TYPED = { # only the typed actions this script uses; full set → Authentication "registerAccount": lambda a, n: [keccak(b"RegisterAccount(address address,uint64 nonce)"), b"\x00" * 12 + bytes.fromhex(a["address"][2:]), _u(n)], "lockCollateral": lambda a, n: [keccak(b"LockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)"), _u(a["marketDeployerId"]), _u(a["coinId"]), keccak(str(a["amount"]).encode()), _u(n)], } _last_nonce = 0 def _next_nonce(): # strictly increasing — avoids sub-ms nonce collisions global _last_nonce _last_nonce = max(_last_nonce + 1, int(time.time() * 1000)) return _last_nonce def sign_and_send(action, extra=None): n = _next_nonce() if action["type"] in _TYPED: struct = keccak(b"".join(_TYPED[action["type"]](action, n))) else: # Agent path h = keccak(json.dumps(action, sort_keys=True, separators=(",", ":")).encode() + n.to_bytes(8, "big")) struct = keccak(keccak(b"Agent(string source,bytes32 actionHash)") + keccak(b"b") + h) sig = Account._sign_hash(keccak(b"\x19\x01" + _DOMAIN + struct), PRIVATE_KEY) return requests.post(f"{BASE_URL}/exchange", timeout=15, json={ "action": action, "nonce": n, **(extra or {}), "signature": {"r": "0x" + sig.r.to_bytes(32, "big").hex(), "s": "0x" + sig.s.to_bytes(32, "big").hex(), "v": sig.v if sig.v >= 27 else sig.v + 27}}).json() def info(q): return requests.post(f"{BASE_URL}/info", json=q, timeout=10).json() # 1. Register — inviteCode is an unsigned, top-level field acct = sign_and_send({"type": "registerAccount", "address": ADDRESS}, {"inviteCode": INVITE_CODE})["response"]["accountId"] # 2. Wait for the 10,000 USDC airdrop to become usable margin. Devnet credits it straight to # the market-deployer margin pool; if it instead arrives as a chain-level balance, move # it in with lockCollateral. Polling marginAvailableForOrder works for either path. def margin_avail(): return int(info({"type": "userAccount", "accountId": str(acct), "marketDeployerId": MARKET_DEPLOYER}).get("marginAvailableForOrder", "0")) def chain_usdc(): bals = info({"type": "userAccount", "accountId": str(acct), "marketDeployerId": 0}).get("chainBalances", []) return next((int(c["amount"]) for c in bals if c["coinId"] == USDC), 0) while margin_avail() == 0: if chain_usdc() > 0: sign_and_send({"type": "lockCollateral", "marketDeployerId": MARKET_DEPLOYER, "coinId": USDC, "amount": str(chain_usdc())}) time.sleep(1) # 3. Place a limit buy — raw price/size, scaled by the contract's priceScale/qtyScale. place = sign_and_send({"type": "order", "grouping": "na", "orders": [{"a": CONTRACT, "b": True, "p": "1000000", "s": "100", "r": False, "t": {"limit": {"tif": "Gtc"}}}]}) # oid comes back inline as statuses[].resting.oid; if the server accepts asynchronously # and omits it, look it up from userOrders instead. statuses = place.get("response", {}).get("data", {}).get("statuses") or [] if statuses and statuses[0].get("resting"): oid = statuses[0]["resting"]["oid"] else: orders = info({"type": "userOrders", "accountId": str(acct), "marketDeployerId": MARKET_DEPLOYER, "contractId": CONTRACT})["orders"] oid = int(orders[-1]["id"]) # 4. Verify, then cancel print(info({"type": "userOrders", "accountId": str(acct), "marketDeployerId": MARKET_DEPLOYER})) print(sign_and_send({"type": "cancel", "cancels": [{"a": CONTRACT, "o": oid}]})) ``` ```typescript TypeScript theme={null} import { keccak256, getBytes, concat, toUtf8Bytes, zeroPadValue, toBeArray, hexlify, SigningKey, Wallet } from "ethers"; const BASE_URL = "https://dev.upsidemax.xyz"; const INVITE_CODE = ""; // from the UpsideMAX team (Devnet only) const USDC = 1, MARKET_DEPLOYER = 1, CONTRACT = 1; // coinId / market deployer / contract, from configs const wallet = Wallet.createRandom(); const PRIVATE_KEY = wallet.privateKey, ADDRESS = wallet.address.toLowerCase(); // --- EIP-712 signer (domain: Exchange / v1 / chainId 9767 / verifyingContract 0x0) --- const kb = (d: Uint8Array) => getBytes(keccak256(d)); const u = (v: bigint | number) => getBytes(zeroPadValue(toBeArray(BigInt(v)), 32)); const hs = (s: string) => kb(toUtf8Bytes(s)); const cat = (a: Uint8Array[]) => getBytes(concat(a)); const DOMAIN = kb(cat([hs("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), hs("Exchange"), hs("1"), u(9767), u(0)])); const TYPED: Record Uint8Array[]> = { // full set → Authentication registerAccount: (a, n) => [hs("RegisterAccount(address address,uint64 nonce)"), getBytes(zeroPadValue(a.address, 32)), u(n)], lockCollateral: (a, n) => [hs("LockCollateral(uint32 marketDeployerId,uint32 coinId,string amount,uint64 nonce)"), u(a.marketDeployerId), u(a.coinId), hs(String(a.amount)), u(n)], }; const canon = (o: any): string => Array.isArray(o) ? "[" + o.map(canon).join(",") + "]" : o && typeof o === "object" ? "{" + Object.keys(o).sort().map(k => JSON.stringify(k) + ":" + canon(o[k])).join(",") + "}" : JSON.stringify(o); function digest(action: any, n: bigint): Uint8Array { let struct: Uint8Array; if (TYPED[action.type]) struct = kb(cat(TYPED[action.type](action, n))); else { // Agent path const h = kb(cat([toUtf8Bytes(canon(action)), getBytes(zeroPadValue(toBeArray(n), 8))])); struct = kb(cat([hs("Agent(string source,bytes32 actionHash)"), hs("b"), h])); } return kb(cat([Uint8Array.from([0x19, 0x01]), DOMAIN, struct])); } let lastNonce = 0; const nextNonce = () => (lastNonce = Math.max(lastNonce + 1, Date.now())); // strictly increasing async function signAndSend(action: any, extra: Record = {}) { const n = BigInt(nextNonce()); const sig = new SigningKey(PRIVATE_KEY).sign(hexlify(digest(action, n))); return (await fetch(`${BASE_URL}/exchange`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action, nonce: Number(n), signature: { r: sig.r, s: sig.s, v: sig.v }, ...extra }) })).json(); } const info = (q: any) => fetch(`${BASE_URL}/info`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(q) }).then(r => r.json()); (async () => { // 1. Register — inviteCode is an unsigned, top-level field const acct = (await signAndSend({ type: "registerAccount", address: ADDRESS }, { inviteCode: INVITE_CODE })).response.accountId; // 2. Wait for the 10,000 USDC airdrop to become usable margin. Devnet credits it straight to // the market-deployer margin pool; if it instead arrives as a chain-level balance, move // it in with lockCollateral. Polling marginAvailableForOrder works for either path. const marginAvail = async () => BigInt((await info({ type: "userAccount", accountId: String(acct), marketDeployerId: MARKET_DEPLOYER })).marginAvailableForOrder ?? "0"); const chainUsdc = async () => { const bals = (await info({ type: "userAccount", accountId: String(acct), marketDeployerId: 0 })).chainBalances ?? []; const c = bals.find((x: any) => x.coinId === USDC); return c ? BigInt(c.amount) : 0n; }; while ((await marginAvail()) === 0n) { const chain = await chainUsdc(); if (chain > 0n) await signAndSend({ type: "lockCollateral", marketDeployerId: MARKET_DEPLOYER, coinId: USDC, amount: String(chain) }); await new Promise(r => setTimeout(r, 1000)); } // 3. Place a limit buy — raw price/size, scaled by the contract's priceScale/qtyScale. const place = await signAndSend({ type: "order", grouping: "na", orders: [{ a: CONTRACT, b: true, p: "1000000", s: "100", r: false, t: { limit: { tif: "Gtc" } } }] }); // oid comes back inline as statuses[].resting.oid; if the server accepts asynchronously // and omits it, look it up from userOrders instead. const statuses = place.response?.data?.statuses ?? []; let oid = statuses[0]?.resting?.oid; if (oid === undefined) { const orders = (await info({ type: "userOrders", accountId: String(acct), marketDeployerId: MARKET_DEPLOYER, contractId: CONTRACT })).orders; oid = Number(orders[orders.length - 1].id); } // 4. Verify, then cancel console.log(await info({ type: "userOrders", accountId: String(acct), marketDeployerId: MARKET_DEPLOYER })); console.log(await signAndSend({ type: "cancel", cancels: [{ a: CONTRACT, o: oid }] })); })(); ``` ## Expected output ```text theme={null} {...open orders...} {"status": "ok", ...cancel...} ``` ## Configuration The IDs in the script come from [`configs`](/info/configs) — enumerate them there rather than hardcoding: | Constant | Value | Meaning | | ----------------- | ----- | ------------------------------------------------ | | `USDC` | `1` | USDC coin ID | | `MARKET_DEPLOYER` | `1` | Default market deployer (joined at registration) | | `CONTRACT` | `1` | Contract ID (BTC-USDC) | Prices and sizes (`p`, `s`) are **raw integer strings**, scaled by the contract's `priceScale` / `qtyScale`. * **`inviteCode required`** — `INVITE_CODE` is missing or invalid. Set a valid single-use code from the UpsideMAX team. * **Loop keeps waiting for margin** — the airdrop hasn't landed yet (it can take \~10s after registration). The loop exits automatically once `marginAvailableForOrder` turns positive. * **Order rejected for margin** — funds aren't usable as margin yet. Wait for `marginAvailableForOrder > 0`, then retry. * **No `oid` in the order response** — on Devnet the order may be accepted asynchronously; the script falls back to reading the order ID from `userOrders`. # Info Requests Source: https://docs.upsidemax.xyz/guide/read-requests Query market data, orders, and account state via POST /info. No signing required — just a JSON body with a type field to route the query. All read operations on UpsideMAX use a single endpoint: `POST /info`. No authentication is required — you send a JSON body with a `type` field that identifies the query, along with any query-specific parameters. The server returns the requested data directly in the response body. ## Endpoint ```text theme={null} POST /info Content-Type: application/json ``` ## Request Structure Every `POST /info` body is a JSON object with a top-level `type` field that routes the request, plus any additional parameters the query requires: ```http theme={null} POST /info HTTP/1.1 Content-Type: application/json {"type": "userOrders", "accountId": "5", "marketDeployerId": 1} ``` The `type` field plays the same routing role here that `action.type` plays for `POST /exchange` write requests — the URL never changes, and you express intent through the JSON body. ## Available Query Types Cache `configs` responses client-side. Configuration data — contract IDs, price scales, quantity scales, and market parameters — changes rarely and only when new contracts are listed. Avoid calling `configs` on every request. | `type` | Description | Reference | | --------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | `configs` | Lists all contracts, their IDs, price/qty scales, and market parameters | [/info/configs](/info/configs) | | `userMarketDeployers` | Returns the market deployer IDs an account is enrolled in | [/info/user-market-deployers](/info/user-market-deployers) | | `userAccount` | Returns account summary: balances, margin, and position overview | [/info/user-account](/info/user-account) | | `userOrders` | Lists all open orders for an account | [/info/user-orders](/info/user-orders) | | `ordersByIds` | Fetches specific orders by their server-assigned order IDs | [/info/orders-by-ids](/info/orders-by-ids) | | `ordersByCloids` | Fetches specific orders by client order IDs | [/info/orders-by-cloids](/info/orders-by-cloids) | | `candleSnapshot` | Returns OHLCV candle data for a contract and time range | [/info/candle-snapshot](/info/candle-snapshot) | | `l2Book` | Returns the current Level 2 order book for a contract | [/info/l2-book](/info/l2-book) | | `marketState` | Returns current market state: mark, index, and last price plus the funding rate index | [/info/market-state](/info/market-state) | | `shareGroupState` | Returns the state of a portfolio margin share group | [/info/share-group-state](/info/share-group-state) | | `userAgents` | Lists agent addresses authorized to act on behalf of an account | [/info/user-agents](/info/user-agents) | ## Example Queries **Get all open orders for account 5:** ```json theme={null} { "type": "userOrders", "accountId": "5", "marketDeployerId": 1 } ``` **Get the L2 order book for BTC-USDC (asset 1):** ```json theme={null} { "type": "l2Book", "asset": 1 } ``` **Get hourly candles for SOL-USDC over the last 24 hours:** ```json theme={null} { "type": "candleSnapshot", "req": { "coin": "SOL-USDC", "interval": "1h", "startTime": 1778486400000, "endTime": 1778572800000 } } ``` **Get contract configuration:** ```json theme={null} { "type": "configs", "marketDeployerId": 1 } ``` ## Response Shape A successful `/info` response returns HTTP 200 with the query result directly in the body. The structure varies by query type — refer to the individual query reference pages linked in the table above. ```json theme={null} [ { "oid": 15, "asset": 1, "isBuy": true, "limitPx": "50", "sz": "10", "tif": "Gtc" } ] ``` # Response Source: https://docs.upsidemax.xyz/guide/responses Reference for success and error response shapes from POST /exchange and POST /info, including business-level errors inside the statuses array. UpsideMAX returns JSON for every request. Understanding the response structure is important because **HTTP 200 does not always mean the operation succeeded** — business-level rejections (such as invalid order prices or unknown order IDs) return HTTP 200 with error detail inside the response body. This page explains every response shape you may encounter. ## POST /exchange Success Response When a write operation is accepted and processed, the server returns HTTP 200 with `status: "ok"`: ```json theme={null} { "status": "ok", "requestId": "req-144115188075855905", "response": { "type": "order", "data": { "statuses": [ { "resting": { "oid": 15 } } ] } } } ``` Always `"ok"` for HTTP-level success. Note that business-level errors can still appear inside `response.data` even when `status` is `"ok"`. A server-generated unique identifier for this request. Include this value when contacting UpsideMAX support — it allows the team to trace the request through server logs. Echoes the `action.type` from the request. Useful for confirming which action the response corresponds to when processing responses asynchronously. Action-specific result data. For `order` actions this contains a `statuses` array. For `registerAccount` this contains `accountId`. See each action's reference page for the full data shape. ## POST /exchange Error Response When the server rejects the request at the HTTP level (bad signature, malformed JSON, rate limit, etc.), it returns a non-200 HTTP status with `status: "error"`: ```json theme={null} { "status": "error", "requestId": "req-144115188075855906", "code": "SIGNATURE_INVALID", "message": "recovered address 0xabc...def does not match action.address 0x123...456" } ``` Always `"error"` for HTTP-level failures. Server-generated request identifier. Provide this to UpsideMAX support when reporting an issue. Machine-readable error code. See [Error Codes](/guide/error-codes) for the full list. Human-readable explanation of the error. For `SIGNATURE_INVALID`, this includes both the recovered address and the expected address to help you debug signing issues. ## Business-Level Errors Some operations return HTTP 200 with `status: "ok"` but contain per-item errors inside `response.data.statuses`. This occurs when the request envelope is valid and signed correctly, but one or more of the operations within the request was rejected by the matching engine. ```json theme={null} { "status": "ok", "requestId": "req-144115188075855907", "response": { "type": "order", "data": { "statuses": [ { "error": "size must be positive" } ] } } } ``` Each entry in `statuses` corresponds to one item in the request's array (e.g. one entry per order in `orders[]`). A successful item looks like `{"resting": {"oid": 15}}` or `{"filled": {...}}`. A rejected item looks like `{"error": ""}`. Always check the `statuses` array in order and cancel responses. An HTTP 200 response does not guarantee that any or all operations within the request succeeded. Iterate over `statuses` and handle each `error` entry explicitly. ## POST /info Response Read query responses return HTTP 200 with the query result directly in the body. There is no wrapper `status` field — the body IS the data: ```json theme={null} [ { "oid": 15, "asset": 1, "isBuy": true, "limitPx": "50", "sz": "10", "tif": "Gtc" } ] ``` If the query fails (bad `type`, missing required fields), the server returns a non-200 status with an error body consistent with the exchange error format. ## The requestId Field Every `POST /exchange` response includes a `requestId` such as `"req-144115188075855905"`. This is a globally unique identifier generated by the server for each incoming request. When you contact UpsideMAX support about a specific request — whether it succeeded unexpectedly, failed unexpectedly, or produced a surprising result — including the `requestId` allows the support team to locate the exact request in server logs and investigate efficiently. Log the `requestId` for every `POST /exchange` call your application makes. Even if you don't need it immediately, having it available significantly speeds up any future debugging or support conversations. # Exchange Requests Source: https://docs.upsidemax.xyz/guide/write-requests Understand the envelope structure of POST /exchange requests — action, signature, and nonce fields for all state-changing operations on UpsideMAX. All state-changing operations on UpsideMAX — placing orders, cancelling orders, registering accounts, adjusting margin, and more — use a single endpoint: `POST /exchange`. The operation to perform is determined entirely by the `action.type` field inside the JSON body, not by the URL. This action-based routing means your HTTP client only ever needs to know one write URL; you change the payload to change the operation. ## Endpoint ```text theme={null} POST /exchange Content-Type: application/json ``` ## Envelope Structure Every `POST /exchange` request body is a JSON object with the following top-level fields: ```json theme={null} { "action": { "type": "", "": "" }, "signature": { "r": "0x<64 hex>", "s": "0x<64 hex>", "v": 27 }, "nonce": 1778572951477 } ``` ### Fields The operation payload. Must contain a `type` string that identifies the action (e.g. `"order"`, `"cancel"`, `"registerAccount"`). All other fields are action-specific. Identifies which action to perform. It also selects the EIP-712 signing path — funds/permission actions use a typed struct, everything else uses the Agent path. See [Authentication](/guide/authentication). Common values: `order`, `cancel`, `cancelByCloid`, `modify`, `registerAccount`, `updateLeverage`, `updateIsolatedMargin`. EIP-712 ECDSA signature (secp256k1) over the action. The server recovers the signer's Ethereum address from this signature to authorize the operation — there are no API keys. The `r` component of the ECDSA signature. Must be a `0x`-prefixed lowercase hex string representing a 32-byte value (66 characters total). The `s` component of the ECDSA signature. Same format as `r`. The recovery parameter. Must be `27` or `28` (Ethereum convention: `27 + recovery_bit`). An int64 Unix millisecond timestamp. Must be unique per signer. Used as replay protection — the server rejects any previously seen nonce from the same address. Optional. The Ethereum address of a vault for which the signer is acting as a proxy. When present, the server verifies that the signer is an authorized agent of the vault and applies the operation to the vault's account. This is a separate vault-proxy mechanism (not currently enabled on Devnet) — **delegated agent trading does not use it**: an agent signs with its own key and the server routes to the master account from the recovered signer, so trade actions ignore `vaultAddress`. Optional. The `domain.chainId` your client used when signing a **typed-path** action, as a `0x`-prefixed hex string. Defaults to `0x2627` (9767). Sits at the top level of the envelope and is **not** part of the signed message. See [Authentication](/guide/authentication#browser-wallet-signing). Conditional. A 6-character alphanumeric alpha test invitation code required by `registerAccount` in gated environments. Include this field at the top level of the envelope — **not** inside `action`. This field is **not** included in the signed message. ## Action-Based Routing Unlike a traditional REST API where `POST /orders` creates an order and `DELETE /orders/:id` cancels one, UpsideMAX routes every write operation through `POST /exchange`. Routing is determined by `action.type` in the body. This keeps the transport layer simple and makes it easy to batch operations in a single request structure. The table below maps `action.type` values to their operations: | `action.type` | Operation | | ----------------------- | ------------------------------------------------ | | `registerAccount` | Register a new trading account for an address | | `order` | Place one or more orders (up to 10 per request) | | `cancel` | Cancel one or more orders by order ID | | `cancelByCloid` | Cancel an order by client order ID | | `modify` | Modify the price or size of an existing order | | `updateSlippageSetting` | Set the market-order slippage cap for an account | | `updateLeverage` | Change leverage for an asset | | `updateIsolatedMargin` | Add or remove isolated margin for a position | ## Example Request The following example places a limit GTC buy order (raw `p` / `s` values — scale per the contract's `configs`): ```json theme={null} { "action": { "type": "order", "orders": [ { "a": 1, "b": true, "p": "50", "s": "10", "r": false, "t": { "limit": { "tif": "Gtc" } } } ], "grouping": "na" }, "signature": { "r": "0x3b2a1f...", "s": "0x7cf133...", "v": 28 }, "nonce": 1778572951477 } ``` See [Authentication](/guide/authentication) for instructions on building the signature, and [Nonce](/guide/nonce) for nonce generation rules. # Account By Address Source: https://docs.upsidemax.xyz/info/account-by-address Resolve a wallet address to its accountId. Works for both master addresses and agent wallet addresses, reporting which one you supplied. The `accountByAddress` query resolves a wallet address to its `accountId`, using the on-chain account registry as the authority. It accepts both kinds of address: * A **master address** resolves to its own `accountId`. * An **agent (API wallet) address** resolves to the `accountId` of the master it is bound to, with `isAgent` set to `true`. Use it to discover your `accountId` after registration, or to confirm which master an agent wallet signs for before trusting it. ## Request ```json theme={null} {"type": "accountByAddress", "address": "0xabc...123"} ``` Must be `"accountByAddress"`. The address to resolve — `"0x"` followed by 40 hex characters. Case-insensitive. ## Response ```json theme={null} { "type": "accountByAddress", "address": "0xabc...123", "accountId": "1", "isAgent": false, "masterId": "0" } ``` The queried address, echoed back in lowercase. The resolved account ID. **`"0"` means the address is not registered** or has no valid binding — treat it as "not found", not as account zero. `true` when the queried address is an agent wallet, in which case `accountId` is the master it is bound to. `false` when the address is a master account itself. When `isAgent` is `true`, the master account's ID — identical to `accountId`. Otherwise `"0"`. ## Interpreting the result | Address supplied | `accountId` | `isAgent` | `masterId` | | ---------------- | --------------------- | --------- | ------------------- | | Master account | Its own ID | `false` | `"0"` | | Agent wallet | The bound master's ID | `true` | Same as `accountId` | | Unregistered | `"0"` | `false` | `"0"` | Because an agent resolves to its master, actions signed by an approved agent execute under the master account. See [Agent Wallets](/agents/api-wallets) for the delegation model. # Candle Snapshot Source: https://docs.upsidemax.xyz/info/candle-snapshot Query historical OHLCV candle data for a contract over any time range, with support for minute, hour, day, week, and month intervals. The `candleSnapshot` query returns historical OHLCV (open/high/low/close/volume) candles for a contract over a specified time range. Candles are returned in ascending time order. The last candle in the response may represent the current open (in-progress) bar, indicated by `"closed": false` — at most one such bar will appear, always at the end of the array. For real-time updates, subscribe to the WebSocket `candle` channel after loading historical data with this endpoint. The WebSocket push will keep your local candle state current without repeated polling. ## Request ```json theme={null} { "type": "candleSnapshot", "asset": "1", "interval": "1m", "startTime": 1782205172447, "endTime": 1782291572447 } ``` Must be `"candleSnapshot"`. The contract ID to fetch candles for, expressed as a decimal string (e.g. `"1"`). Use the `contractId` from `configs`. Candle interval. See the supported intervals table below for all valid values. Start of the time range as Unix milliseconds (inclusive). Defaults to `0` (earliest available data) if omitted. End of the time range as Unix milliseconds (inclusive). Defaults to max int64 (latest available data) if omitted. ## Supported Intervals | Category | Intervals | | -------- | ------------------------------ | | Minutes | `1m`, `3m`, `5m`, `15m`, `30m` | | Hours | `1h`, `2h`, `4h`, `8h`, `12h` | | Day+ | `1d`, `3d`, `1w`, `1M` | Passing an interval string not listed above returns HTTP 400 `BAD_REQUEST`. Validate the interval value in your client before sending the request. If the `asset` does not exist or no trades have occurred in the requested time range, the response returns `"candles": []` with HTTP 200. This is not an error — simply an empty result. ## Response Unlike every other `/info` query, this response carries **no `type` field**. It returns `{asset, interval, candles}` directly — do not route on `type` here. ```json theme={null} { "asset": "1", "interval": "1m", "candles": [ { "s": "1", "i": "1m", "t": 1782270780000, "T": 1782270840000, "o": "100", "c": "100", "h": "100", "l": "100", "v": "4", "n": 1, "closed": true }, { "s": "1", "i": "1m", "t": 1782279660000, "T": 1782279720000, "o": "110", "c": "70", "h": "110", "l": "70", "v": "15", "n": 6, "closed": false } ] } ``` ### Candle Fields Contract ID (same as `asset`). Interval identifier. Bucket open time in Unix milliseconds (inclusive). This is the start of the candle period. Bucket close time in Unix milliseconds (exclusive). The next candle's `t` equals this value. Opening price of the candle (raw integer string). Divide by `10^priceScale` from `configs` to get the display value. Highest price traded during the candle period (raw integer string). Lowest price traded during the candle period (raw integer string). Closing price of the candle (raw integer string). For an open bar (`closed: false`), this is the last traded price so far. Total traded volume during the candle period (raw integer string). Divide by `10^qtyScale` from `configs` to get the display value. Number of individual trades that occurred during the candle period. `true` if this candle is finalized (the period has ended); `false` if this is the current open bar that is still accumulating trades. At most one `false` candle will appear, always as the last element in the array. # Configs Source: https://docs.upsidemax.xyz/info/configs Fetch all coin and contract configuration, including tick sizes, step sizes, leverage tiers, and price/quantity decimal scales for every listed contract. The `configs` query returns all coin definitions and contract configurations available on the exchange. Use this response to look up contract IDs for the `a` field in orders, determine tick and step sizes for price/quantity validation, read decimal scales to convert raw values to display values, and inspect leverage tiers for margin calculations. Because this data only changes when new contracts are listed, you should cache it locally and refresh infrequently. On Devnet, `configs` also returns internal staging/test listings alongside the public contracts, and some are quoted in coins other than USDC. Always select contracts by `name` or `quoteCoinId` — do **not** assume `contractId` `1`/`2`/`3` map to BTC/ETH/SOL. IDs are server-assigned, so never hardcode them. ## Request ```json theme={null} {"type": "configs", "marketDeployerId": 0} ``` Must be `"configs"`. Filter results by market deployer. Use `0` or omit to return configuration for all deployers. Pass a positive integer to restrict results to a specific deployer. ## Response ```json theme={null} { "type": "configs", "marketDeployerIdFilter": 0, "coins": [ { "coinId": 1, "name": "USDC", "szDecimals": 6, "isMargin": true, "status": "Active" }, { "coinId": 3, "name": "BTC", "szDecimals": 8, "isMargin": false, "status": "Active" }, { "coinId": 4, "name": "ETH", "szDecimals": 8, "isMargin": false, "status": "Active" }, { "coinId": 5, "name": "SOL", "szDecimals": 8, "isMargin": false, "status": "Active" } ], "contracts": [ { "contractId": 1, "marketId": 1, "marketDeployerId": 1, "name": "BTC-USDC", "baseCoinId": 3, "quoteCoinId": 1, "tickSize": "1", "stepSize": "1", "priceScale": 2, "qtyScale": 4, "defaultLeverage": "10", "icon": "", "fundingInterval": 3600, "minTradeNtl": "1", "status": "Active", "tiers": [ { "upperBound": "9223372036854775807", "maxLeverage": "50", "imBps": 100, "mmBps": 50 } ] } ] } ``` ### Contract Fields The unique contract identifier. Use this as the `a` field when placing, modifying, or canceling orders. Human-readable contract name (e.g. `"BTC-USDC"`). Minimum price movement expressed as a raw integer string. All order prices must be multiples of this value. Minimum quantity movement expressed as a raw integer string. All order quantities must be multiples of this value. Decimal exponent for prices. Divide any raw price by `10^priceScale` to get the human-readable display value. Decimal exponent for quantities. Divide any raw quantity by `10^qtyScale` to get the human-readable display value. The initial leverage applied to new positions on this contract. URL of the contract's icon. Returns an **empty string** when none is set — never `null`. Funding settlement interval in **seconds**; funding settles once per interval. The value comes from a fixed set: `3600` (1h), `7200` (2h), `14400` (4h), or `28800` (8h). Minimum notional value per trade, expressed as a raw integer string. Leverage tier schedule for this contract. Each tier applies up to `upperBound` position size. Maximum position size (raw) to which this tier applies. The highest tier uses `"9223372036854775807"` (max int64) as a sentinel for "no upper limit." Maximum leverage allowed for positions up to `upperBound`. Initial margin rate in basis points (e.g. `100` = 1%). Maintenance margin rate in basis points. Positions are liquidated when equity falls below this threshold. # L2 Book Source: https://docs.upsidemax.xyz/info/l2-book Pull a one-time full L2 order book snapshot with all price levels for a contract. For live updates, use the WebSocket l2Book channel instead. The `l2Book` query returns a full L2 order book snapshot containing all price levels for a contract at a point in time. This is useful for initializing local order book state before connecting to the WebSocket feed. For ongoing updates, subscribe to the WebSocket `l2Book` channel — subscribing automatically delivers an initial snapshot, so you typically do not need to make this REST call separately. This endpoint returns HTTP 503 with a `NOT_READY` error if no book data exists yet (for example, on a cold start or if no orders have ever been placed on the contract). Implement retry logic with exponential backoff when you receive this response. ## Request ```json theme={null} {"type": "l2Book", "asset": "1"} ``` Must be `"l2Book"`. The contract ID to fetch the order book for, expressed as a decimal string (e.g. `"1"`). Use the `contractId` from `configs`. ## Response ```json theme={null} { "asset": "1", "time": 1782270780000, "bookVersion": 90231, "markPx": "100", "oraclePx": "100", "levels": [ [{"px": "100", "sz": "5", "n": 3}], [{"px": "101", "sz": "2", "n": 1}] ] } ``` Contract ID. Timestamp of the snapshot in Unix milliseconds. Monotonically increasing version number for this book. Use this to deduplicate or order updates when combining REST snapshots with WebSocket deltas. Current mark price (raw integer string). Returns `"0"` if the mark price has not yet been published for this contract. Current oracle (index) price (raw integer string). Returns `"0"` if the oracle price has not yet been published for this contract. Two-element array: `levels[0]` contains bids sorted from highest to lowest price; `levels[1]` contains asks sorted from lowest to highest price. Price at this level (raw integer string). Divide by `10^priceScale` from `configs` to get the display value. Total quantity available at this price level (raw integer string). Divide by `10^qtyScale` from `configs` to get the display value. Number of individual orders resting at this price level. # Market State Source: https://docs.upsidemax.xyz/info/market-state Fetch the current mark price, oracle price, last trade price, and funding rate index for a contract to align local state on startup or reconnect. The `marketState` query returns the current mark price, oracle (index) price, last trade price, funding rate index, and price readiness flag for a contract. Use this on startup or after a reconnect to synchronize your local price state before relying on WebSocket updates. It also serves as a lightweight check to confirm that a contract is ready to accept orders. Check `priceReady` before placing orders. If the mark price is unavailable, order placement may be rejected by the exchange until prices are published. ## Request ```json theme={null} {"type": "marketState", "asset": "1"} ``` Must be `"marketState"`. The contract ID to fetch market state for, expressed as a decimal string (e.g. `"1"`). Use the `contractId` from `configs`. ## Response ```json theme={null} { "type": "marketState", "asset": "1", "markPx": "1133770", "oraclePx": "1133800", "lastPx": "1133700", "priceReady": true, "fundingIndex": "55", "fundingLastTimestamp": 1782270000000 } ``` Current mark price used for PnL calculation and liquidation checks (raw integer string). Returns `"0"` if the mark price has not yet been published for this contract. Current oracle (index) price sourced from external feeds (raw integer string). Returns `"0"` if not yet published. Used to compute the funding rate. Price of the most recent trade on this contract (raw integer string). Returns `"0"` if no trades have occurred yet. `true` if mark and index prices are available and valid for trading. `false` indicates prices have not been published yet — orders placed while this is `false` may be rejected. Cumulative funding rate index snapshot at the time of the last funding settlement. Use this to compute unrealized funding payments for open positions. Unix millisecond timestamp of the most recent funding settlement event. # Order History Source: https://docs.upsidemax.xyz/info/order-history Query your terminated orders — filled, cancelled, rejected, or triggered. Paginated by time window and filterable by contract. The `orderHistory` query returns your account's **terminated** orders: fully filled, cancelled, rejected, or conditional orders that have triggered or been cancelled. Orders that are still resting or only partially filled do **not** appear here — query those with [`userOrders`](/info/user-orders). It shares its request parameters, pagination contract, and value conventions with [`userFills`](/info/user-fills) and [`userFundingFlows`](/info/user-funding-flows). ## Request ```json theme={null} {"type": "orderHistory", "accountId": "5"} ``` Must be `"orderHistory"`. Your account ID. Must be greater than zero. A numeric value is also accepted. Restrict results to one contract. Use `0` or omit for all contracts. Inclusive start of the time window, in Unix milliseconds. `0` or omitted means unbounded. Inclusive end of the time window, in Unix milliseconds. `0` or omitted means unbounded. A `startTime` later than `endTime` returns `400 BAD_REQUEST`. Maximum rows in this page. Defaults to **1000**, which is also the maximum; a larger value returns `400 BAD_REQUEST`. ## Response ```json theme={null} { "type": "orderHistory", "accountId": "5", "contractIdFilter": 0, "orders": [ { "updatedTimeMs": 1700000000002, "height": 42, "seq": 3, "orderId": "101", "clientOrderId": "777", "contractId": 7, "marketDeployerId": 5, "orderSide": "S", "orderType": "SL", "timeInForce": "Gtc", "reduceOnly": true, "triggerPrice": "49000", "triggerPriceType": 1, "price": "50000", "origQty": "10", "filledQty": "10", "leavesQty": "0", "status": "Filled", "origin": "PassiveFill", "cancelReason": -1, "rejectCode": 26, "createdTimeMs": 1700000000001 } ], "count": 1, "truncated": false } ``` ### Envelope fields Echoes the `contractId` filter that was applied. `0` means no filter. Number of rows in this page. When `count` equals your `limit`, another page may be available. `true` when the page was cut short at a row boundary because the response reached its byte ceiling. ### Order object fields Time the order reached its final state, in Unix milliseconds. This is the field you page on. Block height at which the order was finalized. Sequence number within the block. Exchange-assigned order ID. The client order ID you assigned at placement, or `null` when none was set. Contract the order was placed on. Market deployer the contract belongs to. `"B"` for buy, `"S"` for sell. `L`, `M`, `SL`, `SM`, `TPL`, or `TPM`. See the enum table below. `Gtc`, `Alo`, `Ioc`, or `Fok`. `true` when the order could only reduce an existing position. Trigger price for a conditional or TP/SL order (raw integer string). `"0"` for an ordinary order. Which price feed the trigger watched: `0` for mark price, `1` for oracle price. Limit price of the order (raw integer string). Original order quantity (raw integer string). Quantity filled before the order terminated. Quantity still unfilled when the order terminated. `Open`, `Filled`, `Canceled`, or `Untriggered`. How the order came about. See the enum table below. Signed cancellation reason. `-1` indicates a cancellation internal to the matching engine. Rejection code for a rejected order. Time the order was created, in Unix milliseconds. There is no order-level fee or realized-PnL total in this response. Derive both by summing the matching rows in [`userFills`](/info/user-fills) for the same `orderId`. ## Pagination Rows come back in ascending `updatedTimeMs` order, at most `limit` per page. Pass the last row's `updatedTimeMs` as the next request's `startTime`; because `startTime` is inclusive, deduplicate the repeated boundary row by `orderId`. A page with `count` below `limit` is the last one. See [`userFills`](/info/user-fills) for the full walkthrough. ### Enumerations | Field | Values | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `orderSide` | `B` (buy) · `S` (sell) | | `orderType` | `L` (limit) · `M` (market) · `SL` (stop limit) · `SM` (stop market) · `TPL` (take-profit limit) · `TPM` (take-profit market) | | `timeInForce` | `Gtc` · `Alo` (post-only) · `Ioc` · `Fok` | | `status` | `Open` · `Filled` · `Canceled` · `Untriggered` | | `origin` | `Normal` · `Modify` · `Liquidation` · `Conditional` · `PassiveFill` · `Cancel` | | Any unrecognized value | `Unknown` | # Orders By Cloids Source: https://docs.upsidemax.xyz/info/orders-by-cloids Fetch orders by your client-assigned order IDs for easy reconciliation and status tracking without needing to store exchange-assigned order IDs. The `ordersByCloids` query lets you look up orders using the client-assigned order IDs you set at placement time (the `c` field in a place order action). This is useful for reconciliation flows where you want to track order status using your own identifiers without needing to store or map exchange-assigned order IDs. The response `orders[]` array uses the same fields as [`userOrders`](/info/user-orders). ## Request ```json theme={null} { "type": "ordersByCloids", "accountId": "1002", "marketDeployerId": 1, "cloids": ["1778844423064", "1778844423078"] } ``` Must be `"ordersByCloids"`. Your account ID. Client order IDs are scoped to an account, so this field is required to avoid collisions across accounts. The market deployer the orders belong to. Array of client order IDs to look up, each expressed as an int64 decimal string. These must match the `c` values you supplied when the orders were placed. ## Response ```json theme={null} { "type": "ordersByCloids", "accountId": "1002", "marketDeployerId": 1, "orders": [ { "id": "8280", "clientOrderId": "1778844423064", "accountId": "1002", "contractId": 1, "marginMode": "C", "positionSide": "OneWay", "orderSide": "B", "orderType": "L", "timeInForce": "Gtc", "price": "50", "size": "10", "originalSize": "10", "leverage": "10", "status": "Open", "reduceOnly": false } ] } ``` The `orders` array contains the same fields described in [`userOrders`](/info/user-orders#order-fields) — including `originalSize`, since `size` is the **remaining** quantity rather than the quantity originally submitted. Client order IDs that do not match any known order are silently omitted from the response. # Orders ByIds Source: https://docs.upsidemax.xyz/info/orders-by-ids Fetch one or more orders by their exchange-assigned order IDs to check status, fill details, or confirm cancellation without scanning the full order list. The `ordersByIds` query lets you fetch one or more specific orders using their exchange-assigned order IDs — the `id` values returned by `userOrders` or an order placement response. This is the most direct way to check order status or confirm a cancellation without retrieving your entire order list. The response `orders[]` array uses the same fields as [`userOrders`](/info/user-orders). ## Request ```json theme={null} { "type": "ordersByIds", "marketDeployerId": 1, "orderIds": ["8280", "8281"] } ``` Must be `"ordersByIds"`. The market deployer the orders belong to. Array of exchange-assigned order IDs to look up, each expressed as an int64 decimal string (e.g. `["8280", "8281"]`). You can pass a single-element array to look up one order. ## Response ```json theme={null} { "type": "ordersByIds", "marketDeployerId": 1, "orders": [ { "id": "8280", "clientOrderId": "0", "accountId": "5", "contractId": 1, "marginMode": "C", "positionSide": "OneWay", "orderSide": "B", "orderType": "L", "timeInForce": "Gtc", "price": "50", "size": "10", "originalSize": "10", "leverage": "10", "status": "Open", "reduceOnly": false } ] } ``` The `orders` array contains the same fields described in [`userOrders`](/info/user-orders#order-fields) — including `originalSize`, since `size` is the **remaining** quantity rather than the quantity originally submitted. Orders not found for a given ID are silently omitted from the response — check that the returned array length matches your input if you need to confirm all IDs resolved. # Info API Overview Source: https://docs.upsidemax.xyz/info/overview All read queries on UpsideMAX use a single POST /info endpoint. No authentication required — send a JSON body with a type field to route your query. Every read operation on UpsideMAX flows through a single endpoint: `POST https://dev.upsidemax.xyz/info`. There is no authentication or signing required — you send a JSON body with a `type` field, and the API routes your request to the correct query handler. This unified design keeps integration simple whether you are fetching market data, inspecting your account, or looking up orders. Cache `configs` responses locally — they only change when new contracts are listed. This avoids redundant round-trips for tick sizes, leverage tiers, and contract IDs on every request. ## Endpoint ```text theme={null} POST https://dev.upsidemax.xyz/info Content-Type: application/json ``` All requests share the same shape: a JSON object with a required `type` string and any additional fields specific to the query. ```json theme={null} {"type": "", ...} ``` ## Available Query Types Retrieve live and historical market information for any contract. | Type | Description | | -------------------------------------------- | ------------------------------------------------------------------------------ | | [`configs`](/info/configs) | Market and contract configuration — tick sizes, leverage tiers, decimal scales | | [`candleSnapshot`](/info/candle-snapshot) | Historical OHLCV candles for a contract over a time range | | [`l2Book`](/info/l2-book) | Full L2 order book snapshot for a contract | | [`marketState`](/info/market-state) | Mark price, index price, last trade price, and funding rate | | [`ticker`](/info/ticker) | 24-hour rolling statistics — change, high, low, volume, funding rate | | [`shareGroupState`](/info/share-group-state) | Portfolio share group definitions — contract membership and settlement coin | Inspect your account balances, positions, and authorized agents. | Type | Description | | ---------------------------------------------------- | ---------------------------------------------------- | | [`userMarketDeployers`](/info/user-market-deployers) | Enrolled market deployer IDs for your account | | [`userAccount`](/info/user-account) | Balances, open positions, and margin availability | | [`userAgents`](/info/user-agents) | Authorized API wallet agents for a master account | | [`accountByAddress`](/info/account-by-address) | Resolve a wallet or agent address to its `accountId` | Look up active and historical orders by various identifiers. | Type | Description | | ------------------------------------------ | -------------------------------------------- | | [`userOrders`](/info/user-orders) | All active open orders for an account | | [`ordersByIds`](/info/orders-by-ids) | Look up orders by exchange-assigned order ID | | [`ordersByCloids`](/info/orders-by-cloids) | Look up orders by client-assigned order ID | Page through terminated activity. All three share one set of request parameters and one pagination contract. | Type | Description | | ---------------------------------------------- | ------------------------------------------------------------- | | [`userFills`](/info/user-fills) | Historical fills with fee, realized PnL, and position context | | [`orderHistory`](/info/order-history) | Terminated orders — filled, cancelled, rejected, or triggered | | [`userFundingFlows`](/info/user-funding-flows) | Funding settlements per position, with index before and after | # Share Group State Source: https://docs.upsidemax.xyz/info/share-group-state Fetch the definitions of portfolio share groups, including contract membership and settlement coin, used by PORTFOLIO margin mode accounts. The `shareGroupState` query returns the definitions of share groups used by PORTFOLIO margin mode accounts. A share group defines a set of contracts that share a single margin pool, with a common settlement coin. Use this to understand which contracts are grouped together when calculating portfolio margin requirements, and to check whether a group is currently accepting new (non-reduce-only) orders. ## Request ```json theme={null} {"type": "shareGroupState", "groupId": 0} ``` Must be `"shareGroupState"`. Filter results by group ID. Use `0` or omit to return all share groups. Pass a positive integer to retrieve the definition for a single group. ## Response ```json theme={null} { "type": "shareGroupState", "groups": [ { "groupId": 3, "settleCoinId": 1, "status": "Active", "contractIds": [1, 2] } ] } ``` Unique identifier for this share group. Reference this ID in portfolio margin operations. The coin used for settlement and margin within this group. Cross-reference with the `coins` array in `configs` for the coin name and decimals. Current status of the share group: * `"Active"` — the group is operating normally and accepts all order types. * `"Frozen"` — the group is suspended; only `reduceOnly` orders are accepted on contracts in this group. Array of contract IDs that belong to this share group. All contracts in a group share the same margin pool and settlement coin. # Ticker Source: https://docs.upsidemax.xyz/info/ticker Fetch 24-hour rolling statistics for a contract or the whole market — price change, high, low, volume, trade count, plus the current funding rate, mark price, and oracle price. The `ticker` query returns **24-hour rolling statistics** for a contract: price change, high, low, volume and trade count, alongside the current funding rate, mark price, and oracle price. The window is a sliding `[now − 24h, now]` range, matching the conventional exchange definition — it is not a calendar day. Omit `asset` to retrieve every contract in one call. For a live version of the same payload, subscribe to the [`ticker`](/websocket/ticker) WebSocket channel. ## Request ```json theme={null} { "type": "ticker", "asset": "1" } ``` Must be `"ticker"`. The contract ID as a decimal string. **Omit to return every contract** that has traded. Contracts that have never traded are excluded from a market-wide response, and return all-zero values when queried individually. ## Response ```json theme={null} { "type": "ticker", "ts": 1754453600000, "tickers": [ { "asset": "1", "lastPx": "1010", "openPx": "1000", "priceChange": "10", "priceChangePct": "1.0000", "highPx": "1010", "lowPx": "990", "volume": "30000", "count": 3, "windowStartMs": 1754367200000, "fundingRate": "125", "fundingTime": 1754450000000, "markPx": "1010", "oraclePx": "1012" } ] } ``` Generation time of this response — the `now` end of the rolling window, in Unix milliseconds. One entry per contract. Contains a single entry when `asset` was supplied. ### Ticker object fields The contract ID this entry describes. Most recent trade price (raw integer string, scaled by the contract's `priceScale`). Baseline price 24 hours ago — the earliest trade inside the window. `lastPx − openPx`, signed (raw integer string). Percentage change over the window, signed and carried to four decimal places (for example `"1.0000"` or `"-0.8300"`). Returns `"0"` when `openPx` is `0`. This value is already a percentage — do not rescale it. Highest trade price within the window. Lowest trade price within the window. Traded volume over the window, in base units (raw integer string, scaled by the contract's `qtyScale`). Number of trades within the window. Start of the window in Unix milliseconds. For a contract listed less than 24 hours ago, this is the time of its earliest trade. Current funding rate, signed, as a raw fixed-point integer with a scale of **1e8** — `1e8` represents `100%`. The true rate is `fundingRate / 1e8`; to display a percentage, use `fundingRate / 1e6`. It applies **per funding interval** (read `fundingInterval` from [`configs`](/info/configs)). Returns `"0"` if the rate has never been published. The scale is `1e8` — **not** basis points (`1e4`) and not `1e6`. Example: `"125"` is `0.000125%` per interval. Unix millisecond timestamp at which the current funding rate was set. Returns `0` if it has never been set. Current mark price (raw integer string). Tracks the latest price publication regardless of trading activity, and is identical to the `markPx` returned by [`marketState`](/info/market-state). Returns `"0"` if never published. Current oracle (index) price (raw integer string), from the same source as [`marketState`](/info/market-state). Returns `"0"` if never published. Price and volume fields are raw integers — convert them with the contract's `priceScale` and `qtyScale` from [`configs`](/info/configs). Never hardcode a scale. `priceChangePct` is the one exception: it is already a decimal percentage. `markPx` and `oraclePx` update with every price publication, while the OHLCV fields only move when trades occur. A contract can therefore report live prices with `volume` still `"0"`. # User Account Source: https://docs.upsidemax.xyz/info/user-account Query your account equity, collateral balances, open positions, and margin availability for a specific market deployer or across all deployers. The `userAccount` query returns your account's complete financial state for a given market deployer: cross equity, margin availability, collateral balances, and all open positions. Pass `marketDeployerId: 0` for an account-wide overview that also includes portfolio groups, chain-level balances, and per-contract settings across all deployers. All numeric values are raw integers. Use `priceScale` and `qtyScale` from the [`configs`](/info/configs) response to convert prices and quantities to display values. ## Request ```json theme={null} {"type": "userAccount", "accountId": "5", "marketDeployerId": 1} ``` Must be `"userAccount"`. Your account ID, obtained from the `registerAccount` action. Market deployer to query. Use `0` to get a global account overview that includes all deployers, portfolio groups, and chain-level balances. ## Response ```json theme={null} { "type": "userAccount", "accountId": "5", "marketDeployerId": 1, "crossEquity": "1000000000000", "orderFrozen": "0", "orderLoss": "0", "marginAvailable": "1000000000000", "marginAvailableForOrder": "1000000000000", "totalPositionIM": "0", "crossPositionMM": "0", "takerOverrideBps": null, "makerOverrideBps": null, "effectiveTakerBps": 5, "effectiveMakerBps": 2, "marketSlippageBps": 1000, "crossCollaterals": [{"coinId": 1, "amount": "1000000000000"}], "isolatedCollaterals": [], "positions": [ { "id": "1", "contractId": 1, "positionSide": "OneWay", "size": "5", "openValue": "500", "isLongPosition": false, "fundingIndex": "0", "leverage": "10", "isIsolated": false, "unrealizedPnl": "0", "settledFunding": "-500", "mm": "0", "liqPx": "0" } ] } ``` ### Margin Fields Total cross margin equity: the sum of your cross collateral and unrealized PnL across all cross positions (raw integer string). Margin currently reserved to cover the worst-case cost of your open orders (raw integer string). Adverse price difference between your open order prices and the current mark price, representing potential additional margin consumption (raw integer string). Free cross margin: `crossEquity - orderFrozen - orderLoss` (raw integer string). Margin available to open new positions: `marginAvailable - totalPositionIM` (raw integer string). Sum of initial margin allocated to all open cross positions (raw integer string). Total maintenance margin across all cross positions. Liquidation is triggered when `crossEquity` falls below this value (raw integer string). List of collateral balances in the cross margin pool. Each entry contains `coinId` and `amount` (raw integer string). List of collateral balances allocated to isolated margin positions. Each entry contains `posKey` (string, the position composite key = `contractId` + side), `coinId`, and `amount`. ### Position Fields Unique position ID as an int64 decimal **string**. Assigned when the position is first opened; closing a position and reopening in the opposite direction creates a **new** position with a new ID. Use it to line positions up with the history queries. This is a string because an int64 exceeds the JavaScript safe-integer range. Parsing it as a number silently loses precision. The contract this position belongs to. Cross-reference with `configs` to get the contract name and scales. Position mode: `"OneWay"` for one-way mode, `"L"` for hedge-mode long, or `"S"` for hedge-mode short. Absolute position quantity (raw integer string). Divide by `10^qtyScale` from `configs` to get the display value. Notional value at which the position was opened (raw integer string). Divide by `10^priceScale` from `configs` to get the display value. `true` if the position is long (net buyer); `false` if short (net seller). Current leverage applied to this position. `true` if this position uses isolated margin; `false` if it draws from the cross margin pool. The cumulative funding index recorded when this position was last settled. Used to compute accrued-but-unpaid funding: `(currentFundingIndex - fundingIndex) × size`. Mark-price unrealized profit or loss (raw integer string). Negative values represent a loss. Net funding already settled on this position, cumulative and signed (raw integer string). **Positive means net received, negative means net paid.** Maintenance margin allocated to this individual position (raw integer string). Estimated liquidation price (raw integer string) — the mark price at which this position would be liquidated. `"0"` means **unavailable or not applicable**. Render it as blank, never as a price of zero. It is returned when no mark price exists, when collateral so far exceeds notional that no non-negative price triggers liquidation, or for an isolated position in a PORTFOLIO account. Positions on a PORTFOLIO account carry the additional markers `shareType`, `groupId`, and `settleCoinId`. A PORTFOLIO **cross** position computes `liqPx` on its group's combined basis; a PORTFOLIO **isolated** position always reports `"0"`, because such positions are assessed as part of their group rather than individually. ### Account-Level Fields These fields are always returned regardless of the `marketDeployerId` value you pass. Account margin model: `"Unified"` for standard cross margin, or `"Portfolio"` for portfolio margin with share groups. Account-level monotonic deposit nonce — the value the next deposit should carry. Use this to verify deposit sequencing and detect missing or replayed deposit events. Per-user taker fee-rate override under this market deployer, as originally configured. **`null` means unset** — the rate falls back to the owner or contract fee. `0` is an explicit 0%, and a negative value is a rebate. Per-user maker fee-rate override under this market deployer, as originally configured. `null` means unset, with the same fallback and sign rules as `takerOverrideBps`. The taker rate actually in force for this account under this market deployer, after resolving the per-user override against the market deployer's fee table. **`null` means neither layer is set** — the rate then falls back to each contract's default, which can differ per contract, so query it per contract. The maker rate actually in force, resolved the same way as `effectiveTakerBps`. Account-level market-order slippage cap in basis points, used to price market orders as `mark price ± marketSlippageBps / 1e4`. **Always a positive value and never `null`**; defaults to `1000` (10%). Set it with [`updateSlippageSetting`](/exchange/update-slippage-setting). Every `*Bps` fee field returns JSON `null` when unset — the response never carries a sentinel number. `0` (a genuine 0% rate) and negative values (rebates) are real values and must not be treated as "unset". Chain-level available balances: `{coinId, amount}`. These reflect funds available for deposit into the exchange. Per-contract trading settings: `{contractId, marginMode, positionMode, leverage}`. Reflects your current configuration for each contract. Share group views for PORTFOLIO margin mode accounts. Each entry reflects the combined equity and margin state of a portfolio group. Only populated when `marginShareType` is `"Portfolio"`. # User Agents Source: https://docs.upsidemax.xyz/info/user-agents List all agent wallets authorized to sign actions on behalf of a master account, including their addresses, names, expiry timestamps, and slot types. The `userAgents` query returns all agent (API wallet) slots registered for a master account. Agents are sub-wallets that can sign and submit actions on behalf of your master account without exposing your master key. Each agent has an address, an optional label, an expiry time, and a slot type (named or anonymous). This endpoint returns both active and expired agents so you can audit your authorization state and free up quota by revoking or replacing expired entries. Expired agents are still listed in the response. You can see them, revoke them, or replace them to free up agent quota for new authorizations. ## Request ```json theme={null} {"type": "userAgents", "accountId": "5"} ``` Must be `"userAgents"`. Your master account ID. Returns all agent slots associated with this account. ## Response ```json theme={null} { "type": "userAgents", "accountId": "5", "agents": [ { "address": "0xabc...123", "name": "bot1", "validUntil": "0", "isNamed": true } ] } ``` ### Agent Fields The wallet address of the agent. This is the address that signs actions submitted on behalf of your master account. Human-readable label assigned to this agent. Returns an empty string for anonymous slots where no name was provided at registration. Expiry of this agent's authorization as a Unix millisecond timestamp string. `"0"` means the agent has no expiry and is permanently authorized until explicitly revoked. `true` if this agent occupies a named slot (counts against the named agent quota). `false` if this agent occupies an anonymous slot (counts against the anonymous agent quota). Named and anonymous agents draw from separate quota pools. # User Fills Source: https://docs.upsidemax.xyz/info/user-fills Query your historical fills with per-fill fee, realized PnL, and position context. Paginated by time window and filterable by contract. The `userFills` query returns your account's historical fill detail. A single match produces **two rows on chain** — one for the taker and one for the maker — and each side sees only its own row, carrying that side's fee, realized PnL, and position context. Counterparty identity is never disclosed. For fills as they happen, subscribe to the [`userFills`](/websocket/user-fills) WebSocket channel. For orders still working in the book, use [`userOrders`](/info/user-orders). `userFills`, [`orderHistory`](/info/order-history), and [`userFundingFlows`](/info/user-funding-flows) are the three history queries. They share one endpoint, one set of request parameters, and one pagination contract, documented below; their field names match the WebSocket push payloads exactly. ## Request ```json theme={null} {"type": "userFills", "accountId": "5", "contractId": 1} ``` Must be `"userFills"`. Your account ID. Must be greater than zero. A numeric value is also accepted. Restrict results to one contract. Use `0` or omit for all contracts. Inclusive start of the time window, in Unix milliseconds. `0` or omitted means unbounded. Inclusive end of the time window, in Unix milliseconds. `0` or omitted means unbounded. A `startTime` later than `endTime` returns `400 BAD_REQUEST`. Maximum rows in this page. Defaults to **1000**, which is also the maximum; a larger value returns `400 BAD_REQUEST`. ## Response ```json theme={null} { "type": "userFills", "accountId": "5", "contractIdFilter": 1, "fills": [ { "timeMs": 1700000000000, "height": 42, "seq": 3, "sideRole": "Taker", "execId": "900", "contractId": 7, "marketDeployerId": 5, "orderId": "101", "orderSide": "B", "price": "50000", "qty": "3", "notional": "150000", "fee": "12", "realizedPnl": "56", "liquidateType": "None", "positionBefore": "90", "positionSide": "L", "orderType": "L", "orderPrice": "49500" } ], "count": 1, "truncated": false } ``` ### Envelope fields Echoes the `contractId` filter that was applied. `0` means no filter. Number of rows in this page. When `count` equals your `limit`, another page may be available. `true` when the page was cut short at a row boundary because the response reached its byte ceiling. Continue paginating from the last row. ### Fill object fields Fill time in Unix milliseconds. Block height at which the fill was recorded. Sequence number within the block. Your role in this fill: `"Taker"` or `"Maker"`. Unique execution ID for this row. Use it to deduplicate rows when pages overlap. Contract on which the fill occurred. Market deployer the contract belongs to. Your own order ID for this row. `"B"` for buy, `"S"` for sell. Execution price (raw integer string). Filled quantity (raw integer string). Notional value of the fill (raw integer string). Your fee for this fill, signed — a negative value is a rebate. Your realized PnL on this fill, signed. Liquidation classification. `"None"` for an ordinary fill; see the enum table below. Your position size immediately before this fill, signed. `"OneWay"`, `"L"` (long), or `"S"` (short). Type of **your own** order on this row. See the enum table below. The limit price of your order — **not** the execution price in `price`. Returns `"0"` for a market order. There is no order-level fee total in this response. To get the total fee for an order, sum the per-fill `fee` values sharing the same `orderId`. ## Pagination Results are returned in **ascending time order** (oldest first), at most `limit` rows per page. There is no cursor token — page forward through the time window: Send the query with your filters and an optional `startTime`. Read `timeMs` from the final row of the page and pass it as `startTime` on the next request. `startTime` is inclusive, so the row you paged from repeats. Discard duplicates by `execId`. A page where `count` is less than `limit` is the last one. While `count` equals `limit`, keep going. ## Value conventions * 64-bit integers — IDs, prices, quantities, and amounts — are returned as **JSON strings** to preserve precision. * `height`, `timeMs`, and small integers such as `contractId` and `seq` are returned as **bare numbers**. * Enumerations are returned as strings. Any value the server does not recognize is returned as `"Unknown"`. * Prices and quantities are raw integers — convert them with the contract's `priceScale` and `qtyScale` from [`configs`](/info/configs). ### Enumerations | Field | Values | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `orderSide` | `B` (buy) · `S` (sell) | | `orderType` | `L` (limit) · `M` (market) · `SL` (stop limit) · `SM` (stop market) · `TPL` (take-profit limit) · `TPM` (take-profit market) | | `positionSide` | `OneWay` · `L` (long) · `S` (short) | | `sideRole` | `Taker` · `Maker` | | `liquidateType` | `None` · `ForceLiquidate` · `ForceClose` · `AdlLiquidate` · `AdlClose` · `OffsetLiquidate` | | Any unrecognized value | `Unknown` | # User Funding Flows Source: https://docs.upsidemax.xyz/info/user-funding-flows Query your historical funding settlements per position, with the funding index before and after each settlement for exact reconciliation. The `userFundingFlows` query returns your account's funding settlement records, **one row per position per settlement**. Each row carries the funding index before and after the settlement, the amount paid or received, and the resulting balance. It shares its request parameters, pagination contract, and value conventions with [`userFills`](/info/user-fills) and [`orderHistory`](/info/order-history). ## Request ```json theme={null} {"type": "userFundingFlows", "accountId": "5"} ``` Must be `"userFundingFlows"`. Your account ID. Must be greater than zero. A numeric value is also accepted. Restrict results to one contract. Use `0` or omit for all contracts. Inclusive start of the time window, in Unix milliseconds. `0` or omitted means unbounded. Inclusive end of the time window, in Unix milliseconds. `0` or omitted means unbounded. A `startTime` later than `endTime` returns `400 BAD_REQUEST`. Maximum rows in this page. Defaults to **1000**, which is also the maximum; a larger value returns `400 BAD_REQUEST`. ## Response ```json theme={null} { "type": "userFundingFlows", "accountId": "5", "contractIdFilter": 0, "fundingFlows": [ { "timeMs": 1700000000000, "height": 42, "seq": 3, "contractId": 7, "marketDeployerId": 5, "settleCoinId": 2, "positionSize": "3000", "positionSide": "L", "fundingIndexBefore": "104900000", "fundingIndexAfter": "104950000", "amount": "-150", "balanceAfter": "987654", "fundingRate": "12500", "oraclePrice": "50000" } ], "count": 1, "truncated": false } ``` ### Envelope fields Echoes the `contractId` filter that was applied. `0` means no filter. Number of rows in this page. When `count` equals your `limit`, another page may be available. `true` when the page was cut short at a row boundary because the response reached its byte ceiling. ### Funding flow object fields Settlement time in Unix milliseconds. This is the field you page on. Block height at which the settlement was recorded. Sequence number within the block. Contract the position belongs to. Market deployer the contract belongs to. Coin the funding was settled in. Size of the position at settlement (raw integer string). `"OneWay"`, `"L"` (long), or `"S"` (short). Cumulative funding index before this settlement. Cumulative funding index after this settlement. Funding settled, signed — **negative means you paid**, positive means you received. Balance after the settlement was applied. Funding rate of the most recent interval covered by this row. Mark price used for the most recent interval covered by this row, where `ΔIndex = rate × price`. `fundingRate` and `oraclePrice` describe only the **most recent** interval. When a settlement spans several intervals, `amount` is their cumulative total and multiplying the rate by the price will not reproduce it. For exact reconciliation, use the difference between `fundingIndexBefore` and `fundingIndexAfter` — that is the same source `amount` is derived from. ## Pagination Rows come back in ascending `timeMs` order, at most `limit` per page. Pass the last row's `timeMs` as the next request's `startTime` and deduplicate the inclusive boundary row. A page with `count` below `limit` is the last one. See [`userFills`](/info/user-fills) for the full walkthrough. # User Market Deployers Source: https://docs.upsidemax.xyz/info/user-market-deployers Retrieve the list of market deployer IDs your account is currently enrolled in, so you can verify access before trading or transferring collateral. The `userMarketDeployers` query returns the list of market deployer IDs your account is enrolled in. Use this to verify enrollment before attempting to trade or transfer collateral in a deployer — operations against a deployer you are not enrolled in will be rejected. If the returned list is empty or missing the expected deployer ID, complete enrollment before proceeding. ## Request ```json theme={null} {"type": "userMarketDeployers", "accountId": "5"} ``` Must be `"userMarketDeployers"`. Your account ID, obtained from the `registerAccount` action. ## Response ```json theme={null} { "type": "userMarketDeployers", "accountId": "5", "marketDeployerIds": [1] } ``` Array of market deployer IDs your account is currently enrolled in. An empty array means your account has not been enrolled in any deployer yet. # User Orders Source: https://docs.upsidemax.xyz/info/user-orders Retrieve all active open orders for your account within a market deployer, with optional filtering by contract ID to narrow results. The `userOrders` query returns all active orders for your account within a market deployer. You can filter the results to a specific contract by passing its `contractId`, or set it to `0` to retrieve orders across all contracts. Use the returned `id` field to reference orders in cancel or modify actions. ## Request ```json theme={null} { "type": "userOrders", "accountId": "5", "marketDeployerId": 1, "contractId": 0 } ``` Must be `"userOrders"`. Your account ID, obtained from the `registerAccount` action. The market deployer to query orders within. Filter orders by contract. Use `0` or omit to return orders across all contracts. Pass a positive integer to restrict results to that specific contract. ## Response ```json theme={null} { "type": "userOrders", "accountId": "5", "marketDeployerId": 1, "contractIdFilter": 0, "orders": [ { "id": "6", "clientOrderId": "0", "accountId": "5", "contractId": 1, "marginMode": "C", "positionSide": "OneWay", "orderSide": "B", "orderType": "L", "timeInForce": "Gtc", "price": "50", "size": "10", "originalSize": "10", "leverage": "10", "status": "Open", "reduceOnly": false } ] } ``` ### Order Fields Exchange-assigned order ID (int64 as a decimal string). Use this value in cancel and modify actions to reference the order. Your client-assigned order ID set at placement time (the `c` field). `"0"` means no client ID was assigned. The contract this order is placed on. Cross-reference with `configs` for the contract name and scales. `"B"` for a buy (bid) order; `"S"` for a sell (ask) order. `"L"` for a limit order; `"M"` for a market order. Order duration policy: `"Gtc"` (Good Till Cancel), `"Ioc"` (Immediate Or Cancel), or `"Alo"` (Add Liquidity Only / post-only). Order price as a raw integer string. Divide by `10^priceScale` from `configs` to get the display value. **Remaining** quantity as a raw integer string — it decreases as the order fills. This is **not** the quantity you originally submitted. Divide by `10^qtyScale` from [`configs`](/info/configs) to get the display value. The quantity originally submitted, raw and unchanging. **Filled quantity is `originalSize − size`** — use the pair to render a "filled of total" column. Leverage applied to this order at placement time. Current order status: `"Open"` (resting), `"Filled"` (fully matched), or `"Canceled"` (removed from book). `"C"` for cross margin; `"I"` for isolated margin. `true` if this order can only reduce an existing position (it will be rejected or auto-canceled if it would open or increase a position). `true` when the order is a **position-level** TP/SL, whose closing direction follows the position automatically. `false` for an ordinary order or a standalone trigger order. It corresponds one-to-one with the `isPositionTpsl` parameter of the [`tpSl`](/exchange/tp-sl) action. When this order is a TP/SL child, the ID of its parent order. Otherwise `"0"`. Take-profit attached to this order at placement. Present whether or not the order has filled, so a resting order can display its armed exit. Contains: * **`isSet`** *(boolean)* — whether a take-profit is armed. The invariant `isSet == (triggerPrice > 0)` always holds, so this single field is enough to test for presence. * **`triggerPrice`** *(string)* — trigger price (raw). `"0"` when `isSet` is `false`. * **`price`** *(string)* — order price used after triggering (raw). `"0"` means close at market. * **`triggerType`** *(int)* — price source: `0` for mark price, `1` for oracle price. Only these two values are returned. * **`size`** *(string)* — quantity to close (raw). `"0"` closes the whole position. Stop-loss attached to this order at placement. Same fields as `tp`. ### Conditional orders A conditional order carries `isConditional: true` and a `parentOrderId`; every other field is serialized identically to an ordinary order. **Read a conditional order's own trigger price from the top-level `triggerPrice` and `triggerPriceType` — not from its `tp` / `sl` object.** Those objects describe a take-profit or stop-loss *attached at placement*, which a conditional order never has, so their `isSet` is always `false`. An ordinary resting order reports `"0"` for the top-level `triggerPrice`. The same `tp` / `sl` objects appear on order entries pushed by the [`orderUpdates`](/websocket/order-updates) channel, field for field, so one parser handles both. # SDKs Source: https://docs.upsidemax.xyz/sdk/overview Choose how to integrate with UpsideMAX — a typed language SDK, or the REST and WebSocket APIs directly. Every path uses EIP-712 wallet signing, no API keys. Pick the integration path that fits your stack. SDKs wrap the REST and WebSocket API behind a typed client so you don't hand-build EIP-712 signatures or envelopes; the raw APIs work from any language. ## Choose your integration path Typed client for reads, signed writes, and streams. `pip install upside-python-sdk`. Call `POST /exchange` and `POST /info` directly from any language. Realtime order book, trades, candles, and per-address account streams. More language SDKs are on the way. ## What every SDK handles * **EIP-712 signing, no API keys.** Writes are authorized by a wallet signature over a fixed domain; the SDK picks the Agent or Typed path and manages nonces. See [Authentication](/guide/authentication). * **Raw integer prices and sizes.** The wire protocol is integer-only — convert display values with each contract's `priceScale` / `qtyScale` from [`configs`](/info/configs); never hardcode a scale. * **Asynchronous order placement.** A submitted order returns `status: "accepted"` with a count, not the resting order id — read the result from the Info API or the `orderUpdates` / `userFills` WebSocket channels. SDKs default to **Devnet** (`https://dev.upsidemax.xyz`). For Mainnet access, contact the UpsideMAX team. # Python SDK Source: https://docs.upsidemax.xyz/sdk/python/overview Install and get started with the official UpsideMAX Python SDK — EIP-712 wallet-signed REST reads and writes plus realtime WebSocket streams, with no API keys. 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. 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 a **threaded WebSocket** client that auto-reconnects. * Raw-dict responses, typed inputs, and full type hints (ships `py.typed`). * Agent (API-wallet) delegation, TP/SL, leverage / margin, and collateral actions. ## Installation ```bash theme={null} pip install upside-python-sdk ``` Requires **Python 3.9+**. Runtime dependencies: `requests`, `websocket-client`, `eth-account`, `eth-utils`. ## Quick start ```python theme={null} from upside import Info, Exchange from upside.utils import constants # --- reads (no signing) --- info = Info(base_url=constants.QA_API_URL) cfg = info.configs() contract = next(c for c in cfg["contracts"] if c["status"] == "Active") asset = contract["contractId"] print(info.market_state(asset)) # --- writes (EIP-712 signed) --- exchange = Exchange("0x", base_url=constants.QA_API_URL) # Register (Devnet requires an alpha test invitation code from the UpsideMAX team). # A 10,000 USDC test airdrop lands within ~10s. exchange.register_account(invite_code="") # Place a resting limit buy. Prices and sizes are raw integer strings — # scale them with the contract's priceScale / qtyScale from configs. exchange.order(asset=asset, is_buy=True, size="10", price="50") ``` ## The two clients Market data, account and order queries, and realtime WebSocket subscriptions. No wallet or signing required. Orders, cancels, margin and leverage, TP/SL, collateral, 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`](/info/configs) — never hardcode a scale. For example, on a contract with `priceScale = 2`, a display price of `500.00` is sent as `"50000"`. * **Order placement is asynchronous.** A batch returns `{"status": "accepted", "response": {"type": "order", "data": {"count": n}}}` — **not** the resting order id. Read the result from [`Info.user_orders`](/sdk/python/reading-data) / `orders_by_cloids`, or the `orderUpdates` / `userFills` WebSocket channels. Cancels, modifies, and margin actions respond synchronously. * **HTTP 200 does not always mean success.** Gateway-level failures (invalid signature, reused nonce, rate limiting) raise `ClientError` (4xx) or `ServerError` (5xx). Business rejections come back as HTTP 200 — a per-item `error` string inside `statuses[]`, or a non-zero `errorCode` inside `response.data`. Always inspect the response. ## Error handling Every SDK error derives from `UpsideError`: | Exception | Raised when | | ---------------- | ------------------------------------------------------------------------------- | | `ClientError` | The gateway returns a 4xx (bad request, invalid signature, invite/nonce issue). | | `ServerError` | The gateway returns a 5xx (including `DOWNSTREAM_TIMEOUT`). | | `WebsocketError` | A streaming/connection error on the WebSocket client. | | `APIError` | Base class for the HTTP errors above. | ```python theme={null} from upside import ClientError, ServerError try: exchange.order(asset=1, is_buy=True, size="10", price="50") except ClientError as e: print("rejected before processing:", e) except ServerError as e: print("server error, safe to re-check state:", e) ``` ## Environments The SDK defaults to **Devnet** (`https://dev.upsidemax.xyz`), exposed as `constants.QA_API_URL`. Pass `base_url` explicitly to target a different environment. For Mainnet access, contact the UpsideMAX team. 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` / chain id `9767` / zero verifying contract). The SDK selects the correct path automatically — the **Typed path** for `registerAccount`, `approveAgent`, `revokeAgent`, and the collateral actions, and the **Agent path** for everything else. Nonces are strictly increasing millisecond timestamps managed per `Exchange` instance. See [Authentication](/guide/authentication) for the full scheme. # Reading data with Info Source: https://docs.upsidemax.xyz/sdk/python/reading-data Use the UpsideMAX Python SDK Info client for market data, account and order queries, and realtime WebSocket subscriptions — no wallet or signing required. The `Info` client covers every read: market data, account and order queries, and realtime WebSocket streams. It needs no wallet and signs nothing. Every method returns the raw parsed JSON — see the [Info API](/info/overview) pages for the exact response shapes. ```python theme={null} from upside import Info from upside.utils import constants info = Info(base_url=constants.QA_API_URL) # WebSocket starts automatically ``` Pass `skip_ws=True` to construct a REST-only client (no background WebSocket thread). Call `info.close()` when you are done to stop the WebSocket cleanly. ## Market data ```python theme={null} info.configs() # contracts, coins, scales, tiers — cache this info.l2_book(asset) # full order-book snapshot info.market_state(asset) # mark / oracle / last price, funding info.candle_snapshot(asset, "1m", start_ms, end_ms) # historical OHLCV ``` `configs` is the source of truth for contract IDs, coin IDs, and the `priceScale` / `qtyScale` / `tickSize` / `stepSize` you need to convert display values to the raw integers the API expects. Fetch it once and cache it. ## Account & orders ```python theme={null} info.user_account(account_id, market_deployer_id) # balances + positions info.user_market_deployers(account_id) # market deployers the account is enrolled in info.user_agents(account_id) # authorized API wallets info.user_orders(account_id, market_deployer_id, contract_id=0) # active open orders (0 = all contracts) info.orders_by_ids(market_deployer_id, ["8280"]) # look up by exchange order id info.orders_by_cloids(account_id, market_deployer_id, ["1778844423064"]) # look up by client order id info.share_group_state(group_id=0) # portfolio-margin share group ``` `account_id` accepts an int or string; ids that are not found are simply omitted from the `orders_by_ids` / `orders_by_cloids` results. ## WebSocket streams The `Info` client manages one shared WebSocket connection. Subscribe with a subscription dict and a callback; the callback receives each decoded message. `subscribe` returns an integer subscription id you pass back to `unsubscribe`. ```python theme={null} sid = info.subscribe( {"type": "l2Book", "asset": "1"}, lambda m: print(m["data"]["bookVersion"]), ) info.subscribe({"type": "trades", "asset": "1"}, print) # Per-address (private) channels take the wallet address as `user`: info.subscribe({"type": "orderUpdates", "user": "0x
"}, print) info.subscribe({"type": "userFills", "user": "0x
"}, print) info.unsubscribe({"type": "l2Book", "asset": "1"}, sid) info.close() ``` **Channels** — see the [WebSocket](/websocket/overview) reference for payloads: | Scope | Channels | | ----------- | --------------------------------------------- | | Public | `l2Book`, `bbo`, `trades`, `candle`, `config` | | Per-address | `orderUpdates`, `openOrders`, `userFills` | The client pings every 30 seconds and automatically reconnects, replaying its active subscriptions. The WebSocket does **not** push position or balance changes — poll [`user_account`](/info/user-account) for those. # Trading with Exchange Source: https://docs.upsidemax.xyz/sdk/python/trading Use the UpsideMAX Python SDK Exchange client for signed writes — orders, cancels, margin and leverage, TP/SL, collateral transfers, and agent delegation. The `Exchange` client performs every state-changing operation. Each call is signed with your wallet key using EIP-712; the SDK builds the envelope, selects the signing path, and manages the nonce for you. ```python theme={null} from upside import Exchange from upside.utils import constants exchange = Exchange("0x", base_url=constants.QA_API_URL) ``` `wallet` may be a private-key hex string or an `eth_account` LocalAccount. Optional arguments: `account_id` (set it when signing with an agent key — see [Agent delegation](#agent-api-wallet-delegation)) and a shared `nonce_manager`. Prices and sizes are **raw integer strings**, scaled by the contract's `priceScale` / `qtyScale` from [`configs`](/info/configs). Order placement is **asynchronous** and HTTP 200 does not always mean success — see [key behaviors](/sdk/python/overview#key-behaviors). ## Register an account ```python theme={null} # Devnet requires an alpha test invitation code from the UpsideMAX team. On success a # 10,000 USDC test airdrop lands within ~10s, and account_id is captured. exchange.register_account(invite_code="") print(exchange.account_id) ``` ## Orders ```python theme={null} from upside import Cloid exchange.order(asset=1, is_buy=True, size="10", price="50", cloid=Cloid.from_int(1001)) exchange.order(asset=1, is_buy=True, size="10", price="50", tif="Alo") # Gtc | Ioc | Alo | Fok exchange.market_order(asset=1, is_buy=False, size="5") exchange.bulk_orders([...]) # up to 10 orders, one signature ``` `order` builds a single-order `bulk_orders` call. For builder-fee sharing, pass `builder_address` / `builder_fee`; in a batch only `orders[0]`'s builder fields apply to the whole request. A **client order id** (`cloid`) is a positive int64; wrap it with `Cloid.from_int(...)` so you can locate the order later with `orders_by_cloids` or `cancel_by_cloid`. ### Cancel & modify ```python theme={null} exchange.cancel(asset=1, oid=15) exchange.cancel_by_cloid(asset=1, cloid=1001) exchange.cancel_all(asset=1) # per-contract exchange.bulk_cancel([...]) # up to 10 cancels exchange.modify(asset=1, oid=15, price="151", size="8") # locate by oid (or cloid) ``` `modify` changes only price / size / TIF / cloid — provide just the fields you want to change; side, asset, and reduce-only cannot change. ## Margin & leverage ```python theme={null} exchange.update_leverage(asset=1, leverage=20) exchange.update_margin_mode(asset=1, is_cross=False, is_hedge=False) # isolated under ONE_WAY exchange.update_isolated_margin(asset=1, ntli=5000) # ntli > 0 adds, < 0 removes ``` `update_margin_mode` requires no open position or orders on the contract. Omit `is_hedge` to keep the current position mode. Isolated margin works directly under ONE\_WAY; **HEDGE position mode is currently unavailable** and any request passing `is_hedge=True` is rejected. See [Update Margin Mode](/exchange/update-margin-mode). ## Conditional orders (TP/SL) ```python theme={null} exchange.tp_sl(asset=1, tp_price="90000", sl_price="80000") # whole-position, market on trigger exchange.cancel_tp_sl(asset=1) exchange.cancel_conditional(oid=123) ``` At least one of `tp_price` / `sl_price` must be greater than `0`. A limit price of `"0"` fires a market IOC on trigger; a value above `0` places a GTC limit. A size of `"0"` closes the whole position. Trigger type selects the reference price (`0` = mark, `1` = index, `2` = last). ## Collateral transfers ```python theme={null} exchange.lock_collateral(market_deployer_id=1, coin_id=1, amount="1000") exchange.unlock_collateral(market_deployer_id=1, coin_id=1, amount="1000") exchange.transfer_between_deployers(1, 2, coin_id=1, amount="1000") ``` ## Agent (API-wallet) delegation Keep the master key offline and authorize a hot agent key to sign trades. The server routes an agent-signed action to the master account from the recovered signer, so the agent `Exchange` keeps `account_id` pointed at the master. ```python theme={null} # Master authorizes an agent. With no address, a fresh key is generated and # returned so you can build the agent client. response, agent_key = master.approve_agent(agent_name="bot1") agent = Exchange(agent_key, base_url=constants.QA_API_URL, account_id=master.account_id) agent.order(asset=1, is_buy=True, size="10", price="50") master.revoke_agent(agent.address) ``` `approve_agent` and `revoke_agent` are master-only. `valid_until` is a Unix-ms expiry (`0` = permanent). See [API Wallets](/agents/api-wallets) for the delegation model. # WebSocket All Markets Source: https://docs.upsidemax.xyz/websocket/all-markets Subscribe to allMarkets for the oracle price, mark price, and 24-hour volume of every contract in a single frame, pushed roughly once a second. The `allMarkets` channel pushes the current **oracle price, mark price, and 24-hour volume for every contract** in one frame, about once per second. One subscription covers the whole market — you do not need a separate [`ticker`](/websocket/ticker) subscription per contract. This is a public channel and requires no authentication. It carries three values per contract by design. For a single contract's full statistics — price change, high, low, trade count, funding rate — use [`ticker`](/websocket/ticker). ## Subscribing ```json theme={null} {"method": "subscribe", "subscription": {"type": "allMarkets"}} ``` This channel takes **no parameters** — there is no `asset` field. ## Push message format ```json theme={null} { "msg": "AllMarkets", "channel": "allMarkets", "data": { "ts": 1754453600000, "markets": [ {"asset": "1", "oraclePx": "1012", "markPx": "1010", "vol24h": "30000"}, {"asset": "2", "oraclePx": "500", "markPx": "500", "vol24h": "0"} ] } } ``` ## Field reference Generation time of the frame, in Unix milliseconds. One entry per contract that currently has a price. Each entry contains: * **`asset`** *(string)* — the contract ID. * **`oraclePx`** *(string)* — current oracle price, raw integer scaled by `priceScale`; `"0"` when no data. * **`markPx`** *(string)* — current mark price, raw integer scaled by `priceScale`; `"0"` when no data. * **`vol24h`** *(string)* — 24-hour rolling volume in base units, raw integer scaled by `qtyScale`. Measured on the same basis as `volume` in the [`ticker`](/websocket/ticker) channel. ## Consuming the stream Index `markets` by `asset` and replace your local map from each frame. Frames are full snapshots, never deltas. Coverage is all active contracts that have a price. A contract with a price but no trades yet reports `vol24h` as `"0"` — that is not an error. Apply each contract's `priceScale` and `qtyScale` from [`configs`](/info/configs) before display; never hardcode a scale. Use `allMarkets` to drive a market list or ticker tape, then subscribe to [`ticker`](/websocket/ticker) only for the contract the user has open. # Authentication Source: https://docs.upsidemax.xyz/websocket/authentication Send an Auth message over WebSocket to associate your connection with an account and unlock private channel subscriptions on UpsideMAX. Authenticating your WebSocket connection tells the server which account the connection belongs to. While some private channels currently accept an account address directly in the subscription parameters, sending an `Auth` message first is the recommended pattern — it ensures your connection is ready for all private streams and is required for any future access-controlled features. ## Sending the Auth message After opening the WebSocket connection, send the following JSON frame: ```json theme={null} {"msg": "Auth", "accountId": 3} ``` Must be the fixed string `"Auth"`. This identifies the message type to the server. Your numeric account ID. This is the value returned by the `registerAccount` action — not your wallet address. ## Server response The server replies with an `AuthResult` frame on the same connection: ```json theme={null} {"msg": "AuthResult", "success": true, "accountId": 3} ``` `true` if the authentication was accepted. `false` if the `accountId` was not found or was invalid. Echoes back the `accountId` from your request so you can correlate the response in async handlers. ## Error response If authentication fails, the server returns a `success: false` result: ```json theme={null} {"msg": "AuthResult", "success": false, "accountId": 999} ``` Check that the `accountId` was registered on-chain via `registerAccount` before retrying. ## After authenticating Once you receive `"success": true`, you can subscribe to any channel — including `orderUpdates`, `openOrders`, and `userFills` — and the server will associate those streams with your account context. Private channels (`orderUpdates`, `openOrders`, `userFills`) currently accept the account address directly in the subscription `user` field without requiring prior `Auth`. However, authenticating first is strongly recommended for forward compatibility — the access model may tighten in future releases. On a dropped connection the client auto-reconnects after \~5 seconds and the server **restores your authentication and subscriptions automatically** — you don't need to re-send `Auth` or re-subscribe. # WebSocket BBO Channel Source: https://docs.upsidemax.xyz/websocket/bbo Subscribe to bbo over WebSocket for the best bid and ask on every book change — unthrottled, and far lower bandwidth than the full l2Book channel. The `bbo` channel delivers only the best bid (buy-one) and best ask (sell-one) for a contract. If your application only needs the spread or top-of-book price — for example, to display a mid-price or check whether an order would cross — `bbo` is significantly more bandwidth-efficient than subscribing to the full `l2Book`. `bbo` and `l2Book` are produced from the same book change, but **`bbo` is not throttled**: a frame is pushed on every change, so it arrives faster and more frequently than `l2Book`. Pushing begins at the first book change after you subscribe — there is no snapshot on subscribe. ## Subscribing ```json theme={null} {"method": "subscribe", "subscription": {"type": "bbo", "asset": "1"}} ``` Replace `"1"` with the numeric contract ID you want to track. ## Push message format ```json theme={null} { "channel": "bbo", "ts": 1782279307885, "data": { "asset": "1", "time": 1782279300000, "bookVersion": 90231, "bbo": [ {"px": "1133770", "sz": "5", "n": 3}, {"px": "1133780", "sz": "2", "n": 1} ] } } ``` ## Field reference Server send timestamp in Unix milliseconds. Use this for latency measurement and event ordering across channels. The contract ID this update belongs to. Block timestamp in Unix milliseconds at which this top-of-book snapshot was captured. The same monotonically increasing version counter used by `l2Book` for the same asset. You can use this to correlate `bbo` messages with `l2Book` messages or to deduplicate when multiple updates arrive for the same block. Best bid. `null` when there are no resting bids on the book. * **`px`** *(string)* — Best bid price (raw). * **`sz`** *(string)* — Total size available at the best bid (raw). * **`n`** *(number)* — Number of orders resting at the best bid price. Best ask. `null` when there are no resting asks on the book. * **`px`** *(string)* — Best ask price (raw). * **`sz`** *(string)* — Total size available at the best ask (raw). * **`n`** *(number)* — Number of orders resting at the best ask price. ## Comparing bbo and l2Book * You only need to display or act on the top-of-book price. * You are building a price ticker, mid-price display, or spread monitor. * You want to minimise data transfer on mobile or metered connections. * You are subscribing to many assets simultaneously and bandwidth matters. * You need to render a full depth chart or order book ladder. * You are estimating market impact or slippage for larger orders. * You need `markPx` and `oraclePx` alongside the book levels. * Your strategy reacts to liquidity at multiple price levels. Because `bbo` is unthrottled and `l2Book` is rate-limited, you will receive **more** `bbo` frames than `l2Book` frames for the same asset. Both `bbo` and `l2Book` share the same `bookVersion` counter for a given asset. If you subscribe to both on the same connection, you can use `bookVersion` to verify they are in sync and to detect any gaps. # WebSocket Candle Channel Source: https://docs.upsidemax.xyz/websocket/candle Subscribe to the candle WebSocket channel to receive live OHLCV bar updates for a contract at your chosen interval, including closed-bar events. The `candle` channel delivers live OHLCV updates for a contract at a specified interval. The current open bar is pushed on change, throttled to roughly every 100 ms (at most ten frames per second). When a bar closes at an interval boundary, the server pushes the finalized closed bar (with `"closed": true`) immediately followed by the first update for the new open bar — so your chart never misses a transition. The subscription opens with a snapshot frame of recent bars, so a chart can be drawn from the stream alone. ## Subscribing ```json theme={null} { "method": "subscribe", "subscription": {"type": "candle", "asset": "1", "interval": "1m"} } ``` The numeric contract ID to receive candles for. The candle interval. Accepts the same values as the `candleSnapshot` REST endpoint: `1m`, `3m`, `5m`, `15m`, `30m`, `1h`, `2h`, `4h`, `8h`, `12h`, `1d`, `3d`, `1w`, `1M`. ## Push message format ```json theme={null} { "channel": "candle", "ts": 1782279307885, "data": { "s": "1", "i": "1m", "t": 1782279300000, "T": 1782279360000, "o": "110", "c": "70", "h": "110", "l": "70", "v": "15", "n": 6, "closed": false } } ``` ## Field reference Server send timestamp in Unix milliseconds. Contract ID. Matches the `asset` value in your subscription. Interval of this candle (e.g., `"1m"`). Bar open time — Unix milliseconds at the start of this interval bucket. Bar close time — Unix milliseconds at the end of this interval bucket (exclusive). Open price of the bar (raw string). This is the price of the first trade in the interval. Highest trade price within the bar (raw string). Lowest trade price within the bar (raw string). Close price of the bar (raw string). For an open bar, this is the price of the most recent trade so far. Total traded volume within the bar (raw string). Number of individual trades that make up this bar. `false` for an in-progress open bar. `true` for a finalized closed bar — this message is pushed exactly once at the interval boundary and will not be updated further. ## Snapshot on subscribe On a successful subscription the server first pushes a **snapshot frame** marked `"isSnapshot": true`. Its `data` is shaped differently from an incremental frame — an object containing roughly the **last 300 closed bars plus the current open bar**, in ascending time order: ```json theme={null} { "channel": "candle", "isSnapshot": true, "ts": 1782279307885, "data": { "asset": "1", "interval": "1m", "candles": [ {"s": "1", "i": "1m", "t": 1782279240000, "T": 1782279300000, "o": "100", "c": "110", "h": "115", "l": "95", "v": "40", "n": 12, "closed": true} ] } } ``` Incremental frames that follow carry **no** `isSnapshot` and a **single** bar in `data`. When no data exists yet, the snapshot frame carries `"data": {}`. ## Initialising a chart The first frame is the snapshot described above. Render its `candles` array as the initial chart state — no REST call is required for a standard chart window. Subsequent frames carry one bar each. Key on `data.t`: update the bar with a matching `t`, or append a new one. A message with `"closed": true` finalizes that bar. The next message carries the new open bar with a later `t`. For a window deeper than the snapshot, call [`candleSnapshot`](/info/candle-snapshot). **Do not rely on the incremental stream to fill a quiet period.** While the exchange is running, an interval with no trades is backfilled with a flat bar (`v` of `0`) so the axis stays continuous — but after a long gap with no trades, only the most recent flat bar is pushed, not each one in between. Those bars are still stored: re-subscribe to take a fresh snapshot, or call [`candleSnapshot`](/info/candle-snapshot). Do not assume the time axis has no holes. Intervals that elapse while a node is restarting or stopped are not backfilled, leaving a permanent gap in history. Render missing ranges as "no data" — prices and volumes elsewhere remain correct. An open bar is pushed **only when it changes**. If no trade has occurred since the last push, the same values are not re-sent. You can subscribe to multiple intervals for the same asset simultaneously — for example, `1m` and `1h` — each as a separate subscription. They operate independently and do not interfere with each other. All price and volume values are raw strings. Apply the contract's decimal precision before rendering values on a chart. # WebSocket Config Source: https://docs.upsidemax.xyz/websocket/config Subscribe to the config channel to receive lightweight notifications when contract configuration changes — new listings, freezes, or parameter updates. The `config` channel notifies you when any contract-level configuration changes on the exchange: a new contract is listed, an existing contract is frozen or delisted, or parameters such as fee rates, margin tiers, or risk limits are updated. The notification is intentionally lightweight — it tells you *that* something changed and *which* contract was affected, but does not include the new values. After receiving a notification, re-fetch the `configs` endpoint via REST to get the latest configuration. ## Subscribing The `config` channel requires no parameters beyond the channel type: ```json theme={null} {"method": "subscribe", "subscription": {"type": "config"}} ``` ## Push message format ```json theme={null} { "msg": "ConfigChanged", "channel": "config", "data": [ { "entityType": 3, "entityId": "10000001", "version": 42, "operation": 3 } ], "ts": 1782279307885 } ``` The `data` array may contain multiple notification objects if several contracts change configuration in the same block. ## Notification fields The type of entity that changed. Currently always `3`, representing a contract-level configuration change. The contract ID that was affected. The configuration version number after this change. You can compare this against a locally stored version to determine whether you need to re-fetch, and to detect any missed notifications. The type of change: | Value | Name | Meaning | | ----- | -------- | ----------------------------------------------------------------------------------------- | | `1` | `CREATE` | A new contract has been listed and is now available for trading | | `2` | `STATUS` | A contract's trading status changed — it was frozen, unfrozen, or delisted | | `3` | `UPDATE` | One or more contract parameters were updated (fee rates, margin tiers, risk limits, etc.) | ## Handling a config notification Your WebSocket handler receives a `ConfigChanged` message identifying the affected `entityId` and `operation`. Send a `POST /info` request with `{"type": "configs"}` to retrieve the full, updated configuration for all contracts (or filter by `entityId` if the endpoint supports it). Replace the configuration for the affected contract in your local cache. If `operation` is `CREATE`, add the new contract. If `operation` is `STATUS` and the contract is now delisted, remove it from your active contract list. The `ConfigChanged` notification does **not** include the updated configuration values. You must re-fetch `configs` via `POST /info` after receiving this event to get the new parameters. Treating the notification alone as a source of truth will result in stale configuration data. Subscribe to `config` at application startup alongside your other channel subscriptions. This ensures you are notified of new listings immediately — useful if your application auto-populates a tradeable asset list. Contract configuration changes are rare compared to price and order events. Sending the full configuration payload on every change — which can be large when margin tier tables are involved — would waste bandwidth for most subscribers. The lightweight notification pattern lets you re-fetch only when necessary, keeping the WebSocket stream efficient. # WebSocket L2 Book Source: https://docs.upsidemax.xyz/websocket/l2-book Subscribe to l2Book over WebSocket to receive a full L2 order book snapshot on every book change, rate-limited to one frame per 20 ms — no polling required. The `l2Book` channel delivers a complete order book snapshot for a contract **every time the book changes**, rate-limited to at most one frame per **20 ms** per contract. Every message includes all price levels for both bids and asks, so you can always replace your local book state in full rather than applying partial diffs. When you subscribe, the server immediately pushes the current snapshot, so you get a consistent starting state without making a separate REST call. No change is lost to the rate limit: only the newest state is kept, and a frame is emitted once the interval elapses, so the book reaches you within one interval at the latest. ## Subscribing ```json theme={null} {"method": "subscribe", "subscription": {"type": "l2Book", "asset": "1"}} ``` Replace `"1"` with the numeric contract ID you want to track. You can hold simultaneous `l2Book` subscriptions for multiple assets on the same connection. ## Push message format The server pushes a message like the following whenever the book changes: ```json theme={null} { "channel": "l2Book", "ts": 1782270780000, "data": { "asset": "1", "time": 0, "bookVersion": 1, "markPx": "0", "oraclePx": "0", "levels": [ [ {"px": "200", "sz": "20", "n": 2}, {"px": "100", "sz": "10", "n": 1} ], [] ] } } ``` ## Field reference Frame send time in Unix milliseconds, from the server clock. Sits at the top level alongside `data`, and is consistent with the `bbo`, `trades`, and `candle` frames. The contract ID this snapshot belongs to. Matches the `asset` value in your subscription. Snapshot timestamp in Unix milliseconds. May be `0` when the exchange is in a pre-launch state. Monotonically increasing version counter for this asset's order book. Use this to detect duplicate or out-of-order messages — discard any message whose `bookVersion` is lower than or equal to the last one you processed. Current mark price as a raw string. Returns `"0"` when mark price is not yet available. Current oracle price as a raw string. Returns `"0"` when oracle price is not yet available. A two-element array: `levels[0]` contains bids sorted **high-to-low** by price; `levels[1]` contains asks sorted **low-to-high** by price. An empty array means no orders on that side. Each element within a level array is an object with the following fields: * **`px`** *(string)* — Price of this level (raw). * **`sz`** *(string)* — Total size resting at this price (raw). * **`n`** *(number)* — Number of individual orders aggregated at this price. ## Handling the initial snapshot The server pushes the first snapshot immediately on subscribe. You can safely use this as your starting state: Send the subscribe message. Once the subscription is acknowledged the server pushes the current book directly to you, in exactly the same frame format as subsequent updates. A contract with no book data yet sends nothing until its first change. Each subsequent push is also a full snapshot. Replace your entire local book (both sides) with the new `levels` — there is no need to apply diffs or maintain a patch log. If you subscribe to both `l2Book` and `bbo` for the same asset, compare `bookVersion` values to avoid processing stale data when messages arrive out of order. Use the WebSocket `l2Book` channel instead of polling the REST `/info` `l2Book` endpoint. You receive the initial snapshot automatically on subscribe, and every subsequent push keeps your local state current without any request overhead. All price and size values are returned as raw strings. Apply the appropriate decimal precision for the contract when displaying values to users. # WebSocket Open Orders Source: https://docs.upsidemax.xyz/websocket/open-orders Subscribe to openOrders to receive a full snapshot of all active orders on subscribe, followed by real-time incremental updates as orders change state. The `openOrders` channel is the best starting point for managing an account's order state in real time. On subscribe, the server immediately pushes a complete snapshot of every currently active order — both regular open orders and untriggered conditional (TP/SL) orders. All subsequent changes are delivered as incremental update messages using the same compact format as [`orderUpdates`](/websocket/order-updates). This combination means you can always maintain a fully consistent local view of active orders. Pass the account's wallet address (not the numeric account ID) as the `user` parameter. ## Subscribing ```json theme={null} { "method": "subscribe", "subscription": {"type": "openOrders", "user": "0xabc...123"} } ``` ## Initial snapshot Immediately after subscribing, the server sends an `OpenOrdersSnapshot` message: ```json theme={null} { "msg": "OpenOrdersSnapshot", "channel": "openOrders.0xabc...123", "data": [ { "id": "6", "clientOrderId": "0", "accountId": "5", "contractId": 1, "marginMode": "C", "positionSide": "OneWay", "orderSide": "B", "orderType": "L", "timeInForce": "Gtc", "price": "50", "size": "10", "originalSize": "10", "leverage": "10", "status": "Open", "reduceOnly": false } ], "ts": 1782279307885 } ``` The snapshot uses expanded field names for clarity. The `data` array contains one object per active order. An empty array means the account has no open orders at subscription time. ## Snapshot field reference Unique order ID assigned by the exchange. Client-assigned order ID. `"0"` if not set when placing the order. Numeric account ID that owns this order. The contract this order is placed on. Margin mode: `"C"` = cross margin, `"I"` = isolated margin. Position side: `"OneWay"` for one-way mode; `"Long"` or `"Short"` for hedge mode. `"B"` = buy (bid), `"S"` = sell (ask). `"L"` = limit, `"M"` = market. `"Gtc"` (good-till-cancel), `"Ioc"` (immediate-or-cancel), `"Alo"` (add-liquidity-only / post-only), or `"Fok"` (fill-or-kill). Order price (raw string). The quantity originally submitted, raw and unchanging. Filled quantity is `originalSize − size`. Remaining unfilled size (raw string). Leverage applied to this order at placement time. `"Open"` for a regular active order; `"Untriggered"` for a pending conditional order. `true` if this order can only reduce an existing position. ## Incremental update messages After the snapshot, subsequent state changes arrive as `OpenOrdersUpdate` messages: ```json theme={null} { "msg": "OpenOrdersUpdate", "channel": "openOrders.0xabc...123", "data": [ { ...compact entry... } ], "ts": 1782279307885 } ``` Update entries use the same compact short-field format as `orderUpdates` — see the [orderUpdates field reference](/websocket/order-updates#field-reference) for the full mapping. ## Maintaining local order state Subscribe and buffer any incoming `OpenOrdersUpdate` messages until you receive the `OpenOrdersSnapshot`. This prevents a race condition where an update arrives before the snapshot. Store every order in the snapshot keyed by `id`. Orders with `"status": "Untriggered"` are pending conditional orders. For each `OpenOrdersUpdate`, update or remove orders from your local map: upsert if the order is new or modified; remove if `"st": "Filled"` or `"st": "Canceled"`. Pending conditional orders appear in the snapshot with `"isConditional": true` and `"status": "Untriggered"`. When a conditional order triggers, it is removed from the open orders state and a new regular order appears with a new `id`. If you only need ongoing changes without the initial snapshot overhead, subscribe to [`orderUpdates`](/websocket/order-updates) instead. `openOrders` is the right choice when you need a guaranteed consistent starting state. # WebSocket Order Updates Source: https://docs.upsidemax.xyz/websocket/order-updates Subscribe to orderUpdates to receive real-time incremental order state changes — new orders, partial fills, cancels, and TP/SL events. The `orderUpdates` channel sends incremental state changes for every order associated with an account — the moment an order is placed, partially filled, fully filled, cancelled, or when a conditional (TP/SL) order triggers or is cancelled. On subscribe the server first sends one frame of recent **order history** (see [History snapshot on subscribe](#history-snapshot-on-subscribe)); live increments follow. That snapshot contains only orders that have already terminated — for a full view of orders currently resting, subscribe to [`openOrders`](/websocket/open-orders) or query [`userOrders`](/info/user-orders). Pass the account's wallet address (not the numeric account ID) as the `user` parameter. ## Subscribing ```json theme={null} { "method": "subscribe", "subscription": {"type": "orderUpdates", "user": "0xabc...123"} } ``` ## Push envelope All order update messages share this envelope structure: ```json theme={null} { "msg": "OrderUpdate", "channel": "orderUpdates.0xabc...123", "data": [ { "...": "..." } ], "ts": 1782279307885 } ``` The `data` array may contain multiple entries when several orders change state in the same block. ## Regular order entry ```json theme={null} { "id": "15", "r": "144115188075856500", "si": 0, "cid": "1778844423064", "a": "5", "c": 1, "b": "B", "t": "L", "tif": "Gtc", "p": "50", "s": "10", "st": "Open" } ``` ## Field reference | Field | Type | Description | | ----------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique order ID assigned by the exchange | | `r` | string | Server-assigned request ID (debugging only). It is not known at submit time, so do not use it to correlate — match on `cid`, or on the signed `nonce` for a [rejected entry](#rejected-order-entry) | | `si` | number | **subIndex** — the position of this order within the submitted batch, starting at `0`; always `0` for a single-order request. Combine with `n` to map a push back to an exact element of your `orders[]` | | `cid` | string | Client order ID — omitted if you did not set one when placing the order | | `a` | string | Account ID that owns this order | | `c` | number | Contract ID | | `b` | string | Direction: `"B"` = buy, `"S"` = sell | | `t` | string | Order type: `"L"` limit, `"M"` market, `"SL"` stop limit, `"SM"` stop market, `"TPL"` take-profit limit, `"TPM"` take-profit market | | `tif` | string | Time-in-force: `"Gtc"` (good-till-cancel), `"Ioc"` (immediate-or-cancel), `"Alo"` (add-liquidity-only / post-only), `"Fok"` (fill-or-kill) | | `p` | string | Order price (raw) | | `s` | string | Remaining unfilled size (raw) — equals total size minus already-filled size | | `st` | string | Order status: `"Open"`, `"Filled"`, `"Canceled"`, or `"Rejected"` (see [Rejected order entry](#rejected-order-entry)) | | `tp` / `sl` | object | Take-profit / stop-loss **attached to this order at placement** — `{"isSet": bool, "triggerPrice": "…", "price": "…", "triggerType": int, "size": "…"}`, field for field identical to the `tp` / `sl` objects in REST [`userOrders`](/info/user-orders). Present even while the order is still resting, so one parser serves both | On a regular entry `tp` is an **object**; on a [conditional entry](#conditional-tp-sl-order-entry) `tp` is a **scalar trigger price**. Check for `"cond": true` before parsing it. ## History snapshot on subscribe Immediately after a successful subscription the server pushes **one frame carrying up to the last 10 terminated orders**, so a client can render history without a separate REST call. It differs from an incremental frame in two ways: | | Incremental frame | Snapshot frame | | ------ | ----------------- | ------------------------------------------------------------ | | `msg` | `OrderUpdate` | **`OrderHistorySnapshot`** | | `data` | array of entries | **object** `{"rows": [...], "count": N, "truncated": false}` | ```json theme={null} { "msg": "OrderHistorySnapshot", "channel": "orderUpdates.0xabc...123", "data": {"rows": [ { "...": "..." } ], "count": 10, "truncated": false}, "ts": 1782279307885 } ``` **`rows[]` uses different field names from the incremental entries — you cannot reuse one parser for both.** A private-channel snapshot passes through the corresponding REST response body verbatim, so it uses the full REST column names (`order_id`, `contract_id`, `price`) while increments use the compact wire names (`id`, `c`, `p`). Reuse your REST parser for the snapshot. ### Snapshot row fields | Field | Type | Description | Incremental equivalent | | -------------------- | ------ | ----------------------------------------------------------------------------------------------------------------- | -------------------------- | | `updated_time_ms` | number | Time the order terminated (ms) | — | | `created_time_ms` | number | Time the order was placed (ms) | — | | `height` | number | Block height at termination | — | | `seq` | number | Sequence within the block | — | | `order_id` | string | Order ID | `id` | | `cl_ord_id` | string | Client order ID; `"0"` when none was set | `cid` | | `contract_id` | number | Contract ID | `c` | | `market_deployer_id` | number | Market deployer ID | — | | `side` | number | **`66` = buy, `83` = sell** (ASCII `B` / `S`) | `b` (string `"B"` / `"S"`) | | `ord_type` | number | `1` limit, `2` market, `3` stop limit, `4` stop market, `5` take-profit limit, `6` take-profit market | `t` (string) | | `price` | string | Order price (raw) | `p` | | `orig_qty` | string | Original quantity (raw) | — | | `filled_qty` | string | Filled quantity (raw) | — | | `leaves_qty` | string | Remaining quantity (raw) | `s` | | `status` | number | `0` Open, `1` Filled, `2` Canceled, `3` Untriggered | `st` (string) | | `origin` | number | How it terminated: `0` normal, `1` modify, `2` liquidation, `3` conditional trigger, `4` passive fill, `5` cancel | — | | `cancel_reason` | number | Cancellation reason | — | | `reject_code` | number | Rejection code; `0` means not rejected | `code` | Working with the snapshot: * The payload is at **`data.rows`**, and `data` is an object rather than an array. (`openOrders` also sends an object, but its payload key is `orders`.) * **Quoted fields are big integers carried as strings** (`order_id`, `cl_ord_id`, `price`, every `*_qty`). They can exceed the JavaScript safe-integer range — never pass them through `Number()` before comparing or echoing them back. * `side`, `ord_type`, and `status` are **numeric** here and strings in the incremental frames. Map them before display. * It contains **only terminated orders**. Orders still working are not included — use the [`openOrders`](/websocket/open-orders) first frame or [`userOrders`](/info/user-orders) for those. * **Fewer than 10 rows, or none at all, is normal** — the account may have little history, or the history service may be briefly unavailable. Do not treat it as an error. * The snapshot and the increments that follow **may overlap**. Deduplicate by order ID (`order_id` in the snapshot, `id` in increments); nothing is dropped. * The snapshot is fixed at 10 rows, with no paging and no cursor. For deeper history use [`orderHistory`](/info/order-history). ## Cancel entry When an order is cancelled, a compact entry is sent with `"st": "Canceled"`. The `s` field carries the remaining unfilled size at the time of cancellation. ```json theme={null} { "id": "15", "r": "144115188075856500", "cid": "1778844423064", "a": "5", "c": 1, "s": "10", "st": "Canceled" } ``` ## Rejected order entry If the matching engine rejects an order (for example, the contract doesn't exist, insufficient margin, or a position-mode mismatch), a `"st": "Rejected"` entry is pushed here. **A rejected order appears only on this channel** — it is never assigned an order ID, never appears in [`openOrders`](/websocket/open-orders), and cannot be found by any order-ID or client-order-ID query. ```json theme={null} { "id": "-1", "n": "1743600000000", "r": "144115188075856500", "a": "5", "c": 1, "code": 19, "reason": "account not enrolled", "st": "Rejected" } ``` | Field | Type | Description | | -------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | `"-1"` for a rejected order placement, which has no order ID. A **rejected cancel** echoes the real order ID when the target order exists (for example it had already filled), and you can correlate on that directly | | `n` | string | **The correlation key.** The `nonce` you chose when submitting the order, echoed back — use it to match this push to the order you sent | | `si` | number | subIndex within the submitted batch, starting at `0`; always `0` for a single-item request. Combine with `n` to identify which element of `orders[]` / `cancels[]` was rejected | | `b` | string | Direction, present **only when a valid side can be resolved** — omitted when it cannot, such as a rejected cancel | | `r` | string | Request ID, for debugging only. Do **not** correlate on it — it is server-assigned and unknown at submit time | | `cid` | string | Client order ID — present only if you set one; when present, prefer it for correlation | | `a` | string | Account ID | | `c` | number | Contract ID | | `code` | number | Rejection error code | | `reason` | string | Rejection reason (max 40 characters; truncated if longer) | | `st` | string | Always `"Rejected"` | A rejected entry omits `t` / `tif` / `p` / `s` — the order never became active, so it has none of those attributes. `b` appears only when a valid direction can be resolved. ## Conditional (TP/SL) order entry Conditional orders include `"cond": true` and use a different set of status values: ```json theme={null} { "id": "20", "a": "5", "c": 1, "b": "S", "t": "M", "p": "0", "s": "10", "tp": "80000", "tpt": 0, "ro": true, "cond": true, "st": "Untriggered" } ``` Additional fields for conditional orders: | Field | Type | Description | | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cond` | boolean | `true` identifies this as a conditional order | | `tp` | string | Trigger price (raw) | | `tpt` | number | Trigger price type: `0` = mark price, `1` = oracle price | | `ro` | boolean | `true` if this is a reduce-only order | | `ts` | number | **Block time at which the conditional order was created** (Unix ms). This is inside the entry and is *not* the envelope's frame-send `ts` — same name, different meaning | Conditional order statuses: | Status | Meaning | | ------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `Untriggered` | Conditional order is active and waiting for the trigger price | | `Canceled` | Conditional order was cancelled before triggering | | `Triggered` | Trigger price was reached; the order has been promoted to a regular order and a new entry with the regular order ID will follow | `orderUpdates` provides only incremental changes — there is no initial snapshot on subscribe. If you need the full list of currently active orders as a starting point, subscribe to [`openOrders`](/websocket/open-orders) first. Set a client order ID (`cid`) when you place an order, then match incoming `OrderUpdate`s on `cid` to link each update back to your submission — especially useful when placing orders in rapid succession. The `r` request ID is server-assigned and unknown at submit time, so it cannot serve as the correlation key. A rejected order never receives an `id`, so it is correlated by the `nonce` you signed (see [Rejected order entry](#rejected-order-entry)). # WebSocket Overview Source: https://docs.upsidemax.xyz/websocket/overview Connect to the UpsideMAX WebSocket at wss://dev.upsidemax.xyz/ws for real-time order book, trades, candles, and private account updates. The UpsideMAX WebSocket API delivers real-time push streams for market data and account state without polling. Once you open a connection to `wss://dev.upsidemax.xyz/ws`, the server pushes updates as they occur — book channels push on every book change, and timed channels such as `ticker` and `allMarkets` push about once a second. Private account channels such as order updates and fills are available to any subscriber who provides their account address, and authenticating first is strongly recommended for forward compatibility. ## Connection lifecycle Establish a WebSocket connection to `wss://dev.upsidemax.xyz/ws`. No HTTP upgrade headers beyond the standard WebSocket handshake are required. Send an `Auth` message to associate the connection with your account. This is required before subscribing to private channels and recommended for all account-level subscriptions. See [Authentication](/websocket/authentication) for details. Send a `subscribe` message for each channel you want to receive. You can subscribe to multiple channels on the same connection. See [Subscription](/websocket/subscription) for the message format. The server pushes messages to you as events occur. Each message includes a `channel` field that identifies its source so you can route it to the correct handler. Send a standard WebSocket `ping` frame every 30 seconds. The server replies with a `Pong` to keep the connection alive. ## Available channels The table below summarises every channel available on the WebSocket API. Public channels are accessible without any prior authentication step. | Channel | Auth Required | Description | | -------------- | ------------- | -------------------------------------------------------------------------------- | | `l2Book` | No | Full order book snapshots, pushed on book change (at most one per 20 ms) | | `bbo` | No | Best bid/ask on every book change, unthrottled — bandwidth-efficient top-of-book | | `trades` | No | Public trade stream for a contract | | `candle` | No | Real-time OHLCV candles at a chosen interval | | `ticker` | No | 24-hour rolling statistics for one contract, pushed about every second | | `allMarkets` | No | Oracle price, mark price, and 24-hour volume for every contract in one frame | | `config` | No | Contract configuration change notifications | | `orderUpdates` | No\* | Incremental order state changes for an account | | `openOrders` | No\* | Active orders snapshot plus incremental updates | | `userFills` | No\* | All fills where your account is taker or maker | | `userAccount` | No\* | Full account view — collateral, positions, margin — pushed every 3 seconds | \* Private channels currently accept the account address directly in the subscription parameters without requiring a prior `Auth` message. Authentication is still recommended for future compatibility. ## Reconnection If the connection drops, the client automatically reconnects after **5 seconds**. On reconnect, your authentication and subscription state are automatically restored — you do not need to manually re-send your `Auth` message or re-subscribe to your channels. The WebSocket endpoint does not guarantee message ordering across different channels. If you need causal ordering between, for example, `openOrders` and `orderUpdates`, correlate messages using their `ts` timestamps and `bookVersion` / order ID fields. ## Channel quick-reference Full order book snapshots pushed whenever the book changes — bids and asks with price, size, and order count. Best bid and best ask only — lower bandwidth than l2Book for top-of-book use cases. All public trades for a contract as they execute on-chain. Live OHLCV candle updates at any supported interval, with closed-bar events. Incremental order state changes — placed, filled, cancelled, and TP/SL events. Initial snapshot of all active orders followed by real-time incremental updates. Every fill involving your account — whether you were the taker or the maker. 24-hour rolling statistics for a contract — change, high, low, volume, funding rate, mark and oracle price. Oracle price, mark price, and 24-hour volume for every contract in a single frame. The full account view — collateral, positions, and margin — pushed on a timer instead of polled. Lightweight notifications when contract configuration changes — new listings, freezes, or parameter updates. # Subscription Source: https://docs.upsidemax.xyz/websocket/subscription Learn how to subscribe and unsubscribe to WebSocket channels on UpsideMAX using the method + subscription object protocol. Every channel on the UpsideMAX WebSocket follows the same subscribe/unsubscribe protocol. You send a JSON message with a `method` field set to `"subscribe"` or `"unsubscribe"`, and a `subscription` object that specifies the channel type and any required parameters. The server acknowledges each request and begins (or stops) pushing messages on that channel. ## Subscribing to a channel Send the following JSON frame to start receiving messages on a channel: ```json theme={null} {"method": "subscribe", "subscription": {"type": "l2Book", "asset": "1"}} ``` The server responds with a `subscriptionResponse` acknowledgement: ```json theme={null} { "channel": "subscriptionResponse", "data": { "method": "subscribe", "subscription": {"type": "l2Book", "asset": "1"} } } ``` The `data` field mirrors your original request so you can match acknowledgements to subscriptions in async code. ## Unsubscribing from a channel Send the same `subscription` object with `"method": "unsubscribe"` to stop receiving messages: ```json theme={null} {"method": "unsubscribe", "subscription": {"type": "l2Book", "asset": "1"}} ``` The server sends a matching `subscriptionResponse` with `"method": "unsubscribe"` to confirm. The older `{"msg": "Subscribe", "channels": [...]}` array format is **no longer accepted**. All clients must use the `method` + `subscription` object format shown above. ## Error messages If a subscription request is invalid, the server responds on the `error` channel: ```json theme={null} {"channel": "error", "data": {"code": "BAD_SUBSCRIPTION", "message": "Missing required parameter: asset"}} ``` | Code | Meaning | | ------------------- | --------------------------------------------------------------------------------- | | `BAD_SUBSCRIPTION` | One or more required subscription parameters are missing or have an invalid value | | `NOT_AUTHENTICATED` | The requested channel requires an authenticated connection — send `Auth` first | ## All subscription types and parameters The table below lists every channel type, its required parameters, and whether authentication is needed. | type | Parameters | Auth? | | -------------- | -------------------------------------------- | ----- | | `l2Book` | `asset` | No | | `bbo` | `asset` | No | | `trades` | `asset` | No | | `candle` | `asset`, `interval` | No | | `ticker` | `asset` | No | | `allMarkets` | *(none)* | No | | `orderUpdates` | `user` (account address) | No\* | | `openOrders` | `user` (account address) | No\* | | `userFills` | `user` (account address) | No\* | | `userAccount` | `user` (account address), `marketDeployerId` | No\* | | `config` | *(none)* | No | \* Private channels accept the account address in the `user` field without requiring a prior `Auth` message today. Authenticating first is recommended for forward compatibility. You can hold multiple active subscriptions on a single WebSocket connection. There is no hard limit on the number of simultaneous subscriptions, but subscribing to many high-frequency channels (e.g., `l2Book` for many assets) on one connection may increase message processing latency on the client side. ## Quick examples by channel type ```json theme={null} {"method": "subscribe", "subscription": {"type": "l2Book", "asset": "1"}} ``` ```json theme={null} {"method": "subscribe", "subscription": {"type": "bbo", "asset": "1"}} ``` ```json theme={null} {"method": "subscribe", "subscription": {"type": "trades", "asset": "1"}} ``` ```json theme={null} {"method": "subscribe", "subscription": {"type": "candle", "asset": "1", "interval": "1m"}} ``` ```json theme={null} {"method": "subscribe", "subscription": {"type": "orderUpdates", "user": "0xabc...123"}} ``` ```json theme={null} {"method": "subscribe", "subscription": {"type": "openOrders", "user": "0xabc...123"}} ``` ```json theme={null} {"method": "subscribe", "subscription": {"type": "userFills", "user": "0xabc...123"}} ``` ```json theme={null} {"method": "subscribe", "subscription": {"type": "config"}} ``` # WebSocket Ticker Source: https://docs.upsidemax.xyz/websocket/ticker Subscribe to ticker for a contract's 24-hour rolling statistics pushed roughly once a second — price change, high, low, volume, funding rate, mark and oracle price. The `ticker` channel pushes a contract's **24-hour rolling statistics** about once per second. Each frame carries the same fields as one entry of the REST [`ticker`](/info/ticker) response, so you can drive a market header or watchlist row from the stream alone. This is a public channel and requires no authentication. To track every contract at once with a lighter payload, use the [`allMarkets`](/websocket/all-markets) channel instead. ## Subscribing ```json theme={null} {"method": "subscribe", "subscription": {"type": "ticker", "asset": "1"}} ``` The contract ID to track, as a decimal string. Subscribe once per contract. ## Push message format ```json theme={null} { "msg": "Ticker", "channel": "ticker.1", "data": { "asset": "1", "lastPx": "1010", "openPx": "1000", "priceChange": "10", "priceChangePct": "1.0000", "highPx": "1010", "lowPx": "990", "volume": "30000", "count": 3, "windowStartMs": 1754367200000, "fundingRate": "125", "fundingTime": 1754450000000, "markPx": "1010", "oraclePx": "1012" }, "ts": 1754453600000 } ``` ## Field reference The contract ID this frame describes. Most recent trade price (raw integer string). Baseline price 24 hours ago — the earliest trade inside the window. `lastPx − openPx`, signed (raw integer string). Percentage change over the window, signed, to four decimal places. Already a percentage — do not rescale it. Returns `"0"` when `openPx` is `0`. Highest trade price within the window. Lowest trade price within the window. Traded volume over the window in base units (raw integer string). Number of trades within the window. Start of the window in Unix milliseconds, or the earliest trade for a contract listed less than 24 hours ago. Current funding rate per funding interval, signed, as a raw fixed-point integer with a scale of **1e8** (`1e8` = `100%`). The true rate is `fundingRate / 1e8`; display percent is `fundingRate / 1e6`. Returns `"0"` if never published. The scale is `1e8` — **not** basis points (`1e4`) and not `1e6`. Unix millisecond timestamp at which the current funding rate was set. `0` if never set. Current mark price (raw integer string), from the same source as [`marketState`](/info/market-state). `"0"` if never published. Current oracle (index) price (raw integer string), from the same source as [`marketState`](/info/market-state). `"0"` if never published. Frame send time in Unix milliseconds. ## Consuming the stream Every frame is a full snapshot of the rolling window, not a delta. Overwrite your local ticker state on each frame. Within a single push cycle, all contracts share the same window endpoints, so values across contracts are directly comparable. Apply the contract's `priceScale` and `qtyScale` from [`configs`](/info/configs) before display. `priceChangePct` is already a percentage. `markPx` and `oraclePx` follow price publication rather than trading activity, so a quiet contract still streams live prices with `volume` at `"0"`. # WebSocket trades Channel Source: https://docs.upsidemax.xyz/websocket/trades Subscribe to the trades WebSocket channel to receive a real-time stream of all public trades for a contract as they execute on-chain. The `trades` channel streams every public trade for a contract — regardless of which account was involved. Each push message contains an array of one or more trade objects, all of which executed in the same block. If you only care about fills from your own account, use the [`userFills`](/websocket/user-fills) channel instead. ## Subscribing ```json theme={null} {"method": "subscribe", "subscription": {"type": "trades", "asset": "1"}} ``` Replace `"1"` with the numeric contract ID whose trade stream you want to receive. ## Push message format ```json theme={null} { "channel": "trades", "ts": 1782279307885, "data": [ { "asset": "1", "px": "1133770", "sz": "5", "time": 0, "side": "B", "tid": 8842931 } ] } ``` Multiple trade objects may appear in the `data` array when several trades execute within the same block. Process them in array order. ## Trade object fields Contract ID. Matches the `asset` in your subscription. Trade execution price (raw string). Apply the contract's decimal precision before displaying to users. Trade size (raw string). This is the notional quantity that changed hands at `px`. Indicates which side initiated the trade. `"B"` means the buyer was the aggressor (taker); `"S"` means the seller was the aggressor (taker). Globally unique trade ID assigned on-chain. Use this to deduplicate trades if you receive the same block's data from multiple sources. Reserved field; currently `0`. Use the outer `ts` field on the envelope for event timing. ## Outer envelope fields Server send timestamp in Unix milliseconds. Use this as the authoritative timestamp for all trades in the `data` array. ## Backfill on subscribe On a successful subscription the server first pushes a **history snapshot frame**, marked `"isSnapshot": true`, whose `data` array holds up to the **last 100 trades** for the contract in ascending time order. Live incremental frames follow and do **not** carry `isSnapshot`. When the contract has no trade history the snapshot frame carries `"data": []`. ```json theme={null} { "channel": "trades", "isSnapshot": true, "ts": 1782279307885, "data": [ {"asset": "1", "px": "1133770", "sz": "5", "time": 1782279300000, "side": "B", "tid": 8842930} ] } ``` In the backfill frame, `time` is the real trade time. In live incremental frames `time` is currently `0` — use the envelope `ts` as the authoritative timestamp there. ## Building a trade history feed Send the subscribe message. The first frame carries up to 100 recent trades — render it as your initial tape. Subsequent frames arrive without `isSnapshot`. Ordering between the backfill and the live stream guarantees no gap; a small overlap is possible. Use `tid` as the unique key when merging the backfill with the live stream, or when combining with any other source. For longer history than the 100-trade backfill, or for aggregated OHLCV, use the REST [`candleSnapshot`](/info/candle-snapshot) endpoint. To build a volume-weighted average price (VWAP) over a rolling window, accumulate `px × sz` from incoming trade messages, divide by the rolling total `sz`, and reset the window at your chosen interval. # WebSocket User Account Source: https://docs.upsidemax.xyz/websocket/user-account Subscribe to userAccount for a full account snapshot every few seconds — equity, collateral, positions, and margin availability without polling the REST API. The `userAccount` channel pushes the **complete account view** for one account within one market deployer — collateral, positions, and margin — on a timer. The payload is identical to the REST [`userAccount`](/info/user-account) response. A first frame arrives immediately on subscribe, then a fresh frame **every 3 seconds**. Use it to keep equity, available margin, position size, and unrealized PnL current without polling. ## Subscribing ```json theme={null} { "method": "subscribe", "subscription": {"type": "userAccount", "user": "0xabc...123", "marketDeployerId": 1} } ``` The account's wallet address, not the numeric account ID. The server resolves it to an account; an unregistered address receives no frames. The market deployer to report on. The account view differs per deployer, so subscribe once per deployer you care about. This carries the same meaning as `marketDeployerId` in the REST query. Unsubscribe with the same `subscription` object and `"method": "unsubscribe"`. ## Push message format ```json theme={null} { "msg": "UserAccount", "channel": "userAccount.0xabc...123", "data": { "type": "userAccount", "accountId": "1", "marketDeployerId": 1, "crossEquity": "1000000000000", "marginAvailable": "1000000000000", "totalPositionIM": "643246", "crossPositionMM": "0", "crossCollaterals": [{"coinId": 1, "amount": "1000000000000"}], "positions": [ {"contractId": 1, "size": "5", "leverage": "10", "unrealizedPnl": "0", "mm": "0"} ] }, "ts": 1782279307885 } ``` The sample above is abbreviated. `data` carries the full REST response body. ## Field reference Always `"UserAccount"` for this channel. The channel this frame belongs to, keyed by the subscribed address. The account view. Every margin, collateral, position, and account-level field is defined exactly as in the REST [`userAccount`](/info/user-account) response — see that page for the complete field reference. Frame send time in Unix milliseconds, from the server clock. ## Maintaining local state The first frame arrives immediately and is a complete snapshot. Use it as your starting state. Each frame is a **full snapshot, not a delta**. Overwrite your local account state wholesale — there are no patches to apply. Prices and quantities are raw integers. Convert with the contract's `priceScale` and `qtyScale` from [`configs`](/info/configs). `positions[].unrealizedPnl` is computed against the mark price and is recalculated on every frame, so it moves even when your position does not. # WebSocket User Fills Source: https://docs.upsidemax.xyz/websocket/user-fills Subscribe to userFills to receive every fill involving your orders in real time — whether you were the taker aggressor or the resting maker. The `userFills` channel delivers every fill where your account is one of the counterparties — both fills where you were the **taker** (your order crossed the book and aggressed) and fills where you were the **maker** (your resting order was hit). To determine your role in each fill, compare the aggressor order ID (`aid`) and the resting order ID (`rid`) against your own order IDs. For the full public trade stream regardless of account, use the [`trades`](/websocket/trades) channel instead. Pass the account's wallet address (not the numeric account ID) as the `user` parameter. ## Subscribing ```json theme={null} { "method": "subscribe", "subscription": {"type": "userFills", "user": "0xabc...123"} } ``` ## Push message format ```json theme={null} { "msg": "TradeFill", "channel": "fills.0xabc...123", "data": [ { "c": 1, "aid": "15", "rid": "12", "b": "B", "p": "1133770", "s": "5", "e": "8842931", "sp": "-5", "ps": 0 } ], "ts": 1782279307885 } ``` The `data` array may contain multiple fill objects when several fills execute in the same block. Process them in array order. ## Fill object fields Contract ID on which the fill occurred. Taker (aggressor) order ID — the order that crossed the book and matched against the resting order. Maker (resting) order ID — the order that was already in the book and was hit by the aggressor. Direction of the aggressor: `"B"` means the buyer was the aggressor (bought into resting asks); `"S"` means the seller was the aggressor (sold into resting bids). Fill execution price (raw string). Apply the contract's decimal precision before displaying to users. Fill size (raw string) — the quantity that changed hands at price `p`. Execution ID — globally unique on-chain identifier for this fill. Use this to deduplicate fills if you receive the same event from multiple sources. **Your** signed position size *before* this fill settles: positive for long, negative for short, `0` if flat. The value is already picked for your side based on whether you were the taker (`aid`) or maker (`rid`) — you don't derive it. Compute your post-fill position incrementally as `startPosition ± size`; when one resting order fills in several pieces, carry it forward (fill N's `sp` equals the position computed from fill N−1). Position data is private, so this field appears only on the per-address `userFills` channel — never on the public [`trades`](/websocket/trades) channel. Which of your position buckets this fill lands in: `0` = ONE\_WAY, `1` = LONG, `2` = SHORT. In HEDGE mode a contract has separate long and short positions, so `sp` must be accumulated per `ps` bucket. In ONE\_WAY mode `ps` is always `0` and can be ignored. ## Determining your role If `aid` matches one of your order IDs, your order was the aggressor. You paid the taker fee and your order actively crossed the spread to execute. ```text theme={null} aid == your_order_id → you are the taker ``` If `rid` matches one of your order IDs, your order was the resting maker. You received the maker rebate (if applicable) and your limit order was hit by the incoming aggressor. ```text theme={null} rid == your_order_id → you are the maker ``` ## Envelope fields `"TradeFill"` for a live incremental frame, `"TradeFillHistorySnapshot"` for the [history snapshot](#history-snapshot-on-subscribe) sent on subscribe. `"fills."` — the address you subscribed with. Server send timestamp in Unix milliseconds. Use this as the authoritative event time for all fills in the `data` array. ## History snapshot on subscribe Immediately after a successful subscription the server pushes **one frame carrying up to the last 10 fills**, so a blotter can render history without a separate REST call. It differs from a live frame in two ways: | | Incremental frame | Snapshot frame | | ------ | --------------------- | ------------------------------------------------------------ | | `msg` | `TradeFill` | **`TradeFillHistorySnapshot`** | | `data` | array of fill objects | **object** `{"rows": [...], "count": N, "truncated": false}` | ```json theme={null} { "msg": "TradeFillHistorySnapshot", "channel": "fills.0xabc...123", "data": {"rows": [ { "...": "..." } ], "count": 10, "truncated": false}, "ts": 1782279307885 } ``` **`rows[]` uses different field names from the incremental fills — you cannot reuse one parser for both.** A private-channel snapshot passes through the corresponding REST response body verbatim, so it uses the full REST column names (`exec_id`, `price`, `qty`) while increments use the compact wire names (`e`, `p`, `s`). Reuse your REST parser for the snapshot. The snapshot also carries **more** than an increment: `notional`, `fee`, `realized_pnl`, `side_role`, and `liquidate_type` appear only there. ### Snapshot row fields | Field | Type | Description | Incremental equivalent | | -------------------- | ------ | ------------------------------------------------------------------------------------------------------ | --------------------------------- | | `time_ms` | number | Fill time (ms) | — | | `height` | number | Block height of the fill | — | | `seq` | number | Sequence within the block | — | | `exec_id` | string | Execution ID, unique on chain; usable as an idempotency key | `e` | | `order_id` | string | **Your own** order ID on this fill | one of `aid` / `rid` | | `contract_id` | number | Contract ID | `c` | | `market_deployer_id` | number | Market deployer ID | — | | `side` | number | **Your own** direction: `66` = buy, `83` = sell (ASCII `B` / `S`) | not equivalent to `b` — see below | | `side_role` | number | Your role in the fill: `0` = taker, `1` = maker | absent from increments | | `price` | string | Execution price (raw) | `p` | | `qty` | string | Executed quantity (raw) | `s` | | `notional` | string | Notional value of the fill (raw) | — | | `fee` | string | **Your own** fee for this fill (raw); negative is a rebate | — | | `realized_pnl` | string | **Your own** realized PnL on this fill (raw) | — | | `position_before` | string | **Your own** signed position before the fill (raw) | `sp` | | `position_side` | number | Position bucket: `0` one-way, `1` long, `2` short | `ps` | | `liquidate_type` | number | `0` none, `1` force liquidate, `2` force close, `3` ADL liquidate, `4` ADL close, `5` offset liquidate | — | Working with the snapshot: * The payload is at **`data.rows`**, and `data` is an object rather than an array. (`openOrders` also sends an object, but its payload key is `orders`.) * **`side` is already your own direction** — you do not need to derive it from taker/maker. The incremental `b` is the *aggressor's* direction, which is the opposite of yours when you were the maker. Do not run both through the same logic. * **`side_role` states your role directly**, so there is no need to compare `aid` against `rid` the way you do for increments. * `fee`, `realized_pnl`, and `position_before` are **your side's** values, not totals for the whole match. * **Quoted fields are big integers carried as strings** and can exceed the JavaScript safe-integer range — never pass them through `Number()` before comparing or echoing them back. * **Fewer than 10 rows, or none at all, is normal** — the account may have little history, or the history service may be briefly unavailable. Do not treat it as an error. * The snapshot and the increments that follow **may overlap**. Deduplicate by execution ID (`exec_id` in the snapshot, `e` in increments); nothing is dropped. * The snapshot is fixed at 10 rows, with no paging and no cursor. For deeper history use [`userFills`](/info/user-fills). Combine `userFills` with `orderUpdates` to build a complete real-time trade blotter: `orderUpdates` tells you when order status changes, and `userFills` gives you the precise fill price and size for each execution.