# Message Signing

## Overview

When interacting with the Ethereal exchange, many operations like trading and account management require cryptographic signatures to authenticate and authorize your actions.

### Why Sign Messages?

The trading API uses cryptographic signatures for authentication instead of traditional JSON Web Tokens (JWTs). When making an API request, rather than including a JWT obtained from a centralized authentication service, **the requesting client signs an EIP-712 structured message using their private key**. The signature proves the client's identity and authorizes them to access the endpoint.

Majority of endpoints are read-only public facing. However, for endpoints that mutate data such as order placement and cancelations, these calls are authenticated and authorised via signatures in the form of EIP-712 messages (<https://eips.ethereum.org/EIPS/eip-712>).

*Each authenticated endpoint requires a different message type to sign*. Ethereal has a few message types including: `LinkSigner`, `RevokeLinkedSigner`, `RefreshLinkedSigner`, `ExtendLinkedSigner`, `EIP712Auth`, `InitiateWithdraw`, `TradeOrder` , and `CancelOrder`. Once a signature is created, they are sent along with the rest of the HTTP payload, validated, stored, and the relayer batches these operations onchain at a later time.

### Signature Types and Domain

Message signing is one of the trickier parts of integrating with the API. It sits at the boundary between onchain message structures and the HTTP API. The `/v1/rpc/config` endpoint provides the EIP-712 domain and type definitions you need to get started.

```bash
curl -X 'GET' \
  'https://api.ethereal.trade/v1/rpc/config' \
  -H 'accept: application/json'
```

```json
{
  "domain": {
    "name": "Ethereal",
    "version": "1",
    "chainId": 5064014,
    "verifyingContract": "0xB3cDC82035C495c484C9fF11eD5f3Ff6d342e3cc"
  },
  "signatureTypes": {
    "LinkSigner": "address sender,address signer,bytes32 subaccount,uint64 nonce,uint64 signedAt",
    "TradeOrder": "address sender,bytes32 subaccount,uint128 quantity,uint128 price,bool reduceOnly,uint8 side,uint8 engineType,uint32 productId,uint64 nonce,uint64 signedAt",
    "InitiateWithdraw": "address account,bytes32 subaccount,address token,uint256 amount,uint64 nonce,uint64 signedAt,bytes32 destinationAddress,uint32 destinationEndpointId",
    "RevokeLinkedSigner": "address sender,address signer,bytes32 subaccount,uint64 nonce,uint64 signedAt",
    "EIP712Auth": "address sender,uint8 intent,uint64 signedAt",
    "CancelOrder": "address sender,bytes32 subaccount,uint64 nonce",
    "RefreshLinkedSigner": "address sender,address signer,uint64 nonce,uint64 signedAt",
    "ExtendLinkedSigner": "address sender,uint64 nonce,uint64 signedAt"
  }
}
```

{% hint style="warning" %}
This configuration may not reflect the current state. Query the live API endpoint `/v1/rpc/config` to retrieve the latest version.
{% endhint %}

There are 2 components to this response: **domain** and **signatureTypes**.

### Domain

The `domain` object provides context for the signed message and helps prevent cross-application replay attacks. It includes:

* `name`: The name of the signing application or protocol (e.g., "Ethereal")
* `version`: The current version of the contract/application
* `chainId`: The chain ID where the signature is valid
* `verifyingContract`: The address of the contract that will verify the signature

This domain information creates a unique context for each application, ensuring that signatures created for one application cannot be reused in another.

### Signature Types

The `signatureTypes`, `messageTypes` (or `types`) defines the structure of the data being signed. It's an object containing named structures with their respective fields and types. In the example above, `LinkSigner` has the following shape:

```
address sender,address signer,bytes32 subaccount,uint64 nonce,uint64 signedAt
```

Which, when parsed gives the following:

```typescript
[
    { name: 'sender', type: 'address' },
    { name: 'signer', type: 'address' },
    { name: 'subaccount', type: 'bytes32' },
    { name: 'nonce', type: 'uint64' },
    { name: 'signedAt', type: 'uint64' },
]
```

### What is a `nonce`?

Every message type includes a `uint64 nonce`. On Ethereal, the nonce functions as a uniqueness parameter that prevents replay attacks by ensuring each signed message can only be processed once. Unlike traditional implementations using sequential counters, Ethereal uses the current timestamp in nanoseconds, providing a high-precision identifier that guarantees no two legitimate transactions will share the same nonce, even when submitted rapidly.

When signing orders or executing operations, this nanosecond timestamp becomes part of the signed data structure. The exchange validates each signature by verifying the timestamp is within an acceptable window and hasn't been previously processed, automatically rejecting any attempt to reuse a signature. This approach supports high-frequency trading without compromising security, as users can generate multiple valid signatures quickly without tracking on-chain state changes.

To generate a nonce, we recommend simply just retrieving the current time in nanoseconds and adding some randomness at the end of the timestamp.

### Message Expiry via `signedAt`

Message nonces are tracked to prevent reuse. Each signed message includes a timestamp (`signedAt`) that the exchange validates against a tolerance window. If the message is too old, it is rejected regardless of whether the signature is otherwise valid. This prevents replay attacks over extended periods, even if a previously signed message is intercepted.

For operations that are batched and verified onchain, the `nonce` provides an additional layer of protection during the delay between signing and onchain confirmation.

### Walkthrough

Below is a TypeScript guide with concrete examples on how to sign messages. The examples below use [viem](https://viem.sh/) as the only dependency. All snippets are self-contained TypeScript that should be able run with `ts-node` or any bundler.

#### Prerequisites

Before signing any message, you need two things: a wallet and the EIP-712 domain.

{% code overflow="wrap" %}

```typescript
import { createWalletClient, http, parseUnits, toHex, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const API_BASE = "https://api.ethereal.trade/v1";

const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY" as Hex);
const walletClient = createWalletClient({ account, transport: http() });
```

{% endcode %}

Fetch the domain once and reuse it for all subsequent signatures. It only changes if the exchange migrates to a new contract.

{% code overflow="wrap" %}

```typescript
const fetchDomain = async () => {
 const res = await fetch(`${API_BASE}/rpc/config`);
 const { domain } = await res.json();
 return domain as {
   name: string;
   version: string;
   chainId: number;
   verifyingContract: Hex;
 };
};

const domain = await fetchDomain();
```

{% endcode %}

#### Message Timings: `nonce` and `signedAt`&#x20;

Every signed message includes timing fields. These serve different purposes and use different units. Mixing them up is one of the most common integration mistakes.

* `nonce` nanoseconds since Unix Epoch. Used for replay protection and uniqueness. Sent as a string because nanosecond timestamps exceed JavaScript's safe integer range.
* `signedAt` seconds since Unix Epoch. Used to check message freshness. Sent as a number.

Both are validated against the server's clock: `nonce` **must be within 1 hour**, and `signedAt` **must be within 1 hour in the past and 10 seconds in the future**.

#### Subaccount `name` Encoding

Subaccounts are identified by a `bytes32` value. This is most likely a UTF-8 name right-padded with zeros to 32 bytes. If you made a deposit on app.ethereal.trade, the default subaccount name is "`primary`". Read through [subaccounts](https://docs.ethereal.trade/trading/perpetual-futures/subaccounts "mention") if you are unfamiliar with subaccounts on Ethereal.

{% code overflow="wrap" %}

```typescript
// "primary" is "0x7072696d61727900000000000000000000000000000000000000000000000000"
```

{% endcode %}

#### Decimal Precision

All quantities and prices on Ethereal use **9 decimal places of precision**. The API request body accepts human-readable decimal strings like "5.5", but the EIP-712 signed message requires the raw `bigint` representation.

{% hint style="info" %}
A common mistake is using 18 decimals (ETH wei). Ethereal uses 9.
{% endhint %}

{% code overflow="wrap" %}

```typescript
 const toGwei = (decimal: string): bigint => parseUnits(decimal, 9);

 // "5.5"    -> 5500000000n
 // "4200.5" -> 4200500000000n
```

{% endcode %}

{% hint style="warning" %}
Always derive both the signed `bigint` value and the request body's decimal string from the same source of truth. Floating-point arithmetic in most languages introduces tiny rounding errors (e.g., `0.1 + 0.2` producing `0.30000000000000004`).\
\
If your price or quantity string has trailing noise like `"5.500000000000000003"`, it will not match the `bigint` value you signed, and the server will return a 401.\
\
**The safest approach is to work with string representations throughout and avoid intermediate floating-point math entirely.**
{% endhint %}

#### Order Placement/Cancelation

Order placement is the most common signing operation. The signed message type is `TradeOrder`, and the EIP-712 type definition looks like this:

{% code overflow="wrap" %}

```typescript
const Types = {
 TradeOrder: [
   { name: "sender", type: "address" },
   { name: "subaccount", type: "bytes32" },
   { name: "quantity", type: "uint256" },
   { name: "price", type: "uint256" },
   { name: "reduceOnly", type: "bool" },
   { name: "side", type: "uint8" },
   { name: "engineType", type: "uint8" },
   { name: "productId", type: "uint32" },
   { name: "nonce", type: "uint64" },
   { name: "signedAt", type: "uint64" },
 ],
} as const;
```

{% endcode %}

{% hint style="info" %}
The type definitions here are hardcoded for clarity. You can also parse them dynamically from the `signatureTypes` field in the `/v1/rpc/config` response.
{% endhint %}

{% hint style="info" %}
The `side` and `engineType` fields are numeric enums. `side=0=Buy`, `side=1=Sell`, `engineType=0=Perp`, `engineType=1=Spot`. As of writing Ethereal only supports `engineType=0`.
{% endhint %}

To place a ***limit order***, sign the `TradeOrder` message with the desired `price` and `quantity`, then send it alongside the order details in the request body.

A few important differences between the signed message and the request body to be aware of:

* `productId` in the signature corresponds to `onchainId` in the body
  * You can find out a product's `onchainId` by listing products via the REST API
* `price` and `quantity` are bigint values in the signature but decimal strings in the body
* `nonce` is a `bigint` in the signature *but a string in the body*
* `signedAt` is a `bigint` in the signature *but a number in the body*

{% code overflow="wrap" expandable="true" %}

```typescript
const placeLimitOrder = async () => {
 const nonce = getNonce();
 const signedAt = getSignedAt();
 const subaccount = encodeSubaccount("primary");

 const signature = await walletClient.signTypedData({
   account,
   domain,
   types,
   primaryType: "TradeOrder",
   message: {
     sender: account.address,
     subaccount,
     quantity: toGwei("5.5"),
     price: toGwei("4200.5"),
     reduceOnly: false,
     side: 0, // BUY
     engineType: 0, // PERP
     productId: 1, // 1=BTCUSD
     nonce,
     signedAt: BigInt(signedAt),
   },
 });

 const res = await fetch(`${API_BASE}/order`, {
   method: "POST",
   headers: { "Content-Type": "application/json" },
   body: JSON.stringify({
     data: {
       sender: account.address,
       subaccount,
       quantity: "5.5",
       price: "4200.5",
       reduceOnly: false,
       side: 0,
       engineType: 0,
       onchainId: 1,
       type: "LIMIT",
       timeInForce: "GTD",
       postOnly: false,
       nonce: nonce.toString(),
       signedAt,
     },
     signature,
   }),
 });

 return res.json();
};
```

{% endcode %}

***Market orders*** use the same `TradeOrder` signature type, but with **one critical difference**: the `price` must be `0n` (zero) in the signed message. In the request body, set type: "`MARKET`" and omit the `price` field entirely.

{% code overflow="wrap" expandable="true" %}

```typescript
 const placeMarketOrder = async () => {
   const nonce = getNonce();
   const signedAt = getSignedAt();
   const subaccount = encodeSubaccount("primary");

   const signature = await walletClient.signTypedData({
     account,
     domain,
     types,
     primaryType: "TradeOrder",
     message: {
       sender: account.address,
       subaccount,
       quantity: toGwei("5.5"),
       price: 0n, // Must be 0 for market orders
       reduceOnly: false,
       side: 0, // BUY
       engineType: 0, // PERP
       productId: 1, // 1=BTCUSD
       nonce,
       signedAt: BigInt(signedAt),
     },
   });

   const res = await fetch(`${API_BASE}/order`, {
     method: "POST",
     headers: { "Content-Type": "application/json" },
     body: JSON.stringify({
       data: {
         sender: account.address,
         subaccount,
         quantity: "5.5",
         // no price field for market orders
         reduceOnly: false,
         side: 0,
         engineType: 0,
         onchainId: 1,
         type: "MARKET",
         nonce: nonce.toString(),
         signedAt,
       },
       signature,
     }),
   });

   return res.json();
 };
```

{% endcode %}

{% hint style="info" %}
Signing a market order with a non-zero price is one of the most common causes of 4xx errors. The signature will not match what the server expects.
{% endhint %}

The `CancelOrder` signature is simpler than `TradeOrder`. It only requires the `sender`, `subaccount`, and `nonce`. The specific orders to cancel are listed in the request body, *not in the signed message*. This means a single signature can cancel up to **200 orders** at once.

{% code overflow="wrap" expandable="true" %}

```typescript
const Types = {
  CancelOrder: [
    { name: "sender", type: "address" },
    { name: "subaccount", type: "bytes32" },
    { name: "nonce", type: "uint64" },
  ],
} as const;
```

{% endcode %}

You can specify orders an API assigned UUID via `orderIds`, your `clientOrderIds`, or both. The combined count of both arrays **cannot exceed 200**.

{% code overflow="wrap" expandable="true" %}

```typescript
const cancelOrders = async (orderIds: string[]) => {
  const nonce = getNonce();
  const subaccount = encodeSubaccount("primary");

  const signature = await walletClient.signTypedData({
    account,
    domain,
    types,
    primaryType: "CancelOrder",
    message: { sender: account.address, subaccount, nonce },
  });

  const res = await fetch(`${API_BASE}/order/cancel`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      data: {
        sender: account.address,
        subaccount,
        nonce: nonce.toString(),
        orderIds,
      },
      signature,
    }),
  });

  return res.json();
};
```

{% endcode %}

#### Delegated Trading via `LinkedSigner`

Linked signers enable delegated trading. This is a secondary private key that can place orders and cancel orders on behalf of your account without needing your primary wallet to approve every action. This is what powers one-click trading in the Ethereal exchange app.

Key properties of linked signers:

* They **can** place orders and cancel orders for the subaccount they're linked to
* They **cannot** withdraw funds - only the account owner retains withdrawal control
* They expire after **90 days** of inactivity
* A subaccount can have **multiple** linked signers (useful for multi-device setups or bots)

To link a signer, both parties must sign the same `LinkSigner` message: the account owner (proving they authorize this delegation) and the new signer (proving they control the signer key). The client typically generates the signer's private key locally.

{% code overflow="wrap" expandable="true" %}

```typescript
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";

const Types = {
  LinkSigner: [
    { name: "sender", type: "address" },
    { name: "signer", type: "address" },
    { name: "subaccount", type: "bytes32" },
    { name: "nonce", type: "uint64" },
    { name: "signedAt", type: "uint64" },
  ],
} as const;
```

{% endcode %}

{% code overflow="wrap" expandable="true" %}

```typescript
const linkSigner = async (subaccountId: string) => {
  // Generate a new private key for the linked signer — store this securely
  const signerPrivateKey = generatePrivateKey();
  const signerAccount = privateKeyToAccount(signerPrivateKey);
  const signerWalletClient = createWalletClient({
    account: signerAccount,
    transport: http(),
  });

  const nonce = getNonce();
  const signedAt = getSignedAt();
  const subaccount = encodeSubaccount("primary");

  // The same message is signed by both parties
  const message = {
    sender: account.address,
    signer: signerAccount.address,
    subaccount,
    nonce,
    signedAt: BigInt(signedAt),
  };

  const [signature, signerSignature] = await Promise.all([
    walletClient.signTypedData({
      account,
      domain,
      types,
      primaryType: "LinkSigner",
      message,
    }),
    signerWalletClient.signTypedData({
      account: signerAccount,
      domain,
      types: LinkSignerTypes,
      primaryType: "LinkSigner",
      message,
    }),
  ]);

  const res = await fetch(`${API_BASE}/linked-signer/link`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      data: {
        subaccountId,
        sender: account.address,
        signer: signerAccount.address,
        subaccount,
        nonce: nonce.toString(),
        signedAt,
      },
      signature,       // from the account owner
      signerSignature, // from the new signer
    }),
  });

  return { signerPrivateKey, response: await res.json() };
};
```

{% endcode %}

{% hint style="danger" %}
Store the `signerPrivateKey` securely. You'll need it for all subsequent trades made through this linked signer. If lost, you can always revoke and link a new one.
{% endhint %}

Once linked, trading with a signer works exactly like trading with your main account. The only difference is that the `sender` field must be the **linked signer's address** (not the account owner's), and you sign with the signer's private key.

{% hint style="info" %}
A common mistake is setting `sender` to the account owner's address when signing with a linked signer. The `sender` must always be the address of whoever is producing the signature.
{% endhint %}

#### Smart Contract Wallets

Smart contract wallets (e.g., Safe/Gnosis multisigs) are supported via [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271). The exchange automatically detects whether the sender address is a contract and calls `isValidSignature` on it rather than performing ECDSA recovery. No changes to the request format are needed from the caller's side.

**One restriction:** linked signers must always be EOA wallets. Smart contract wallets can be account owners, but they cannot be used as linked signers.

### Common Issues & Troubleshooting

#### **Floating-point precision loss (401)**

This is the single most common issue. When the decimal string in your request body doesn't exactly match the `bigint` value in your signed message, the server recomputes a different hash and signature verification fails.

The root cause is floating-point arithmetic. In most programming languages, operations on decimal numbers introduce tiny rounding errors. For example, a price that should be `"1234.5"` might end up as `"1234.500000000000000003"` after passing through floating-point math. The server parses this string into a `bigint` and gets a different value than what you signed.

```ts
// Dangerous: floating-point math can introduce rounding noise
const price = 0.1 + 0.2;            // 0.30000000000000004
const priceStr = String(price);     // "0.30000000000000004"
parseUnits(priceStr, 9);            // 300000000n - but you might have signed 300000001n

// Safe: keep values as strings, never pass through floating-point
const price = "0.3";
parseUnits(price, 9);               // 300000000n (correct)
```

The safest approach:

* **Pass prices and quantities as strings throughout your entire pipeline.** If you receive a number from an upstream source, convert it to a string with explicit precision before doing anything else.
* **Derive both the signed `bigint` and the request body string from the same string literal.** For example, define `const qty = "5.5"`, then use `toGwei(qty)` in the signature and `qty` in the body.
* **Never use floating-point arithmetic** (addition, multiplication, division) on prices or quantities. If you need to compute a value, do it in `bigint` space and format back to a string.

#### **Signature verification failed (401)**

A 401 with signature verification failure means the server recovered a different address from the signature than the `sender` you specified. Common causes:

**Market order signed with a non-zero price.** For market orders, the `price` field in the signed message must be `0n`. The request body should set `type: "MARKET"` and omit `price` entirely.

**Wrong decimal precision.** Ethereal uses 9 decimal places, not 18. If you're coming from an ETH/ERC-20 background where `parseUnits(value, 18)` is the norm, this is easy to get wrong.

```ts
parseUnits("5.5", 9);   // 5500000000n          (correct)
parseUnits("5.5", 18);  // 5500000000000000000n (incorrect)
```

**Sender mismatch.** The `sender` address in the signed message must be the address of whoever is signing. When using a linked signer, this must be the signer's address and not the account owner's address.

**Stale EIP-712 domain.** If you've hardcoded the domain rather than fetching from `/v1/rpc/config`, it may be outdated after a contract migration.

**Non-standard `v` value.** Ethereal only accepts signature `v` values of `27` or `28`. `viem` produces this format by default. If you're using a different library that returns `v` as `0` or `1`, add `27`.

#### **Timestamp or nonce rejected (400)**

**Clock skew.** The `signedAt` timestamp must be within 1 hour in the past and 10 seconds in the future relative to the server's clock. Ensure your system clock is NTP-synced.

**Wrong nonce unit.** The nonce must be in **nanoseconds**, not seconds or milliseconds:

```ts
BigInt(Date.now()) * 1_000_000n;       // milliseconds -> nanoseconds (correct)
BigInt(Date.now());                    // milliseconds (incorrect)
BigInt(Math.floor(Date.now() / 1000)); // seconds      (incorrect)
```

`nonce` and `signedAt` are validated before signature verification. If they fall outside the allowed range, the request is rejected with a 400 validation error. You won't even reach signature checking.

#### **Linked signer issues (400/401)**

**Signer expired.** Linked signers expire after *90 days of inactivity*. Check the signer's status via `GET /v1/linked-signer/address/{address}`. Use `POST /linked-signer/extend` (signed by the signer) or `POST /linked-signer/refresh` (signed by the account owner) to reactivate.

**Wrong subaccount.** A linked signer is scoped to the subaccount it was linked to. It cannot sign orders for a different subaccount.

**Revoking with open orders.** All resting orders must be canceled before a linked signer can be revoked.

#### **Validation errors (400)**

Any kind of input validation occur leading to 400 can prevent order placement. There are many but the common errors we see include:

* **Subaccount not 32 bytes.** The subaccount must be a `0x`-prefixed hex string representing exactly 32 bytes (66 characters total). Use `toHex(bytes, { size: 32 })` to ensure correct padding
* **Cancel batch too large.** A single cancel request can target at most **200 orders** (`orderIds` and `clientOrderIds` combined)
* **`onchainId` vs `productId`.** The request body uses `onchainId`, while the signed message uses `productId`
* **Order expiry out of range.** If you set `expiresAt`, it must be greater than `signedAt` and at most `signedAt + 6652800` (\~77 days)
* **`postOnly` requires GTD.** If `postOnly` is `true`, the `timeInForce` must be `"GTD"`
* **`close` only on market orders.** The `close` flag (to close an entire position) is only valid on market orders with `reduceOnly: true` and `quantity: "0"`

{% hint style="info" %}
For more examples, read through [Python SDK](https://docs.ethereal.trade/developer-guides/sdk/python-sdk) as it has utility functions to assist with message signing.
{% endhint %}
