# Agent Wallets for Automated Trading
Source: https://upside.mintlify.app/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 production 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 production automation.
```json theme={null}
{
"action": {
"type": "approveAgent",
"agentAddress": "0xabc0000000000000000000000000000000000001",
"agentName": "market-maker-prod",
"validUntil": 1893456000000
},
"signature": {
"r": "0x...",
"s": "0x...",
"v": 28
},
"nonce": 1782259200123
}
```
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 [Read API](/info/overview) and private [WebSocket channels](/websocket/overview). 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.
When using the documented proxy or vault flow, include the target `vaultAddress` at the top level. The server verifies that the recovered signer is authorized for that target.
```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,
"vaultAddress": "0xmaster000000000000000000000000000000000001"
}
```
Build and sign the exact action shape defined by the relevant Write 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 production systems, 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, incorrect canonical JSON, double hashing, or invalid `v` | Rebuild the signed message 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.
Build canonical JSON, hash the message, and format ECDSA signatures.
Generate unique replay-protection values for each signing address.
Review the shared `POST /exchange` envelope and proxy fields.
# LLMs.txt for Upside DEX
Source: https://upside.mintlify.app/agents/llms-txt
Use Upside DEX machine-readable Markdown documentation with coding assistants, custom agents, and retrieval pipelines.
Upside DEX 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://upside.mintlify.app/llms.txt
https://upside.mintlify.app/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://upside.mintlify.app/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 Upside DEX 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://upside.mintlify.app/llms.txt
```
Download the full corpus:
```bash theme={null}
curl -fsSL https://upside.mintlify.app/llms-full.txt \
-o upside-llms-full.txt
```
A minimal Python loader:
```python theme={null}
from __future__ import annotations
import requests
DOCS_URL = "https://upside.mintlify.app/llms.txt"
def load_docs(url: str = DOCS_URL) -> str:
response = requests.get(url, timeout=20)
response.raise_for_status()
return response.text
documentation_index = load_docs()
print(documentation_index[:500])
```
## Prompt recipes
### Endpoint discovery
```text theme={null}
Using the Upside DEX 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 production-oriented implementation from the cited Upside DEX 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 Upside DEX 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
Canonical JSON, signed-message construction, keccak256, and ECDSA signature formatting.
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 production permissions in an LLM context. The model should produce an intent or unsigned payload; a separate trusted service should validate and sign it.
# AI Agents on Upside DEX
Source: https://upside.mintlify.app/agents/overview
Connect coding assistants and automated trading agents to Upside DEX using machine-readable docs, signed write requests, unsigned reads, and real-time WebSocket streams.
Upside DEX 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://upside.mintlify.app/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://upside.mintlify.app/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.
## Choose the right integration path
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 production 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.
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 Upside DEX API.
Documentation index:
https://upside.mintlify.app/llms.txt
Full documentation:
https://upside.mintlify.app/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 canonical JSON, ECDSA secp256k1 signing, and a unique nonce.
- 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.
```
## Production 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 against non-production infrastructure and small limits before increasing exposure.
# approveAgent: Authorize an API Wallet for Automated Trading
Source: https://upside.mintlify.app/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
```
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 for this agent. Omit or pass an empty string to use the anonymous agent slot. Named agents consume one of the 3 named quota slots. If a named agent with the same name already exists, this request overwrites it.
Expiry timestamp as Unix milliseconds. Pass `0` or omit entirely to create a permanent authorization that does not expire.
## 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: Cancel Resting Orders by Exchange Order ID
Source: https://upside.mintlify.app/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
```
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
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.
# cancelAll: Cancel All Open Orders for One Contract
Source: https://upside.mintlify.app/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
```
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
```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.
# cancelByCloid: Cancel Orders by Client Order ID
Source: https://upside.mintlify.app/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
```
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
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.
# cancelConditional: Cancel a Single Conditional Order
Source: https://upside.mintlify.app/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
```
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
| 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. |
# cancelTpSl: Cancel All TP/SL for a Position
Source: https://upside.mintlify.app/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
```
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.
# enrollUserToMarketDeployer: Join a Market Deployer
Source: https://upside.mintlify.app/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 Upside DEX.
A **market deployer** is an isolated trading environment with its own smart contract set and collateral pool. When you register your account, Upside DEX 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
```
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 read API to discover available IDs.
## Response
```json theme={null}
{
"status": "ok",
"requestId": "req-144115188075855890",
"response": {
"type": "enrollUserToMarketDeployer",
"data": {
"enrolled": true
}
}
}
```
### Response Fields
`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.
# lockCollateral: Deposit Funds into a Market Deployer
Source: https://upside.mintlify.app/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 (MD), 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
```
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 response includes an `errorCode` and a human-readable `errorMessage`. 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 an MD.
* **Amount ≤ 0** — the `amount` field must be a positive integer string.
```json theme={null}
{
"status": "error",
"errorCode": 4001,
"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).
# lockIntoShareGroup: Deposit Chain Funds into a Share Group
Source: https://upside.mintlify.app/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-MD 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
```
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 response includes an `errorCode` and a human-readable `errorMessage`. 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": "error",
"errorCode": 4022,
"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: Update an Open Order Price, Size, or TIF
Source: https://upside.mintlify.app/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
```
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.
* **Change price, increase size, or change TIF/cloid** — the order is removed from its current position and re-queued at the back of the new price level. It loses its original queue priority.
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
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 |
| `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)` | 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 |
# order: Place Limit and Market Orders on Upside DEX
Source: https://upside.mintlify.app/exchange/order
Place limit (GTC, IOC, ALO) or market orders on Upside DEX. Submit up to 10 orders in a single batched request using compact single-letter keys.
The `order` action lets you place one or more limit or market orders against a specific contract. Each request can contain up to 10 order objects, all signed with a single ECDSA signature. Order objects use compact single-letter keys to keep payloads small and reduce bandwidth overhead.
## Endpoint
```
POST https://dev.upsidemax.xyz/exchange
Content-Type: application/json
```
## Request Body
Place a limit order that rests in the order book until filled or cancelled (GTC), or choose IOC/ALO for alternative execution policies.
```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
}
```
Place a market order that executes immediately at the best available price. Omit the `p` field entirely for market orders.
```json theme={null}
{
"action": {
"type": "order",
"orders": [
{
"a": 1,
"b": true,
"s": "10",
"r": false,
"t": { "market": {} }
}
],
"grouping": "na"
},
"signature": { "r": "0x...", "s": "0x...", "v": 28 },
"nonce": 1778572951478
}
```
Testnet market orders may return `"price 99999 exceeds maxTicks 10000"`. Use limit orders when testing against the testnet environment.
## Action Fields
Fixed value: `"order"`.
Array of order objects to submit. Maximum **10** orders per request. See the order object fields below.
Fixed value: `"na"`. Reserved for future order grouping strategies.
## 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 open a buy/long position; `false` to open a sell/short position.
**price** — Limit price as a decimal string (e.g. `"100"`). Required for limit orders. Omit entirely for market orders.
**size** — Order quantity as a decimal string (e.g. `"10"`). Must be positive.
**reduceOnly** — When `true`, the order only reduces an existing position and will never increase or open a new one. Set to `false` for standard orders.
**orderType** — Specifies the execution type and time-in-force. See the order type formats table below.
**clientOrderId** — An int64 decimal string you assign for your own tracking (e.g. `"1778572951477"`). Use this value later with `cancelByCloid` to cancel by your own ID without needing the exchange-assigned `oid`.
## 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 whatever is available 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 at the best available price; omit `p` when using this type |
## Responses
You will receive one status entry per order in the same index order as your request.
The limit order was accepted and is waiting in the order book for a match.
```json theme={null}
{
"status": "ok",
"response": {
"type": "order",
"data": {
"statuses": [
{ "resting": { "oid": 1 } }
]
}
}
}
```
The order matched against existing liquidity and was fully or partially filled on arrival.
```json theme={null}
{
"status": "ok",
"response": {
"type": "order",
"data": {
"statuses": [
{ "filled": { "totalSz": "5", "avgPx": "100", "oid": 2 } }
]
}
}
}
```
The order was rejected. The `error` string explains the reason.
```json theme={null}
{
"status": "ok",
"response": {
"type": "order",
"data": {
"statuses": [
{ "error": "size must be positive" }
]
}
}
}
```
### Response Fields
Exchange-assigned order ID for an order that is now resting in the book. Store this value to cancel the order later via the `cancel` action.
Exchange-assigned order ID for an order that filled immediately.
Total quantity filled, as a decimal string.
Average fill price across all matched lots, as a decimal string.
Rejection reason when the order could not be placed or filled.
## Known Order Errors
| Error | Cause |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `size must be positive` | The submitted quantity is zero or negative |
| `price 99999 exceeds maxTicks 10000` | The limit price exceeds the contract's allowed maximum tick |
| `share margin group is FROZEN` | Account is in Portfolio margin mode and the share group is frozen; only `reduceOnly` orders are accepted |
For accounts using **Portfolio (PORTFOLIO) margin mode**, the system automatically draws from the shared margin pool for all contracts in the same share group. You do not need to include any additional fields in the order request to opt into this behavior.
# POST /exchange – Write API Overview for Upside DEX
Source: https://upside.mintlify.app/exchange/overview
All state-changing operations on Upside DEX route through POST /exchange and are dispatched by the action.type field in your request body.
Every write operation on Upside DEX — 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
```
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)
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)
Configure fees and delegate signing to agent wallets.
* [`updateFeeSetting`](/exchange/update-fee-setting)
* [`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, optionally filtered by contract. |
| [`modify`](/exchange/modify) | Modify the price or size of an existing open order. |
### 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. |
### Fee Settings
| Action | Description |
| -------------------------------------------------- | ---------------------------------------------- |
| [`updateFeeSetting`](/exchange/update-fee-setting) | Update the fee configuration for your account. |
### 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.
# registerAccount: Register Your Wallet on Upside DEX
Source: https://upside.mintlify.app/exchange/register-account
Register your wallet address with Upside DEX to receive a unique accountId required for all subsequent trading and collateral operations.
Before you can place orders or manage collateral on Upside DEX, 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, registration requires a valid invite code. Pass `inviteCode` at the **envelope top level** — alongside `action`, `signature`, and `nonce` — not inside the `action` object. The invite code is **not** included in the signed payload.
## Request
```
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 invite 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.
## 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 invite 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 read 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.
# revokeAgent: Remove an Authorized API Wallet
Source: https://upside.mintlify.app/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
```
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).
# setMarginShareType: Switch Account Margin Share Mode
Source: https://upside.mintlify.app/exchange/set-margin-share-type
Switch your account between UNIFIED mode (isolated per-MD margin) and PORTFOLIO mode (shared group margin pool) to control how margin is allocated.
Upside DEX 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 MD 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
```
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 response includes an `errorCode` and a human-readable `errorMessage`. 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": "error",
"errorCode": 4010,
"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.
# tpSl: Create Take-Profit and Stop-Loss Orders
Source: https://upside.mintlify.app/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
```
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": "0",
"slLimitPrice": "0",
"tpSize": "0",
"slSize": "0",
"tpTriggerType": 0,
"slTriggerType": 0
},
"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 closing order placed when the trigger fires. `true` = buy (closes a SHORT position), `false` = sell (closes a LONG position). Required when `isPositionTpsl` is `false`; inferred automatically for position-level TP/SL.
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"`.
Limit price used when the TP trigger fires. `"0"` = execute as a market IOC order after triggering. Any positive value = execute as a GTC limit order at that price.
Limit price used when the SL trigger fires. `"0"` = execute as a market IOC order after triggering. Any positive value = execute as a GTC limit order at that price.
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` = index price, `2` = last traded price. Defaults to `0`.
The price feed used to evaluate the SL trigger: `0` = mark price, `1` = index price, `2` = last traded price. Defaults to `0`.
## Response
### Success
```json theme={null}
{
"status": "ok",
"response": {
"type": "tpSl",
"data": { "tpOrderId": 123, "slOrderId": 124 }
}
}
```
### 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).
# transferBetweenDeployers: Move Funds Between Deployers
Source: https://upside.mintlify.app/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
```
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 response includes an `errorCode` and a human-readable `errorMessage`. 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": "error",
"errorCode": 4003,
"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).
# transferMdToShareGroup: Move Funds into a Share Group
Source: https://upside.mintlify.app/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-MD 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
```
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 response includes an `errorCode` and a human-readable `errorMessage`. Common causes include:
* **Amount exceeds withdrawable balance on the MD** — 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": "error",
"errorCode": 4020,
"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).
# transferShareGroupToMd: Move Funds from Share Group
Source: https://upside.mintlify.app/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-MD 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
```
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 response includes an `errorCode` and a human-readable `errorMessage`. 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": "error",
"errorCode": 4021,
"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).
# unlockCollateral: Withdraw Funds from a Market Deployer
Source: https://upside.mintlify.app/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 (MD) 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 MD 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
```
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 response includes an `errorCode` and a human-readable `errorMessage`. 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 MD must be one you are enrolled in.
```json theme={null}
{
"status": "error",
"errorCode": 4002,
"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).
# unlockFromShareGroup: Withdraw Funds from a Share Group
Source: https://upside.mintlify.app/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
```
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 response includes an `errorCode` and a human-readable `errorMessage`. 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": "error",
"errorCode": 4023,
"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).
# updateFeeSetting: Override Fee Rates per Market Deployer
Source: https://upside.mintlify.app/exchange/update-fee-setting
Set per-user taker and maker fee rate overrides within a market deployer. User-level overrides take the highest priority in the three-tier fee hierarchy.
The `updateFeeSetting` action lets you configure per-user fee rate overrides for your account within a specific market deployer. User-level fee settings take the highest priority in the three-tier hierarchy — they override both the market deployer's default rates and any contract-level defaults.
Maker fees can be negative to represent rebates, but the absolute value of the maker rate must not exceed the taker rate (`|makerBps| ≤ takerBps`). Omitting `takerBps` or `makerBps` clears that override entirely, causing your account to fall back to the owner or contract default for that rate. Explicitly passing `0` sets the fee to exactly 0% rather than clearing the override.
## Endpoint
```
POST https://dev.upsidemax.xyz/exchange
```
## Request
All requests must include an ECDSA signature and a millisecond-precision nonce.
```json theme={null}
{
"action": {
"type": "updateFeeSetting",
"marketDeployerId": 1,
"takerBps": 5,
"makerBps": -2
},
"signature": { "r": "0x...", "s": "0x...", "v": 28 },
"nonce": 1778858500000
}
```
### Action fields
Fixed value: `"updateFeeSetting"`.
The ID of the market deployer whose fee settings you are overriding for your account.
Your taker fee rate override, expressed in basis points (1 bps = 0.01%). Omit this field to clear any existing taker override and fall back to the default. Pass `0` to explicitly set a 0% taker fee.
Your maker fee rate override, expressed in basis points. Can be negative to represent a maker rebate. Omit this field to clear any existing maker override and fall back to the default. The constraint `|makerBps| ≤ takerBps` must hold.
## Response
### Success
```json theme={null}
{
"status": "ok",
"response": {
"type": "updateFeeSetting",
"data": {
"takerBefore": 10,
"makerBefore": 2,
"takerAfter": 5,
"makerAfter": -2
}
}
}
```
### Response fields
Your previous taker fee override in basis points. Returns `-32768` if no user-level taker override was previously set.
Your previous maker fee override in basis points. Returns `-32768` if no user-level maker override was previously set.
Your new taker fee override in basis points, as stored after this request.
Your new maker fee override in basis points, as stored after this request.
A `takerBefore` or `makerBefore` value of `-32768` (the minimum value for an int16) signals that no user-level override was previously configured for that rate — it is a sentinel, not a fee value.
# updateIsolatedMargin: Add or Remove Isolated Position Margin
Source: https://upside.mintlify.app/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
```
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. |
| `no isolated collateral slot` | The account has no available isolated collateral slot for this contract. |
| `insufficient cross balance` | Your cross margin pool doesn't have enough available margin to fund the addition. |
| `amount exceeds withdrawable cross margin` | The addition amount exceeds the cross margin available to transfer into isolated. |
| `not enough isolated margin to remove` | The removal amount exceeds the current isolated margin balance. |
| `removal would push effective leverage above position leverage` | After removal, the remaining isolated equity would fall below the initial margin requirement. |
| `mark price not ready` | The mark price for this contract is temporarily unavailable; retry shortly. |
# updateLeverage: Set Per-Contract Leverage
Source: https://upside.mintlify.app/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
```
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. |
# updateMarginMode: Switch Between Cross and Isolated Margin
Source: https://upside.mintlify.app/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`) 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
```
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
},
"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.
## Response
### Success
```json theme={null}
{
"status": "ok",
"response": {
"type": "updateMarginMode",
"data": { "marginModeBefore": 1, "marginModeAfter": 2 }
}
}
```
### Rejection
```json theme={null}
{
"status": "ok",
"response": {
"type": "updateMarginMode",
"data": {
"marginModeBefore": 0,
"marginModeAfter": 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.
Present only on rejection. Identifies the failure reason.
Present only on rejection. Human-readable description of the failure.
## Error reference
| `errorMessage` | Cause |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `invalid marginMode` | The requested margin mode value is not recognized. Ensure `isCross` is a valid boolean. |
| `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. |
| `ISOLATED margin mode requires HEDGE position mode` | Your account is in ONE\_WAY position mode, which is incompatible with isolated margin. Switch to HEDGE mode first. |
| `MD owner/liquidator account must stay CROSS (R17)` | Your account type is restricted to cross margin and cannot switch to isolated. |
# How to Sign Upside DEX Write Requests with ECDSA secp256k1
Source: https://upside.mintlify.app/guide/authentication
Learn how to sign POST /exchange requests using ECDSA secp256k1. The server recovers your identity from the signature — no API keys needed.
Every `POST /exchange` request must include a valid ECDSA signature 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`.
## Signing Process
Construct the action as a plain JSON object. Every action has a `type` field that identifies the operation. Additional fields depend on the action type.
```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 object to canonical JSON: **sort keys alphabetically** and **remove all whitespace** (no spaces, no newlines). In Python this is:
```python theme={null}
import json
canonical = json.dumps(action, sort_keys=True, separators=(",", ":"))
```
For the order example above, the canonical form is:
```
{"grouping":"na","orders":[{"a":1,"b":true,"p":"50","r":false,"s":"10","t":{"limit":{"tif":"Gtc"}}}],"type":"order"}
```
Key ordering is crucial. The server re-serializes the action using the same rules and any mismatch will produce a different hash, causing `SIGNATURE_INVALID`.
Concatenate four parts separated by newline characters (`\n`), then UTF-8 encode the result:
```
XPE_V2\n\n\n
```
* **`XPE_V2`** — fixed protocol prefix
* **``** — the value of `action.type`, e.g. `order`
* **``** — the full canonical JSON string from Step 2
* **``** — the int64 Unix millisecond timestamp for this request
In Python:
```python theme={null}
import time
nonce = int(time.time() * 1000)
msg = f"XPE_V2\n{action['type']}\n{canonical}\n{nonce}".encode("utf-8")
```
Hash the UTF-8 encoded message bytes using **Ethereum keccak256** — this is NOT the same as NIST SHA-3 despite sharing the same underlying construction.
```python theme={null}
from eth_utils import keccak
digest = keccak(msg) # returns 32 bytes
```
Never hash the message twice. The Ethereum signing convention already pads the digest internally in some libraries — with `Account._sign_hash` you pass the raw keccak256 output directly and the library does not hash again.
Sign the 32-byte digest directly using secp256k1. Set `v = 27 + recovery_bit` to follow the Ethereum convention.
```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
```
The `recovery_bit` (0 or 1) determines which of the two possible public keys recovers correctly. By adding 27 you get `v` = 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` must be 32-byte values represented as `0x`-prefixed lowercase hex strings (66 characters total including the prefix). `v` is an integer — either `27` or `28`.
## Request Envelope
The full `POST /exchange` body looks like this:
```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
}
```
An optional `vaultAddress` field can be included at the top level of the envelope for vault proxy operations. When present, the server verifies that the signer is an authorized agent of the specified vault.
## Complete Signing Helper
Here is a reusable `sign_and_send` function that implements the full signing flow:
```python theme={null}
import json, time, requests
from eth_account import Account
from eth_utils import keccak
BASE_URL = "https://dev.upsidemax.xyz"
def sign_and_send(action: dict, private_key: bytes) -> dict:
"""Sign an action and POST it to /exchange. Returns the parsed JSON response."""
nonce = int(time.time() * 1000)
# Step 1-2: canonical JSON
canonical = json.dumps(action, sort_keys=True, separators=(",", ":"))
# Step 3: build message
msg = f"XPE_V2\n{action['type']}\n{canonical}\n{nonce}".encode("utf-8")
# Step 4: keccak256 hash
digest = keccak(msg)
# Step 5: sign
sig = Account._sign_hash(digest, 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)
resp.raise_for_status()
return resp.json()
```
## Common Errors
| Error | Typical Cause |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SIGNATURE_INVALID` | Recovered address doesn't match the registered account. Most often caused by: unsorted JSON keys, extra whitespace in canonical JSON, double-hashing the message, or using the wrong private key. |
| `SIGNATURE_INVALID` (wrong `v`) | The `v` value was not adjusted to the `27+` range. Ensure `v = sig.v if sig.v >= 27 else sig.v + 27`. |
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.
# Upside DEX API Data Types, Formats, and Conventions
Source: https://upside.mintlify.app/guide/data-types
Reference for numeric strings, timestamps, address format, contract IDs, client order IDs, and the short key convention used in order payloads.
Upside DEX 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", price_scale) # → 8500.0
```
On testnet (QA/UAT), the SOL1 contract currently uses integer prices and sizes with no scaling — `priceScale = 0` and `qtyScale = 0`. Always check `configs` rather than hardcoding scale values.
## 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.utcfromtimestamp(1754450974231 / 1000)
# → datetime.datetime(2025, 8, 5, 18, 29, 34, 231000)
```
## Ethereum Addresses
Addresses follow the standard Ethereum format: `0x` prefix followed by 40 lowercase hexadecimal characters (42 characters total).
```
"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 testnet, the only available contract is SOL1 with ID `1`. Call `configs` to enumerate all contracts and their IDs in any environment.
```json theme={null}
{ "a": 1 } // asset = SOL1
```
## 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 per account. 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 and Troubleshooting for Upside DEX API
Source: https://upside.mintlify.app/guide/error-codes
Reference for HTTP-level error codes and business-level error messages returned by the Upside DEX API, with common troubleshooting guidance.
Upside DEX 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 the registered account |
| `ACCOUNT_ALREADY_EXISTS` | 400 | The address is already registered; `registerAccount` cannot be called twice for the same address |
| `INVITE_CODE_INVALID` | 403 | The invite 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 Upside 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 maxTicks ` | The order price exceeds the allowed maximum. On testnet the limit is `10000`. |
| `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. **Canonical JSON key ordering** — Keys in the action object must be sorted alphabetically when you serialize to JSON. In Python: `json.dumps(action, sort_keys=True, separators=(",", ":"))`. Verify the serialized string manually for your first request.
2. **No whitespace in canonical JSON** — The separators must be `(",", ":")` with no spaces. A single extra space anywhere in the string will produce a different hash.
3. **Correct message format** — The signed message must be exactly: `XPE_V2\n\n\n` with newline (`\n`) as the separator and the message UTF-8 encoded before hashing.
4. **No double-hashing** — Hash the message with keccak256 exactly once, then pass the 32-byte digest directly to the signing function. Do not hash again.
5. **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.
6. **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 maxTicks`** — On testnet, price must be ≤ 10000.
* **`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 matching engine did not return a result within the server's 5-second timeout window. Critically, this means the operation was **not applied** — you will not find a new order or cancellation as a result of that request.
**What to do:**
1. Wait 1–2 seconds.
2. Query `POST /info` with `userOrders` to confirm the current state of your orders.
3. If the order does not exist, retry the request with a fresh nonce.
4. If you continue to see timeouts, check the Upside 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 invite 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 Upside team to obtain a valid invite code for restricted environments.
# Upside DEX API: Perpetuals Trading Guide for Developers
Source: https://upside.mintlify.app/guide/introduction
Upside DEX is an action-based perpetuals exchange. The API provides signed writes, unsigned reads, and real-time WebSocket streams for developers.
Upside DEX 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 Upside DEX 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, Upside DEX 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
The UAT and QA environments are for testing only. Funds and accounts are not shared across environments. The Production URL is not yet public — contact the Upside team to get access.
| Environment | REST Base URL | WebSocket URL |
| -------------- | --------------------------- | ---------------------------- |
| **QA** | `https://dev.upsidemax.xyz` | `wss://dev.upsidemax.xyz/ws` |
| **UAT** | Contact Upside team | Contact Upside team |
| **Production** | TBD — contact Upside team | TBD — contact Upside team |
## Testnet Constraints
The QA and UAT environments currently operate with a single contract:
* **Contract:** SOL1
* **Contract ID:** `a` = `1`
* **Quote currency:** USDT1
* **Price (`p`):** Must be a positive integer; maximum value is `10000`
* **Size (`s`):** Must be a positive integer
These constraints apply to the testnet only. Production contracts will support decimal prices and sizes, and will list additional assets beyond SOL1.
## API Version
Current API version: **0.5.4**
## 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 Requirements and Replay Protection on Upside DEX
Source: https://upside.mintlify.app/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
```
**JavaScript / TypeScript**
```javascript 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 appears in the fourth segment of the `XPE_V2\n...\n...\n` message, 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.
# Upside DEX API Quickstart: Your First Trade in Python
Source: https://upside.mintlify.app/guide/quickstart
Make your first signed Upside DEX API call: generate a wallet, register an account, place a limit order, and cancel it using Python.
This guide walks you through the full lifecycle of a trade on Upside DEX — from generating a fresh wallet keypair to placing and cancelling a limit order. You'll write about 50 lines of Python and interact with the QA environment, which runs the SOL1/USDT1 perpetual contract.
## Prerequisites
You need **Python 3.8 or later** and three packages:
```bash theme={null}
pip install eth-account eth-utils requests
```
| Package | Purpose |
| ------------- | -------------------------------- |
| `eth-account` | ECDSA key generation and signing |
| `eth-utils` | Keccak-256 hashing |
| `requests` | HTTP calls to the API |
## Steps
Use `eth_account.Account.create()` to generate a fresh private key and address. In a production system you would load an existing private key from a secure secret store — never hardcode it.
```python theme={null}
from eth_account import Account
wallet = Account.create()
PRIVATE_KEY = wallet.key # bytes
ADDRESS = wallet.address.lower() # 0x-prefixed lowercase hex
print(f"Address: {ADDRESS}")
```
All `POST /exchange` requests must be signed. The helper below constructs the canonical message, hashes it with keccak256, signs it with your private key, and posts the envelope to the API.
The message format is:
```
XPE_V2\n\n\n
```
```python theme={null}
import json, time, requests
from eth_account import Account
from eth_utils import keccak
BASE_URL = "https://dev.upsidemax.xyz"
def sign_and_send(action):
nonce = int(time.time() * 1000)
canonical = json.dumps(action, sort_keys=True, separators=(",", ":"))
msg = f"XPE_V2\n{action['type']}\n{canonical}\n{nonce}".encode()
sig = Account._sign_hash(keccak(msg), 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()
```
See [Authentication](/guide/authentication) for a full explanation of every step in the signing process.
Before you can trade, you must register your address on the exchange. This is a one-time operation per address.
```python theme={null}
result = sign_and_send({"type": "registerAccount", "address": ADDRESS})
account_id = result["response"]["accountId"]
print(f"Registered: accountId={account_id}")
```
Some environments require an `inviteCode` field at the **top level of the envelope** (not inside `action`) for `registerAccount`. If registration returns `INVITE_CODE_INVALID`, add `"inviteCode": ""` to the envelope dictionary alongside `"action"`, `"signature"`, and `"nonce"`. The invite code is **not** included in the signed message.
Place a limit GTC (Good Till Cancelled) buy order for 10 units of SOL1 at a price of 50 USDT1. On testnet, price and size must be positive integers and price must not exceed 10000.
```python theme={null}
result = sign_and_send({
"type": "order",
"orders": [
{
"a": 1, # asset ID — 1 = SOL1
"b": True, # isBuy
"p": "50", # price (string)
"s": "10", # size (string)
"r": False, # reduceOnly
"t": {"limit": {"tif": "Gtc"}},
}
],
"grouping": "na",
})
oid = result["response"]["data"]["statuses"][0]["resting"]["oid"]
print(f"Order placed: oid={oid}")
```
Confirm the order is live by querying `POST /info` with `userOrders`. No signing is required for read queries.
```python theme={null}
info_resp = requests.post(
f"{BASE_URL}/info",
json={"type": "userOrders", "accountId": str(account_id), "marketDeployerId": 1},
timeout=10,
)
orders = info_resp.json()
print(f"Open orders: {orders}")
```
Cancel the order using its `oid`. Provide the asset ID (`a`) and order ID (`o`) in the `cancels` array.
```python theme={null}
time.sleep(0.5)
result = sign_and_send({"type": "cancel", "cancels": [{"a": 1, "o": oid}]})
print(f"Cancel result: {result['response']['data']['statuses']}")
```
## Complete Example
Paste the full script below to run all six steps end to end:
```python theme={null}
import json, time, requests
from eth_account import Account
from eth_utils import keccak
BASE_URL = "https://dev.upsidemax.xyz"
wallet = Account.create()
PRIVATE_KEY = wallet.key
ADDRESS = wallet.address.lower()
def sign_and_send(action):
nonce = int(time.time() * 1000)
canonical = json.dumps(action, sort_keys=True, separators=(",", ":"))
msg = f"XPE_V2\n{action['type']}\n{canonical}\n{nonce}".encode()
sig = Account._sign_hash(keccak(msg), 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()
# 1. Register
result = sign_and_send({"type": "registerAccount", "address": ADDRESS})
account_id = result["response"]["accountId"]
print(f"Registered: accountId={account_id}")
# 2. Place limit buy order
result = sign_and_send({
"type": "order",
"orders": [{"a": 1, "b": True, "p": "50", "s": "10", "r": False,
"t": {"limit": {"tif": "Gtc"}}}],
"grouping": "na",
})
oid = result["response"]["data"]["statuses"][0]["resting"]["oid"]
print(f"Order placed: oid={oid}")
# 3. Verify
info_resp = requests.post(
f"{BASE_URL}/info",
json={"type": "userOrders", "accountId": str(account_id), "marketDeployerId": 1},
timeout=10,
)
print(f"Open orders: {info_resp.json()}")
# 4. Cancel
time.sleep(0.5)
result = sign_and_send({"type": "cancel", "cancels": [{"a": 1, "o": oid}]})
print(f"Cancel result: {result['response']['data']['statuses']}")
```
## Expected Output
```
Registered: accountId=7
Order placed: oid=15
Cancel result: ['success']
```
Account IDs and order IDs are assigned sequentially by the server, so your values will differ.
# POST /info: Read Query Structure, Types, and Usage
Source: https://upside.mintlify.app/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 Upside DEX 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
```
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 deployers associated with a user address | [/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: last price, funding rate, open interest | [/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 SOL1 (asset 1):**
```json theme={null}
{
"type": "l2Book",
"asset": 1
}
```
**Get hourly candles for SOL1 over the last 24 hours:**
```json theme={null}
{
"type": "candleSnapshot",
"req": {
"coin": "SOL1",
"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"
}
]
```
# Upside DEX API Response Formats: Success and Errors
Source: https://upside.mintlify.app/guide/responses
Reference for success and error response shapes from POST /exchange and POST /info, including business-level errors inside the statuses array.
Upside DEX 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 Upside 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 Upside 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 Upside 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.
# POST /exchange: Write Request Structure and Format
Source: https://upside.mintlify.app/guide/write-requests
Understand the envelope structure of POST /exchange requests — action, signature, and nonce fields for all state-changing operations on Upside DEX.
All state-changing operations on Upside DEX — 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
```
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. This value is also used as the second segment of the signed message (`XPE_V2\n\n...`).
Common values: `order`, `cancel`, `cancelByCloid`, `modify`, `registerAccount`, `updateLeverage`, `updateIsolatedMargin`.
ECDSA signature over the canonical action payload. 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.
Conditional. A 6-character alphanumeric invite 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, Upside DEX 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 |
| `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 for 10 units of SOL1 at price 50:
```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.
# candleSnapshot: Fetch Historical OHLCV Candle Data
Source: https://upside.mintlify.app/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
```json theme={null}
{
"type": "candleSnapshot",
"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
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: Fetch Market and Contract Configuration Data
Source: https://upside.mintlify.app/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.
## 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": "USDT1",
"szDecimals": 6,
"isMargin": true,
"status": "Active"
}
],
"contracts": [
{
"contractId": 1,
"marketId": 1,
"marketDeployerId": 1,
"name": "SOL1",
"baseCoinId": 1,
"quoteCoinId": 1,
"tickSize": "1",
"stepSize": "1",
"priceScale": 1,
"qtyScale": 1,
"defaultLeverage": "10",
"fundingInterval": 3600,
"minTradeNtl": "1",
"status": "Active",
"tiers": [
{
"upperBound": "9223372036854775807",
"maxLeverage": "100",
"imBps": 100,
"mmBps": 100
}
]
}
]
}
```
### Contract Fields
The unique contract identifier. Use this as the `a` field when placing, modifying, or canceling orders.
Human-readable contract name (e.g. `"SOL1"`).
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.
Funding settlement interval in seconds (e.g. `3600` = hourly).
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.
# l2Book: Fetch Full L2 Order Book Snapshot via REST
Source: https://upside.mintlify.app/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}]
]
}
```
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.
# marketState: Get Current Mark, Index, and Last Price
Source: https://upside.mintlify.app/info/market-state
Fetch the current mark price, index 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, index (oracle) 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",
"indexPx": "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 index (oracle) 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.
# ordersByCloids: Fetch Orders by Client-Assigned Order ID
Source: https://upside.mintlify.app/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",
"leverage": "10",
"status": "Open",
"reduceOnly": false
}
]
}
```
The `orders` array contains the same fields described in [`userOrders`](/info/user-orders#order-fields). Client order IDs that do not match any known order are silently omitted from the response.
# ordersByIds: Fetch Orders by Exchange-Assigned Order IDs
Source: https://upside.mintlify.app/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",
"leverage": "10",
"status": "Open",
"reduceOnly": false
}
]
}
```
The `orders` array contains the same fields described in [`userOrders`](/info/user-orders#order-fields). 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.
# POST /info: Upside DEX Read API Overview and Query Types
Source: https://upside.mintlify.app/info/overview
All read queries on Upside DEX 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 Upside DEX 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
```
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 |
| [`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 |
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 |
# shareGroupState: List Portfolio Share Group Definitions
Source: https://upside.mintlify.app/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": [10, 11]
}
]
}
```
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.
# userAccount: Query Account Balances and Open Positions
Source: https://upside.mintlify.app/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": "1",
"marketDeployerId": 1,
"crossEquity": "1000000000000",
"orderFrozen": "0",
"orderLoss": "0",
"marginAvailable": "1000000000000",
"marginAvailableForOrder": "1000000000000",
"totalPositionIM": "0",
"crossPositionMM": "0",
"crossCollaterals": [{"coinId": 1, "amount": "1000000000000"}],
"isolatedCollaterals": [],
"positions": [
{
"contractId": 1,
"positionSide": "OneWay",
"size": "5",
"openValue": "500",
"isLongPosition": false,
"fundingIndex": "0",
"leverage": "10",
"isIsolated": false,
"unrealizedPnl": "0",
"mm": "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 `contractId`, `coinId`, and `amount`.
### Position Fields
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.
Maintenance margin allocated to this individual position (raw integer string).
### 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.
Monotonically increasing counter tracking the number of on-chain deposits processed for this account. Use this to verify deposit sequencing and detect missing or replayed deposit events.
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"`.
# userAgents: List All Authorized API Wallet Agent Slots
Source: https://upside.mintlify.app/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.
# userMarketDeployers: List Enrolled Market Deployers
Source: https://upside.mintlify.app/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.
# userOrders: List All Active Open Orders for an Account
Source: https://upside.mintlify.app/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",
"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.
Order quantity as a raw integer string. Divide by `10^qtyScale` from `configs` to get the display value.
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).
# Authenticating a WebSocket Connection on Upside DEX
Source: https://upside.mintlify.app/websocket/authentication
Send an Auth message over WebSocket to associate your connection with an account and unlock private channel subscriptions on Upside DEX.
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.
Re-send your `Auth` message immediately after every reconnect, before re-subscribing to any channels. This keeps your connection authenticated without any gap in coverage.
# WebSocket bbo Channel: Best Bid and Ask Price Stream
Source: https://upside.mintlify.app/websocket/bbo
Subscribe to bbo over WebSocket for the best bid and ask prices per block — far lower bandwidth than the full l2Book order book channel.
The `bbo` channel delivers only the best bid (buy-one) and best ask (sell-one) for a contract on every block. 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`.
## 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.
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: Real-Time OHLCV Bar Updates
Source: https://upside.mintlify.app/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 approximately every 500 ms as new trades arrive within the bar. 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. To show historical candles before the subscription starts, call the REST `candleSnapshot` endpoint first, then use the WebSocket stream for all subsequent updates.
## 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`, `6h`, `12h`, `1d`, `3d`, `1w`.
## 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.
## Initialising a chart
Call the `candleSnapshot` endpoint with your chosen `asset` and `interval` to load historical OHLCV data. Render these bars as the initial chart state.
Subscribe using the same `asset` and `interval`. Buffer incoming WebSocket messages while the REST response is in-flight.
Once you have both datasets, use `data.t` as the key. Replace or append bars from the WebSocket stream — for any bar where `data.t` matches an existing bar from REST, the WebSocket version is newer and should take precedence.
When you receive a message with `"closed": true`, finalize that bar in your chart. The next message will carry the new open bar with a later `t` value.
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: Contract Configuration Notifications
Source: https://upside.mintlify.app/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 l2Book: Real-Time Full Order Book Snapshots
Source: https://upside.mintlify.app/websocket/l2-book
Subscribe to l2Book over WebSocket to receive full L2 order book snapshots for any contract on every block — no polling required.
The `l2Book` channel delivers a complete order book snapshot for a contract on every block — roughly every 500 ms. 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.
## 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 on every block:
```json theme={null}
{
"channel": "l2Book",
"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
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. The first `l2Book` message you receive is a full snapshot of the current book — store all levels in your local state.
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 openOrders: Active Orders Snapshot and Updates
Source: https://upside.mintlify.app/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",
"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), or `"Alo"` (add-liquidity-only / post-only).
Order price (raw string).
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 orderUpdates: Incremental Order State Changes
Source: https://upside.mintlify.app/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. Because this channel is incremental, it does not send a snapshot on subscribe. If you need an initial full view of all active orders, subscribe to [`openOrders`](/websocket/open-orders) instead, then switch to (or layer on) `orderUpdates` for ongoing changes.
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": [ { ...entry... } ],
"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",
"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 | Request ID for correlating with the original action that created this order |
| `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 |
| `tif` | string | Time-in-force: `"Gtc"` (good-till-cancel), `"Ioc"` (immediate-or-cancel), `"Alo"` (add-liquidity-only / post-only) |
| `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"` |
## 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` = index price |
| `ro` | boolean | `true` if this is a reduce-only order |
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.
When you place an order, store its `requestId` (`r` field) alongside the order. When you receive an `OrderUpdate`, match on `r` to confirm which of your pending actions the update corresponds to — especially useful when placing orders in rapid succession.
# Upside DEX WebSocket API: Real-Time Data Streaming
Source: https://upside.mintlify.app/websocket/overview
Connect to the Upside DEX WebSocket at wss://dev.upsidemax.xyz/ws for real-time order book, trades, candles, and private account updates.
The Upside DEX 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 — typically every block (\~500 ms) for price channels. 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 (or the JSON `{"msg": "Ping"}` message) at least every 30 seconds. The server closes idle connections after 60 seconds without activity.
## 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 every block |
| `bbo` | No | Best bid/ask — bandwidth-efficient top-of-book |
| `trades` | No | Public trade stream for a contract |
| `candle` | No | Real-time OHLCV candles at a chosen interval |
| `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 |
\* 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, wait **5 seconds** before reconnecting to avoid hammering the server during an outage. After reconnecting, re-send your `Auth` message (if applicable) and re-subscribe to every channel — the server does not persist subscription state across connections.
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 on every block — 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.
Lightweight notifications when contract configuration changes — new listings, freezes, or parameter updates.
# WebSocket Subscription Protocol for Upside DEX API
Source: https://upside.mintlify.app/websocket/subscription
Learn how to subscribe and unsubscribe to WebSocket channels on Upside DEX using the method + subscription object protocol introduced in v0.14.
Every channel on the Upside DEX 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 was deprecated in v0.14 and 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 |
| `orderUpdates` | `user` (account address) | No\* |
| `openOrders` | `user` (account address) | No\* |
| `userFills` | `user` (account address) | 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 trades Channel: Real-Time Public Trade Stream
Source: https://upside.mintlify.app/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.
## Building a trade history feed
Send the subscribe message. The server starts pushing trades from the moment of subscription. There is no backfill on subscribe.
To display historical trades or fill in chart data, call the REST `candleSnapshot` endpoint before or alongside subscribing. This gives you aggregated OHLCV history, and the WebSocket stream picks up from the current point in time.
If you consume trades from multiple sources (e.g., a REST snapshot and the WebSocket stream), use the `tid` field as the unique key to avoid counting the same trade twice.
The `trades` channel pushes from subscription time only — there is no historical backfill. Use `candleSnapshot` via the REST API to retrieve historical OHLCV data for chart initialisation.
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 userFills: Real-Time Personal Fill Stream
Source: https://upside.mintlify.app/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"
}
],
"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.
## 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
Always `"TradeFill"` for fills on this channel.
`"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.
`userFills` pushes from subscription time only — there is no historical backfill on subscribe. To retrieve past fills, query the REST fills history endpoint.
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.