> 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.md).

# Authentication & Signing

For API key ownership, signer selection, and nonce rules, see [API Keys and Nonces](/documentation/for-developers/developers/trading/api-keys-and-nonces.md).

## Typed signature

In Sodex, we use [EIP712](https://eips.ethereum.org/EIPS/eip-712) for Typed structured data hashing and signing. We use different domains for the Add API key and other actions.

### Add API key

For ordinary registration without builder or permissions, sign `AddAPIKey`. Set `domain.chainId` and `X-API-Chain` to the target network: mainnet (`286623`) or testnet (`138565`). The ordinary message has no `chainID` field; the EIP-712 domain provides network separation.

```typescript
{
  types: {
    EIP712Domain: [
      { name: "name", type: "string" },
      { name: "version", type: "string" },
      { name: "chainId", type: "uint256" },
      { name: "verifyingContract", type: "address" }
    ],
    AddAPIKey: [
      { name: 'accountID', type: "uint64" },
      { name: 'name', type: 'string' },
      { name: 'keyType', type: 'uint8' },
      { name: 'publicKey', type: 'bytes' },
      { name: 'expiresAt', type: 'uint64' },
      { name: 'nonce', type: 'uint64' }
    ],
  },
  domain: {
    name: "universal",
    version: "1",
    chainId: <X-API-Chain>,
    verifyingContract: "0x0000000000000000000000000000000000000000"
  },
  primaryType: "AddAPIKey",
  message: {
    nonce: 1760373925000,
    accountID: 1010,
    name: "api-key-01",
    keyType: 1,
    publicKey: "0x3d4595c8742d0a58173a9963c05755b59a8f8256",
    expiresAt: 0,
  }
}
```

When the `addAPIKey` request includes `builder`, sign `AddAPIKeyWithBuilder` instead:

```typescript
{
  types: {
    EIP712Domain: [
      { name: "name", type: "string" },
      { name: "version", type: "string" },
      { name: "chainId", type: "uint256" },
      { name: "verifyingContract", type: "address" }
    ],
    AddAPIKeyWithBuilder: [
      { name: 'chainID', type: "uint64" },
      { name: 'nonce', type: "uint64" },
      { name: 'accountID', type: "uint64" },
      { name: 'name', type: "string" },
      { name: 'keyType', type: "uint8" },
      { name: 'publicKey', type: "bytes" },
      { name: 'expiresAt', type: "uint64" },
      { name: 'builderID', type: "uint64" },
      { name: 'maxFeeRate', type: "uint64" }
    ],
  },
  domain: {
    name: "universal",
    version: "1",
    chainId: <X-API-Chain>,
    verifyingContract: "0x0000000000000000000000000000000000000000"
  },
  primaryType: "AddAPIKeyWithBuilder",
  message: {
    chainID: 286623 or 138565,
    nonce: 1760373925000,
    accountID: 1010,
    name: "api-key-01",
    keyType: 1,
    publicKey: "0x3d4595c8742d0a58173a9963c05755b59a8f8256",
    expiresAt: 0,
    builderID: 1234,
    maxFeeRate: 10,
  }
}
```

When creating a permissioned API key, sign `UserSignedAddPermissionedAPIKeyAction`. A permissioned API key request must omit `builder`.

```typescript
{
  types: {
    EIP712Domain: [
      { name: "name", type: "string" },
      { name: "version", type: "string" },
      { name: "chainId", type: "uint256" },
      { name: "verifyingContract", type: "address" }
    ],
    UserSignedAddPermissionedAPIKeyAction: [
      { name: 'chainID', type: "uint64" },
      { name: 'nonce', type: "uint64" },
      { name: 'accountID', type: "uint64" },
      { name: 'name', type: "string" },
      { name: 'keyType', type: "uint8" },
      { name: 'publicKey', type: "bytes" },
      { name: 'expiresAt', type: "uint64" },
      { name: 'permissions', type: "uint64" }
    ],
  },
  domain: {
    name: "universal",
    version: "1",
    chainId: <X-API-Chain>,
    verifyingContract: "0x0000000000000000000000000000000000000000"
  },
  primaryType: "UserSignedAddPermissionedAPIKeyAction",
  message: {
    chainID: 286623 or 138565,
    nonce: 1760373925000,
    accountID: 1010,
    name: "api-key-01",
    keyType: 1,
    publicKey: "0x3d4595c8742d0a58173a9963c05755b59a8f8256",
    expiresAt: 0,
    permissions: 2,
  }
}
```

Use the master wallet to sign all three registration types. For builder and permissioned registration, `domain.chainId` must match `X-API-Chain`, while `message.chainID` is `286623` for mainnet or `138565` for testnet. Ordinary `AddAPIKey` registration does not include `message.chainID`.

The REST request JSON and the EIP-712 typed data intentionally use different field names for the API key type. In request payloads this field is `type`, while in the typed signing structure it is `keyType`.

After you get the signature bytes `sig`, prepend byte `2` to get the typed signature. For example, your signed signature is `0x789a6bcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab`. The correct typed signature is `0x02789a6bcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab`.

### Approve builder fee

Use the universal domain and sign `ApproveBuilderFeeAction`:

```typescript
{
  types: {
    EIP712Domain: [
      { name: "name", type: "string" },
      { name: "version", type: "string" },
      { name: "chainId", type: "uint256" },
      { name: "verifyingContract", type: "address" }
    ],
    ApproveBuilderFeeAction: [
      { name: 'chainID', type: "uint64" },
      { name: 'nonce', type: "uint64" },
      { name: 'accountID', type: "uint64" },
      { name: 'builderID', type: "uint64" },
      { name: 'maxFeeRate', type: "uint64" }
    ],
  },
  domain: {
    name: "universal",
    version: "1",
    chainId: <X-API-Chain>,
    verifyingContract: "0x0000000000000000000000000000000000000000"
  },
  primaryType: "ApproveBuilderFeeAction",
  message: {
    chainID: 286623 or 138565,
    nonce: 1760373925000,
    accountID: 1010,
    builderID: 1234,
    maxFeeRate: 10,
  }
}
```

You must use the master wallet's private key to sign `ApproveBuilderFeeAction`. Like other universal actions, prepend byte `2` before the signature bytes.

`maxFeeRate` must be between `0` and `2000`, inclusive. A value of `0` clears the builder approval.

### Trading actions

```typescript
{
  types: {
    EIP712Domain: [
      { name: "name", type: "string" },
      { name: "version", type: "string" },
      { name: "chainId", type: "uint256" },
      { name: "verifyingContract", type: "address" }
    ],
    ExchangeAction: [
      { name: 'payloadHash', type: "bytes32" },
      { name: 'nonce', type: 'uint64' }
    ],
  },
  domain: {
    name: "spot" or "futures",
    version: "1",
    chainId: 286623 or 138565,
    verifyingContract: "0x0000000000000000000000000000000000000000"
  },
  primaryType: "ExchangeAction",
  message: {
    payloadHash: "0x7521d1cadbcfa91eec65aa16715b94ffc1c9654ba57ea2ef1a2127bca1127a83",
    nonce: 1760373925000,
  }
}
```

For mainnet use `286623` for `domain.chainId`, for testnet use `138565` for `domain.chainId`. For spot actions, use `spot` for `domain.name` and for perps actions, `futures` for `domain.name`.

After you get the signature bytes `sig`, append byte `1` before the sig bytes to get typed signature. For example, your signed signature is `0x789a6bcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab`, the correct typed signature is `0x01789a6bcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab`.

### How to compute `payloadHash`

`payloadHash = Keccak256(json.Marshal(payload))`

The payload is a JSON object with two fields:

```typescript
{
  "type": "<actionName>",   // e.g. "newOrder", "cancelOrder", "updateLeverage"
  "params": { ... }         // action-specific parameters
}
```

* For spot: `type` is one of `"newOrder"`, `"cancelOrder"`, `"newTwapOrder"`, `"cancelTwapOrder"`, `"transferAsset"`, `"scheduleCancel"`, `"revokeAPIKey"`, and more.
* For perps: `type` is one of `"newOrder"`, `"cancelOrder"`, `"newTwapOrder"`, `"cancelTwapOrder"`, `"updateLeverage"`, `"updateMargin"`, `"updateCollateral"` (testnet only), `"transferAsset"`, `"scheduleCancel"`, `"revokeAPIKey"`, and more.

You must use your private key of the master account to sign `revokeAPIKey` action.

**Important rules for producing a correct `payloadHash`:**

1. **Compact JSON** — no whitespace or newlines. Use `json.Marshal` in Go, or `JSON.stringify` without extra arguments in JavaScript.
2. **Key order must match the Go struct field order** — the server verifies signatures by parsing the request body into Go structs and re-marshaling via `json.Marshal`, which serializes fields in struct definition order. If your JSON keys are in a different order, the hash will differ and signature verification will fail.
   * Refer to the struct definitions in [sodex-go-sdk-public](https://github.com/sodex-tech/sodex-go-sdk-public) for the authoritative field order.
   * For example, `PerpsOrderItem` fields must appear in this order: `clOrdID`, `modifier`, `side`, `type`, `timeInForce`, `price`, `quantity`, `funds`, `stopPrice`, `stopType`, `triggerType`, `reduceOnly`, `positionSide`.
3. **`DecimalString` fields are JSON strings, not numbers** — fields typed as `DecimalString` in the schema (e.g. `price`, `quantity`, `funds`, `stopPrice`) must be serialized as quoted strings in the signing payload (e.g. `"quantity":"0.001"`, not `"quantity":0.001`). The HTTP request body uses the same format.
4. **`omitempty` fields must be omitted when unset** — optional pointer fields in the Go struct (those with `json:",omitempty"`) must not appear in the JSON when they have no value. Non-optional fields (e.g. `modifier`, `reduceOnly`, `positionSide`) must always be present, even with zero values.

**Example** — perps market buy order signing payload:

```json
{"type":"newOrder","params":{"accountID":12345,"symbolID":1,"orders":[{"clOrdID":"my-order-1","modifier":1,"side":1,"type":2,"timeInForce":3,"quantity":"0.001","reduceOnly":false,"positionSide":1}]}}
```

Note: the HTTP request body contains only the `params` object (without the `type` wrapper), using the same field order and types as the signing payload.

## End-to-end signing example

Signing a `newOrder` action with a registered API key, end-to-end.

> Prerequisite: you have already called `addAPIKey` (signed by the master wallet — see [Add API Key](#add-api-key)) and hold the API key's private key locally.

### Inputs

```
Registered API key:
  name                   = "api-key-01"         ← this is what X-API-Key carries
  publicKey (EVM addr)   = 0x3d4595c8742d0a58173a9963c05755b59a8f8256
  private key            = 0x2222222222222222222222222222222222222222222222222222222222222222
                           ← held by the client, used to produce X-API-Sign

Nonce                    = 1760373925001
Signing payload hash     = keccak256(compact JSON of {type, params})
                         = 0x7521d1cadbcfa91eec65aa16715b94ffc1c9654ba57ea2ef1a2127bca1127a83
```

### Steps

1. Compute `payloadHash` as described in [How to compute `payloadHash`](#how-to-compute-payloadhash).
2. EIP-712-sign the `ExchangeAction{payloadHash, nonce}` struct with the **API key's private key** (`0x2222…`) under domain `{name:"futures", chainId:286623, verifyingContract:0x00…00}`.
3. Prepend byte `0x01` to the 65-byte signature → this is the value of `X-API-Sign`.
4. Send the request with `X-API-Key` set to the **name** of the API key:

```bash
curl -X POST https://mainnet-gw.sodex.dev/api/v1/perps/trade/orders \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H "X-API-Key: api-key-01" \
  -H "X-API-Sign: 0x01<65-byte-signature-from-api-key-priv>" \
  -H "X-API-Nonce: 1760373925001" \
  -d '{"accountID":12345,"symbolID":1,"orders":[…]}'
```

The server looks up the API key named `api-key-01` on `accountID=12345`, recovers the signer address from `X-API-Sign`, and verifies it equals the API key's registered `publicKey` (`0x3d45…8256`).

### Common pitfalls

* Putting the API key's EVM address (`0x3d45…8256`) in `X-API-Key` — **wrong**; use the `name` string.
* Signing with a key that does not match `X-API-Key` — when this header names a registered API key, sign with that key's private key. Direct master-wallet signing requires omitting `X-API-Key`; it is supported but not recommended for routine trading. Account-level actions such as API key registration, revocation, and builder-fee approval require the master wallet.
* Forgetting the `0x01` prefix — the server rejects un-typed raw 65-byte signatures.

## Signed Request Example (EIP-712)

Install `viem` and `tsx`, save as `sign.ts`, set `SODEX_API_KEY_PRIVATE_KEY`, and run `npx tsx sign.ts`. This example signs locally and does not submit an order. The account and order values below are examples; use your own registered account and market parameters before submission.

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

async function main() {
  const account = privateKeyToAccount(process.env.SODEX_API_KEY_PRIVATE_KEY as `0x${string}`);
  const nonce = BigInt(Date.now());
  const params = {
    accountID: 12345,
    symbolID: 1,
    orders: [{ clOrdID: "my-order-1", modifier: 1, side: 1, type: 2,
      timeInForce: 3, quantity: "0.001", reduceOnly: false, positionSide: 1 }],
  };
  const payloadHash = keccak256(toHex(JSON.stringify({ type: "newOrder", params })));
  const signature = await account.signTypedData({
    domain: { name: "futures", version: "1", chainId: 286623,
      verifyingContract: "0x0000000000000000000000000000000000000000" },
    types: { ExchangeAction: [
      { name: "payloadHash", type: "bytes32" },
      { name: "nonce", type: "uint64" },
    ] },
    primaryType: "ExchangeAction",
    message: { payloadHash, nonce },
  });
  console.log(JSON.stringify({
    body: params,
    headers: { "X-API-Sign": concatHex(["0x01", signature]), "X-API-Nonce": nonce.toString() },
  }, null, 2));
}
main().catch(console.error);
```
