> For the complete documentation index, see [llms.txt](https://sodex.com/documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sodex.com/documentation/for-developers/developers/trading/authentication-and-signing/manual-rest-signing.md).

# Manual REST Signing

Implement Perps order signing without an SDK. For the shorter SDK-based path, use [Trading Quickstart](/documentation/for-developers/developers/trading/quickstart.md). This tutorial covers canonical payloads, EIP-712 signatures, raw HTTP requests, and execution tracking.

Before starting, activate and fund your testnet trading account and [register a trading API key](/documentation/for-developers/developers/trading/api-keys-and-nonces.md#register-a-trading-api-key). The example below submits a real testnet order; it is not a simulation.

## 1. Choose an environment

| Environment | REST base URL                  | WebSocket base URL           | Chain ID |
| ----------- | ------------------------------ | ---------------------------- | -------- |
| Mainnet     | `https://mainnet-gw.sodex.dev` | `wss://mainnet-gw.sodex.dev` | `286623` |
| Testnet     | `https://testnet-gw.sodex.dev` | `wss://testnet-gw.sodex.dev` | `138565` |

Spot REST paths start with `/api/v1/spot`; perpetual REST paths start with `/api/v1/perps`.

## 2. Make a public request

Public market-data endpoints do not require a signature.

```bash
curl "https://testnet-gw.sodex.dev/api/v1/perps/markets/tickers?symbol=BTC-USD" \
  -H "Accept: application/json"
```

Successful REST responses use a common envelope with `code`, `timestamp`, and endpoint-specific `data`. A `code` value of `0` means the request succeeded.

## 3. Pick an integration path

For most applications, start with an [official SDK](/documentation/for-developers/sdks.md). The SDKs implement canonical request serialization, EIP-712 signing, signature prefixes, and nonce coordination.

If you need a custom client, read [Authentication & Signing](/documentation/for-developers/developers/trading/authentication-and-signing.md) before implementing write requests. In particular:

* `X-API-Key` contains the registered **key name**, not a private key or address.
* `X-API-Sign` contains the EIP-712 signature; the private key is never sent.
* `X-API-Nonce` must be unique for the signing address and remain inside the accepted time window.
* Use the `spot`, `futures`, or `universal` signing domain required by the action.

## 4. Get your account ID and trading symbol

Use the **master wallet address** for account lookup, not the API key's public address. All commands below use the same testnet environment:

```bash
export BASE_URL='https://testnet-gw.sodex.dev'
export USER_ADDRESS='YOUR_MASTER_WALLET_ADDRESS'
curl -sS "$BASE_URL/api/v1/perps/accounts/$USER_ADDRESS/state" \
  -H 'Accept: application/json'

curl -sS "$BASE_URL/api/v1/perps/markets/symbols" \
  -H 'Accept: application/json'
```

Check `code == 0` before reading `data`.

| Value you need           | Where to get it                                                                                      | How it is used                                                                             |
| ------------------------ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `accountID`              | `data.aid` from account state                                                                        | Identifies the trading account in the signed order. This is not `uid` or a wallet address. |
| `symbolID`               | `id` of the selected object in the symbols response's `data` array                                   | Identifies the market in the signed order.                                                 |
| `symbol`                 | `name` from that same symbol object                                                                  | Used by market queries and WebSocket subscriptions. Do not use `displayName`.              |
| Available margin         | `data.am` (cross) or `data.ami` (isolated) from account state                                        | Check the margin available for the intended position mode.                                 |
| Price and quantity rules | `tickSize`, `stepSize`, `pricePrecision`, `minQuantity`, `maxQuantity`, `minNotional`, `maxNotional` | Choose a price and quantity that satisfy the market's filters before signing.              |

The state endpoint returns the primary account by default. To trade a sub-account, pass its `accountID` in the state query and use that account ID consistently in the order and subsequent queries. An unactivated account must be activated before continuing.

See [Account State](/documentation/for-developers/api-reference/trading-api/rest-v1/sodex-rest-perps-api.md#query-state-for-frontend), [Symbol Fields](/documentation/for-developers/api-reference/trading-api/rest-v1/schema.md#perpssymbol), and [Order Validation Rules](/documentation/for-developers/api-reference/trading-api/rest-v1/sodex-rest-perps-api.md#place-multiple-orders).

## 5. Sign and submit a limit order

This example places one **BUY / LIMIT / GTC** order. GTC orders can remain open until filled or canceled. Use a testnet price and quantity you intend to trade; do not copy a production order size.

Install dependencies and save the TypeScript code below as `place-order.ts`:

```bash
npm install viem tsx
export SODEX_API_KEY_NAME='YOUR_REGISTERED_KEY_NAME'
# Set SODEX_API_KEY_PRIVATE_KEY securely in your local environment.
export ACCOUNT_ID='ACCOUNT_ID_FROM_STEP_4'
export SYMBOL_ID='SYMBOL_ID_FROM_STEP_4'
export ORDER_PRICE='YOUR_LIMIT_PRICE'
export ORDER_QUANTITY='YOUR_QUANTITY'
npx tsx place-order.ts
```

The request uses these values:

| Field               | Example value               | Meaning                                                                                     |
| ------------------- | --------------------------- | ------------------------------------------------------------------------------------------- |
| `modifier`          | `1`                         | Normal order                                                                                |
| `side`              | `1`                         | Buy                                                                                         |
| `type`              | `1`                         | Limit                                                                                       |
| `timeInForce`       | `1`                         | Good till canceled                                                                          |
| `positionSide`      | `1`                         | BOTH                                                                                        |
| `price`, `quantity` | Decimal strings             | Preserve the selected market's precision; do not convert amounts to floating-point numbers. |
| `clOrdID`           | Generated before submission | Your client order identifier; retain it for reconciliation.                                 |

```typescript
import { concatHex, keccak256, toHex } from "viem";
import { privateKeyToAccount } from "viem/accounts";

async function main() {
  const accountID = Number(process.env.ACCOUNT_ID);
  const symbolID = Number(process.env.SYMBOL_ID);
  if (!Number.isSafeInteger(accountID) || !Number.isSafeInteger(symbolID)) {
    throw new Error("Use exact integer IDs; use an official SDK for IDs outside the safe integer range");
  }
  const price = process.env.ORDER_PRICE;
  const quantity = process.env.ORDER_QUANTITY;
  const keyName = process.env.SODEX_API_KEY_NAME;
  if (!price || !quantity || !keyName) throw new Error("Set order price, quantity, and API key name");
  const signer = privateKeyToAccount(process.env.SODEX_API_KEY_PRIVATE_KEY as `0x${string}`);
  const nonce = BigInt(Date.now());
  const clOrdID = "quickstart-" + nonce.toString();
  const params = {
    accountID,
    symbolID,
    orders: [{ clOrdID, modifier: 1, side: 1, type: 1, timeInForce: 1,
      price, quantity, reduceOnly: false, positionSide: 1 }],
  };
  const payloadHash = keccak256(toHex(JSON.stringify({ type: "newOrder", params })));
  const signature = await signer.signTypedData({
    domain: { name: "futures", version: "1", chainId: 138565,
      verifyingContract: "0x0000000000000000000000000000000000000000" },
    types: { ExchangeAction: [
      { name: "payloadHash", type: "bytes32" },
      { name: "nonce", type: "uint64" },
    ] },
    primaryType: "ExchangeAction",
    message: { payloadHash, nonce },
  });
  console.log("Client order ID:", clOrdID); // Retain this before submitting.
  const response = await fetch("https://testnet-gw.sodex.dev/api/v1/perps/trade/orders", {
    method: "POST",
    headers: {
      "Content-Type": "application/json", "Accept": "application/json",
      "X-API-Key": keyName, "X-API-Nonce": nonce.toString(),
      "X-API-Sign": concatHex(["0x01", signature]),
    },
    body: JSON.stringify(params),
  });
  console.log("HTTP status:", response.status);
  console.log(await response.text()); // Preserve uint64 order IDs without rounding.
}
main().catch(console.error);
```

The signed payload is `{type: "newOrder", params}`, but the HTTP body is **only `params`**. Keep the field order shown above. The testnet signing domain is `futures` with chain ID `138565`; the signature is prefixed with `0x01`. See [Authentication & Signing](/documentation/for-developers/developers/trading/authentication-and-signing.md) for the original signing definitions.

### Check the submission result

1. Check the HTTP status and top-level `code`. HTTP success alone is insufficient.
2. When the top-level `code` is `0`, inspect each item in `data`. A batch can contain individual failures.
3. Match each result by `clOrdID`. When its `code` is `0`, save the returned `orderID`; otherwise read its `error`.
4. Acceptance is **not a fill**. Continue with execution tracking below.

Pre-validation can return one error for the whole batch rather than one result per order. Do not assume response length always equals request length.

## 6. Track order status and fills

Connect to `wss://testnet-gw.sodex.dev/ws/perps`. Prefer subscribing before submission so that immediately filled orders are not missed. Replace `YOUR_MASTER_WALLET_ADDRESS` and `SYMBOL_NAME_FROM_STEP_4` in each message:

```json
{"op":"subscribe","id":1,"params":{"channel":"accountOrderUpdate","user":"YOUR_MASTER_WALLET_ADDRESS","symbols":["SYMBOL_NAME_FROM_STEP_4"]}}
```

```json
{"op":"subscribe","id":2,"params":{"channel":"accountTrade","user":"YOUR_MASTER_WALLET_ADDRESS","symbols":["SYMBOL_NAME_FROM_STEP_4"]}}
```

Check the subscription acknowledgement's `success` field. Use [Account Order Updates](/documentation/for-developers/api-reference/trading-api/websocket-v1/account-order-updates.md) for state changes and [Account Trades](/documentation/for-developers/api-reference/trading-api/websocket-v1/account-trades.md) for executions. Follow the subscription schema when specifying a sub-account.

| Order status          | Interpretation                                                                       |
| --------------------- | ------------------------------------------------------------------------------------ |
| `NEW`                 | Accepted; it may still be resting on the book.                                       |
| `PARTIALLY_FILLED`    | Some quantity has executed; some remains.                                            |
| `FILLED`              | The full order quantity has executed.                                                |
| `CANCELED`, `EXPIRED` | No further execution is expected, but partial fills may already exist. Check trades. |
| `REJECTED`            | The order was not accepted. Inspect the error before correcting the request.         |

### Recover state with REST

Set `SYMBOL` to the market name from step 4. Use the account ID from the same step:

```bash
export SYMBOL='SYMBOL_NAME_FROM_STEP_4'
curl -sS --get "$BASE_URL/api/v1/perps/accounts/$USER_ADDRESS/orders" \
  --data-urlencode "accountID=$ACCOUNT_ID" --data-urlencode "symbol=$SYMBOL"

curl -sS --get "$BASE_URL/api/v1/perps/accounts/$USER_ADDRESS/orders/history" \
  --data-urlencode "accountID=$ACCOUNT_ID" --data-urlencode "symbol=$SYMBOL"

export ORDER_ID='ORDER_ID_FROM_SUBMISSION'
curl -sS --get "$BASE_URL/api/v1/perps/accounts/$USER_ADDRESS/trades" \
  --data-urlencode "accountID=$ACCOUNT_ID" --data-urlencode "symbol=$SYMBOL" \
  --data-urlencode "orderID=$ORDER_ID"
```

An order missing from open orders is not necessarily filled: check history for its `orderID` or `clOrdID`, then inspect its trades. Paginate history if the order is outside the returned page. To stop an unfilled GTC order, use [Cancel Multiple Orders](/documentation/for-developers/api-reference/trading-api/rest-v1/sodex-rest-perps-api.md#cancel-multiple-orders); signing a cancellation is a separate action.

## Handle errors and reconnects

| Situation                                         | What to do                                                                                                                                                                                                        |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP timeout or connection lost during submission | Treat the outcome as unknown. Query open orders and history using the retained client order ID before submitting again. A fresh nonce does not make a retry idempotent.                                           |
| An order result has a non-zero `code`             | Read its error, check the market filters and available margin, then correct the request.                                                                                                                          |
| WebSocket disconnected                            | Reconnect, resubscribe, and reconcile open orders, history, and trades through REST. Follow the documented [ping/pong rules](/documentation/for-developers/api-reference/trading-api/websocket-v1.md#connection). |
| Rate limited                                      | Back off according to the applicable [rate limits](/documentation/for-developers/developers/trading/api-rate-limits.md). Do not repeatedly resend signed orders.                                                  |

Use a separate API key for each concurrent trading process so that processes do not share a nonce tracker. Never place private keys or signature headers in logs.

## Next steps

{% content-ref url="/pages/o3Yj9kHutFbmg6uDLnhS" %}
[Authentication & Signing](/documentation/for-developers/developers/trading/authentication-and-signing.md)
{% endcontent-ref %}

{% content-ref url="/pages/HWeWqIzQUTNao2smQ2pp" %}
[Rate Limits](/documentation/for-developers/developers/trading/api-rate-limits.md)
{% endcontent-ref %}

{% content-ref url="/pages/JhkOXmaIr0osLMjp2DfF" %}
[REST API](/documentation/for-developers/api-reference/trading-api/rest-v1.md)
{% endcontent-ref %}

{% content-ref url="/pages/U25Aeh8OoqsoN02ofoj4" %}
[WebSocket Streams](/documentation/for-developers/api-reference/trading-api/websocket-v1.md)
{% endcontent-ref %}
