Skip to content

EIP-712 Signing Implementation

This guide provides detailed implementation instructions for EIP-712 signature generation across different programming languages and libraries.

Core Concepts

Domain Separator

The domain separator for off-chain authentication (uses zero address as verifying contract):

const domain = {
  name: "Synthetix",
  version: "1",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000000"
};

Message Types

Each API method has its own EIP-712 type definition. Here are the actual implementations:

WebSocket Authentication

const authTypes = {
  AuthMessage: [
    { name: "subAccountId", type: "uint256" },
    { name: "timestamp", type: "uint256" },
    { name: "action", type: "string" }
  ]
};
 
// Example message
const authMessage = {
  subAccountId: "123456789", // Your subaccount ID
  timestamp: Math.floor(Date.now() / 1000), // Unix timestamp in seconds
  action: "websocket_auth"
};

Place Orders

const placeOrderTypes = {
  Order: [
    { name: "symbol", type: "string" },
    { name: "side", type: "string" },
    { name: "orderType", type: "string" },
    { name: "price", type: "string" },
    { name: "triggerPrice", type: "string" },
    { name: "quantity", type: "string" },
    { name: "reduceOnly", type: "bool" },
    { name: "isTriggerMarket", type: "bool" },
    { name: "clientOrderId", type: "string" },
    { name: "closePosition", type: "bool" }
  ],
  PlaceOrders: [
    { name: "subAccountId", type: "uint256" },
    { name: "orders", type: "Order[]" },
    { name: "grouping", type: "string" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};
 
// Order type values — plain strings, no JSON.stringify needed
const orderTypes = {
  limitGtc: "limitGtc",        // Limit order, Good Till Canceled
  limitIoc: "limitIoc",        // Limit order, Immediate or Cancel
  limitAlo: "limitAlo",        // Limit order, Add Liquidity Only (post-only)
  market: "market",            // Market order
  triggerSl: "triggerSl",      // Trigger Stop Loss
  triggerTp: "triggerTp"       // Trigger Take Profit
};
 
// Example usage in order:
const order = {
  symbol: "BTC-USDT",
  side: "buy",
  orderType: "limitGtc",       // Plain string value
  price: "50000.00",           // Required for limit orders, empty string for market
  triggerPrice: "",            // Required for trigger orders, empty string otherwise
  quantity: "0.1",
  reduceOnly: false,
  isTriggerMarket: false,      // For trigger orders: true = market execution
  clientOrderId: "0x1234567890abcdef1234567890abcdef",
  closePosition: false
};

Cancel Orders

const cancelOrderTypes = {
  CancelOrders: [
    { name: "subAccountId", type: "uint256" },
    { name: "orderIds", type: "uint256[]" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};

Cancel All Orders

const cancelAllOrderTypes = {
  CancelAllOrders: [
    { name: "subAccountId", type: "uint256" },
    { name: "symbol", type: "string" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};

Modify Order

const modifyOrderTypes = {
  ModifyOrder: [
    { name: "subAccountId", type: "uint256" },
    { name: "orderId", type: "uint256" },
    { name: "price", type: "string" },
    { name: "quantity", type: "string" },
    { name: "triggerPrice", type: "string" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};

Update Leverage

const updateLeverageTypes = {
  UpdateLeverage: [
    { name: "subAccountId", type: "uint256" },
    { name: "symbol", type: "string" },
    { name: "leverage", type: "string" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};

Create Subaccount

const createSubaccountTypes = {
  CreateSubaccount: [
    { name: "masterSubAccountId", type: "uint256" },
    { name: "name", type: "string" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};
 
// Note: masterSubAccountId is the ID of an existing subaccount you own.
// It proves ownership and links the new subaccount to your master account.

Update SubAccount Name

const updateSubAccountNameTypes = {
  UpdateSubAccountName: [
    { name: "subAccountId", type: "uint256" },
    { name: "name", type: "string" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};

Add Delegated Signer

const addDelegatedSignerTypes = {
  AddDelegatedSigner: [
    { name: "delegateAddress", type: "address" },
    { name: "subAccountId", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" },
    { name: "expiresAt", type: "uint256" },
    { name: "permissions", type: "string[]" }
  ]
};
 
// expiresAt: when the delegation permission expires (0 = no expiration)
// expiresAfter: when this signing request expires
// permissions: currently supports ["trading"]

Remove All Delegated Signers

const removeAllDelegatedSignersTypes = {
  RemoveAllDelegatedSigners: [
    { name: "subAccountId", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
};

SubAccount Actions

Used for: getPositions, getOpenOrders, getOrderHistory, getTrades, getFundingPayments, getSubAccount, getDelegatedSigners, getBalanceUpdates

const subAccountActionTypes = {
  SubAccountAction: [
    { name: "subAccountId", type: "uint256" },
    { name: "action", type: "string" },
    { name: "expiresAfter", type: "uint256" }
  ]
};

Nonce

The nonce field is a replay-protection value required for all state-changing actions (any action not starting with get). Each nonce must be a positive integer, incrementing and unique per request.

Recommended generation approaches:
  • Cryptographically random bytes converted to a uint256 (reference implementation approach)
  • Date.now() also works as a convenient non-zero unique value

The nonce is not validated as a timestamp — it only needs to be a positive integer, incrementing and unique per request.

expiresAfter

The expiresAfter field is an optional Unix timestamp in milliseconds that sets when the request expires. Use 0 to indicate no expiration.

const expiresAfter = Date.now() + 300000; // 5 minutes from now (milliseconds)

JavaScript/TypeScript Implementation

Using viem

import { createWalletClient, http, parseSignature } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { mainnet } from 'viem/chains';
import crypto from 'crypto';
 
// Signature validation function
function validateSignature(signature: { v: number; r: string; s: string }) {
  if (!signature || typeof signature !== 'object') {
    throw new Error('Invalid signature object');
  }
 
  // Validate v - must be 0, 1, 27, or 28
  if (![0, 1, 27, 28].includes(signature.v)) {
    throw new Error(`Invalid v value: ${signature.v}. Must be 0, 1, 27, or 28`);
  }
 
  // Validate r - must be 32-byte hex string
  if (!signature.r || !/^0x[a-fA-F0-9]{64}$/.test(signature.r)) {
    throw new Error('Invalid r value: must be 0x-prefixed 32-byte hex string');
  }
 
  // Validate s - must be 32-byte hex string
  if (!signature.s || !/^0x[a-fA-F0-9]{64}$/.test(signature.s)) {
    throw new Error('Invalid s value: must be 0x-prefixed 32-byte hex string');
  }
 
  return true;
}
 
class SynthetixSigner {
  private account: any;
  private client: any;
  private domain: any;
 
  constructor(privateKey: `0x${string}`, chainId = 1) {
    this.account = privateKeyToAccount(privateKey);
    this.client = createWalletClient({
      account: this.account,
      chain: chainId === 1 ? mainnet : mainnet, // Add other chains as needed
      transport: http()
    });
 
    this.domain = {
      name: "Synthetix",
      version: "1",
      chainId: BigInt(chainId),
      verifyingContract: "0x0000000000000000000000000000000000000000"
    };
  }
 
  async signWebSocketAuth(subAccountId: string, timestamp?: number) {
    const types = {
      AuthMessage: [
        { name: "subAccountId", type: "uint256" },
        { name: "timestamp", type: "uint256" },
        { name: "action", type: "string" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(subAccountId),
      timestamp: BigInt(timestamp || Math.floor(Date.now() / 1000)), // Unix seconds
      action: "websocket_auth"
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "AuthMessage",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signPlaceOrder(orderData: {
    subAccountId: string;
    orders: any[];
    grouping: string;
    nonce?: number;
    expiresAfter?: number;
  }) {
    const types = {
      Order: [
        { name: "symbol", type: "string" },
        { name: "side", type: "string" },
        { name: "orderType", type: "string" },
        { name: "price", type: "string" },
        { name: "triggerPrice", type: "string" },
        { name: "quantity", type: "string" },
        { name: "reduceOnly", type: "bool" },
        { name: "isTriggerMarket", type: "bool" },
        { name: "clientOrderId", type: "string" },
        { name: "closePosition", type: "bool" }
      ],
      PlaceOrders: [
        { name: "subAccountId", type: "uint256" },
        { name: "orders", type: "Order[]" },
        { name: "grouping", type: "string" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(orderData.subAccountId),
      orders: orderData.orders,
      grouping: orderData.grouping,
      nonce: BigInt(orderData.nonce || Date.now()),
      expiresAfter: BigInt(orderData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "PlaceOrders",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signCancelOrder(cancelData: {
    subAccountId: string;
    orderIds: string[];
    nonce?: number;
    expiresAfter?: number;
  }) {
    const types = {
      CancelOrders: [
        { name: "subAccountId", type: "uint256" },
        { name: "orderIds", type: "uint256[]" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(cancelData.subAccountId),
      orderIds: cancelData.orderIds.map(id => BigInt(id)),
      nonce: BigInt(cancelData.nonce || Date.now()),
      expiresAfter: BigInt(cancelData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "CancelOrders",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signCancelAllOrders(cancelAllData: {
    subAccountId: string;
    symbol: string;
    nonce?: number;
    expiresAfter?: number;
  }) {
    const types = {
      CancelAllOrders: [
        { name: "subAccountId", type: "uint256" },
        { name: "symbol", type: "string" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(cancelAllData.subAccountId),
      symbol: cancelAllData.symbol,
      nonce: BigInt(cancelAllData.nonce || Date.now()),
      expiresAfter: BigInt(cancelAllData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "CancelAllOrders",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signModifyOrder(modifyData: {
    subAccountId: string;
    orderId: string;
    price?: string;
    quantity?: string;
    triggerPrice?: string;
    nonce?: number;
    expiresAfter?: number;
  }) {
    const types = {
      ModifyOrder: [
        { name: "subAccountId", type: "uint256" },
        { name: "orderId", type: "uint256" },
        { name: "price", type: "string" },
        { name: "quantity", type: "string" },
        { name: "triggerPrice", type: "string" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(modifyData.subAccountId),
      orderId: BigInt(modifyData.orderId),
      price: modifyData.price || "",
      quantity: modifyData.quantity || "",
      triggerPrice: modifyData.triggerPrice || "",
      nonce: BigInt(modifyData.nonce || Date.now()),
      expiresAfter: BigInt(modifyData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "ModifyOrder",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signUpdateLeverage(leverageData: {
    subAccountId: string;
    symbol: string;
    leverage: string;
    nonce?: number;
    expiresAfter?: number;
  }) {
    const types = {
      UpdateLeverage: [
        { name: "subAccountId", type: "uint256" },
        { name: "symbol", type: "string" },
        { name: "leverage", type: "string" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(leverageData.subAccountId),
      symbol: leverageData.symbol,
      leverage: leverageData.leverage,
      nonce: BigInt(leverageData.nonce || Date.now()),
      expiresAfter: BigInt(leverageData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "UpdateLeverage",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signCreateSubaccount(createData: {
    masterSubAccountId: string;
    name: string;
    nonce?: number;
    expiresAfter?: number;
  }) {
    const types = {
      CreateSubaccount: [
        { name: "masterSubAccountId", type: "uint256" },
        { name: "name", type: "string" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      masterSubAccountId: BigInt(createData.masterSubAccountId),
      name: createData.name,
      nonce: BigInt(createData.nonce || Date.now()),
      expiresAfter: BigInt(createData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "CreateSubaccount",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signUpdateSubAccountName(updateData: {
    subAccountId: string;
    name: string;
    nonce?: number;
    expiresAfter?: number;
  }) {
    const types = {
      UpdateSubAccountName: [
        { name: "subAccountId", type: "uint256" },
        { name: "name", type: "string" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(updateData.subAccountId),
      name: updateData.name,
      nonce: BigInt(updateData.nonce || Date.now()),
      expiresAfter: BigInt(updateData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "UpdateSubAccountName",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signAddDelegatedSigner(delegateData: {
    delegateAddress: string;
    subAccountId: string;
    nonce?: number;
    expiresAfter?: number;
    expiresAt?: number;
    permissions: string[];
  }) {
    const types = {
      AddDelegatedSigner: [
        { name: "delegateAddress", type: "address" },
        { name: "subAccountId", type: "uint256" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" },
        { name: "expiresAt", type: "uint256" },
        { name: "permissions", type: "string[]" }
      ]
    };
 
    const message = {
      delegateAddress: delegateData.delegateAddress,
      subAccountId: BigInt(delegateData.subAccountId),
      nonce: BigInt(delegateData.nonce || Date.now()),
      expiresAfter: BigInt(delegateData.expiresAfter || 0),
      expiresAt: BigInt(delegateData.expiresAt || 0),
      permissions: delegateData.permissions
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "AddDelegatedSigner",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signRemoveAllDelegatedSigners(removeData: {
    subAccountId: string;
    nonce?: number;
    expiresAfter?: number;
  }) {
    const types = {
      RemoveAllDelegatedSigners: [
        { name: "subAccountId", type: "uint256" },
        { name: "nonce", type: "uint256" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(removeData.subAccountId),
      nonce: BigInt(removeData.nonce || Date.now()),
      expiresAfter: BigInt(removeData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "RemoveAllDelegatedSigners",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
 
  async signSubAccountAction(actionData: {
    subAccountId: string;
    action: string;
    expiresAfter?: number;
  }) {
    const types = {
      SubAccountAction: [
        { name: "subAccountId", type: "uint256" },
        { name: "action", type: "string" },
        { name: "expiresAfter", type: "uint256" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(actionData.subAccountId),
      action: actionData.action,
      expiresAfter: BigInt(actionData.expiresAfter || 0)
    };
 
    const signature = await this.client.signTypedData({
      domain: this.domain,
      types,
      primaryType: "SubAccountAction",
      message
    });
 
    const parsedSig = parseSignature(signature);
    validateSignature(parsedSig);
    return parsedSig;
  }
}
 
// Usage Example
async function example() {
  const signer = new SynthetixSigner("0x..." as `0x${string}`);
 
  // WebSocket authentication (timestamp in seconds)
  const authSig = await signer.signWebSocketAuth("123456789");
  console.log('Auth signature:', authSig);
 
  // Place orders
  const orderSig = await signer.signPlaceOrder({
    subAccountId: "123456789",
    orders: [{
      symbol: "BTC-USDT",
      side: "buy",
      orderType: "limitGtc",
      price: "50000",
      triggerPrice: "",
      quantity: "0.1",
      reduceOnly: false,
      isTriggerMarket: false,
      clientOrderId: "0x1234567890abcdef1234567890abcdef",
      closePosition: false
    }],
    grouping: "na",
    nonce: Date.now()
  });
  console.log('Order signature:', orderSig);
 
  // Cancel orders
  const cancelSig = await signer.signCancelOrder({
    subAccountId: "123456789",
    orderIds: ["987654321", "987654322"],
    nonce: Date.now()
  });
  console.log('Cancel signature:', cancelSig);
 
  // Cancel all orders for a symbol
  const cancelAllSig = await signer.signCancelAllOrders({
    subAccountId: "123456789",
    symbol: "BTC-USDT",
    nonce: Date.now()
  });
  console.log('Cancel all signature:', cancelAllSig);
 
  // Update leverage
  const leverageSig = await signer.signUpdateLeverage({
    subAccountId: "123456789",
    symbol: "BTC-USDT",
    leverage: "20",
    nonce: Date.now()
  });
  console.log('Update leverage signature:', leverageSig);
 
  // Create subaccount
  const createSig = await signer.signCreateSubaccount({
    masterSubAccountId: "123456789", // An existing subaccount you own
    name: "New Trading Account",
    nonce: Date.now()
  });
  console.log('Create subaccount signature:', createSig);
 
  // Update subaccount name
  const renameSig = await signer.signUpdateSubAccountName({
    subAccountId: "123456789",
    name: "Renamed Account",
    nonce: Date.now()
  });
  console.log('Update subaccount name signature:', renameSig);
 
  // Add delegated signer
  const addDelegateSig = await signer.signAddDelegatedSigner({
    delegateAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
    subAccountId: "123456789",
    permissions: ["trading"],
    nonce: Date.now()
  });
  console.log('Add delegated signer signature:', addDelegateSig);
 
  // Remove all delegated signers
  const removeAllSig = await signer.signRemoveAllDelegatedSigners({
    subAccountId: "123456789",
    nonce: Date.now()
  });
  console.log('Remove all delegated signers signature:', removeAllSig);
 
  // Get positions (no nonce required for SubAccountAction)
  const positionsSig = await signer.signSubAccountAction({
    subAccountId: "123456789",
    action: "getPositions",
    expiresAfter: Date.now() + 300000 // Optional: 5 minute expiration (milliseconds)
  });
  console.log('Positions signature:', positionsSig);
}

Python Implementation

Using web3.py and eth_account

from eth_account.messages import encode_structured_data
from eth_account import Account
import json
import time
import re
import os
 
def validate_signature(signature):
    """Validate EIP-712 signature components"""
    if not isinstance(signature, dict):
        raise ValueError("Invalid signature object")
 
    # Validate v - must be 0, 1, 27, or 28
    v = signature.get('v')
    if v not in [0, 1, 27, 28]:
        raise ValueError(f"Invalid v value: {v}. Must be 0, 1, 27, or 28")
 
    # Validate r - must be 32-byte hex string
    r_val = signature.get('r')
    if not r_val or not re.match(r'^0x[a-fA-F0-9]{64}$', r_val):
        raise ValueError("Invalid r value: must be 0x-prefixed 32-byte hex string")
 
    # Validate s - must be 32-byte hex string
    s_val = signature.get('s')
    if not s_val or not re.match(r'^0x[a-fA-F0-9]{64}$', s_val):
        raise ValueError("Invalid s value: must be 0x-prefixed 32-byte hex string")
 
    return True
 
DOMAIN_FIELDS = [
    {"name": "name", "type": "string"},
    {"name": "version", "type": "string"},
    {"name": "chainId", "type": "uint256"},
    {"name": "verifyingContract", "type": "address"}
]
 
class SynthetixSigner:
    def __init__(self, private_key, chain_id=1):
        self.account = Account.from_key(private_key)
        self.domain = {
            "name": "Synthetix",
            "version": "1",
            "chainId": chain_id,
            "verifyingContract": "0x0000000000000000000000000000000000000000"
        }
 
    def _sign(self, types, primary_type, message, domain=None):
        typed_data = {
            "types": {
                "EIP712Domain": DOMAIN_FIELDS,
                **types
            },
            "primaryType": primary_type,
            "domain": domain or self.domain,
            "message": message
        }
 
        encoded = encode_structured_data(typed_data)
        signed = self.account.sign_message(encoded)
 
        signature = {
            "v": signed.v,
            "r": "0x" + signed.r.hex(),
            "s": "0x" + signed.s.hex()
        }
        validate_signature(signature)
        return signature
 
    def sign_websocket_auth(self, sub_account_id, timestamp=None):
        if timestamp is None:
            timestamp = int(time.time())  # Unix seconds
 
        return self._sign(
            types={
                "AuthMessage": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "timestamp", "type": "uint256"},
                    {"name": "action", "type": "string"}
                ]
            },
            primary_type="AuthMessage",
            message={
                "subAccountId": sub_account_id,
                "timestamp": timestamp,
                "action": "websocket_auth"
            }
        )
 
    def sign_place_order(self, order_data):
        return self._sign(
            types={
                "Order": [
                    {"name": "symbol", "type": "string"},
                    {"name": "side", "type": "string"},
                    {"name": "orderType", "type": "string"},
                    {"name": "price", "type": "string"},
                    {"name": "triggerPrice", "type": "string"},
                    {"name": "quantity", "type": "string"},
                    {"name": "reduceOnly", "type": "bool"},
                    {"name": "isTriggerMarket", "type": "bool"},
                    {"name": "clientOrderId", "type": "string"},
                    {"name": "closePosition", "type": "bool"}
                ],
                "PlaceOrders": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "orders", "type": "Order[]"},
                    {"name": "grouping", "type": "string"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            primary_type="PlaceOrders",
            message={
                "subAccountId": order_data['subAccountId'],
                "orders": order_data['orders'],
                "grouping": order_data['grouping'],
                "nonce": order_data.get('nonce', int(time.time() * 1000)),
                "expiresAfter": order_data.get('expiresAfter', 0)
            }
        )
 
    def sign_cancel_order(self, cancel_data):
        return self._sign(
            types={
                "CancelOrders": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "orderIds", "type": "uint256[]"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            primary_type="CancelOrders",
            message={
                "subAccountId": cancel_data['subAccountId'],
                "orderIds": cancel_data['orderIds'],
                "nonce": cancel_data.get('nonce', int(time.time() * 1000)),
                "expiresAfter": cancel_data.get('expiresAfter', 0)
            }
        )
 
    def sign_cancel_all_orders(self, cancel_all_data):
        return self._sign(
            types={
                "CancelAllOrders": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "symbol", "type": "string"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            primary_type="CancelAllOrders",
            message={
                "subAccountId": cancel_all_data['subAccountId'],
                "symbol": cancel_all_data['symbol'],
                "nonce": cancel_all_data.get('nonce', int(time.time() * 1000)),
                "expiresAfter": cancel_all_data.get('expiresAfter', 0)
            }
        )
 
    def sign_modify_order(self, modify_data):
        return self._sign(
            types={
                "ModifyOrder": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "orderId", "type": "uint256"},
                    {"name": "price", "type": "string"},
                    {"name": "quantity", "type": "string"},
                    {"name": "triggerPrice", "type": "string"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            primary_type="ModifyOrder",
            message={
                "subAccountId": modify_data['subAccountId'],
                "orderId": modify_data['orderId'],
                "price": modify_data.get('price', ""),
                "quantity": modify_data.get('quantity', ""),
                "triggerPrice": modify_data.get('triggerPrice', ""),
                "nonce": modify_data.get('nonce', int(time.time() * 1000)),
                "expiresAfter": modify_data.get('expiresAfter', 0)
            }
        )
 
    def sign_update_leverage(self, leverage_data):
        return self._sign(
            types={
                "UpdateLeverage": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "symbol", "type": "string"},
                    {"name": "leverage", "type": "string"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            primary_type="UpdateLeverage",
            message={
                "subAccountId": leverage_data['subAccountId'],
                "symbol": leverage_data['symbol'],
                "leverage": leverage_data['leverage'],
                "nonce": leverage_data.get('nonce', int(time.time() * 1000)),
                "expiresAfter": leverage_data.get('expiresAfter', 0)
            }
        )
 
    def sign_create_subaccount(self, create_data):
        return self._sign(
            types={
                "CreateSubaccount": [
                    {"name": "masterSubAccountId", "type": "uint256"},
                    {"name": "name", "type": "string"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            primary_type="CreateSubaccount",
            message={
                "masterSubAccountId": create_data['masterSubAccountId'],
                "name": create_data['name'],
                "nonce": create_data.get('nonce', int(time.time() * 1000)),
                "expiresAfter": create_data.get('expiresAfter', 0)
            }
        )
 
    def sign_update_subaccount_name(self, update_data):
        return self._sign(
            types={
                "UpdateSubAccountName": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "name", "type": "string"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            primary_type="UpdateSubAccountName",
            message={
                "subAccountId": update_data['subAccountId'],
                "name": update_data['name'],
                "nonce": update_data.get('nonce', int(time.time() * 1000)),
                "expiresAfter": update_data.get('expiresAfter', 0)
            }
        )
 
    def sign_add_delegated_signer(self, delegate_data):
        return self._sign(
            types={
                "AddDelegatedSigner": [
                    {"name": "delegateAddress", "type": "address"},
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"},
                    {"name": "expiresAt", "type": "uint256"},
                    {"name": "permissions", "type": "string[]"}
                ]
            },
            primary_type="AddDelegatedSigner",
            message={
                "delegateAddress": delegate_data['delegateAddress'],
                "subAccountId": delegate_data['subAccountId'],
                "nonce": delegate_data.get('nonce', int(time.time() * 1000)),
                "expiresAfter": delegate_data.get('expiresAfter', 0),
                "expiresAt": delegate_data.get('expiresAt', 0),
                "permissions": delegate_data['permissions']
            }
        )
 
    def sign_remove_all_delegated_signers(self, remove_data):
        return self._sign(
            types={
                "RemoveAllDelegatedSigners": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "nonce", "type": "uint256"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            primary_type="RemoveAllDelegatedSigners",
            message={
                "subAccountId": remove_data['subAccountId'],
                "nonce": remove_data.get('nonce', int(time.time() * 1000)),
                "expiresAfter": remove_data.get('expiresAfter', 0)
            }
        )
 
    def sign_subaccount_action(self, action_data):
        return self._sign(
            types={
                "SubAccountAction": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "action", "type": "string"},
                    {"name": "expiresAfter", "type": "uint256"}
                ]
            },
            primary_type="SubAccountAction",
            message={
                "subAccountId": action_data['subAccountId'],
                "action": action_data['action'],
                "expiresAfter": action_data.get('expiresAfter', 0)
            }
        )
 
# Usage Example
def example():
    signer = SynthetixSigner(private_key="0x...")
 
    # WebSocket authentication (timestamp in seconds)
    auth_sig = signer.sign_websocket_auth(sub_account_id=123456789)
    print(f"Auth signature: {auth_sig}")
 
    # Place orders
    order_sig = signer.sign_place_order({
        "subAccountId": 123456789,
        "orders": [{
            "symbol": "BTC-USDT",
            "side": "buy",
            "orderType": "limitGtc",
            "price": "50000",
            "triggerPrice": "",
            "quantity": "0.1",
            "reduceOnly": False,
            "isTriggerMarket": False,
            "clientOrderId": "0x1234567890abcdef1234567890abcdef",
            "closePosition": False
        }],
        "grouping": "na",
        "nonce": int(time.time() * 1000)
    })
    print(f"Order signature: {order_sig}")
 
    # Cancel orders
    cancel_sig = signer.sign_cancel_order({
        "subAccountId": "123456789",
        "orderIds": ["987654321", "987654322"],
        "nonce": int(time.time() * 1000)
    })
    print(f"Cancel signature: {cancel_sig}")
 
    # Cancel all orders for a symbol
    cancel_all_sig = signer.sign_cancel_all_orders({
        "subAccountId": "123456789",
        "symbol": "BTC-USDT",
        "nonce": int(time.time() * 1000)
    })
    print(f"Cancel all signature: {cancel_all_sig}")
 
    # Update leverage
    leverage_sig = signer.sign_update_leverage({
        "subAccountId": "123456789",
        "symbol": "BTC-USDT",
        "leverage": "20",
        "nonce": int(time.time() * 1000)
    })
    print(f"Update leverage signature: {leverage_sig}")
 
    # Create subaccount
    create_sig = signer.sign_create_subaccount({
        "masterSubAccountId": "123456789",
        "name": "New Trading Account",
        "nonce": int(time.time() * 1000)
    })
    print(f"Create subaccount signature: {create_sig}")
 
    # Update subaccount name
    rename_sig = signer.sign_update_subaccount_name({
        "subAccountId": "123456789",
        "name": "Renamed Account",
        "nonce": int(time.time() * 1000)
    })
    print(f"Update subaccount name signature: {rename_sig}")
 
    # Add delegated signer
    add_delegate_sig = signer.sign_add_delegated_signer({
        "delegateAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
        "subAccountId": "123456789",
        "permissions": ["trading"],
        "nonce": int(time.time() * 1000)
    })
    print(f"Add delegated signer signature: {add_delegate_sig}")
 
    # Remove all delegated signers
    remove_all_sig = signer.sign_remove_all_delegated_signers({
        "subAccountId": "123456789",
        "nonce": int(time.time() * 1000)
    })
    print(f"Remove all delegated signers signature: {remove_all_sig}")
 
    # Get positions (no nonce required for SubAccountAction)
    positions_sig = signer.sign_subaccount_action({
        "subAccountId": "123456789",
        "action": "getPositions",
        "expiresAfter": int(time.time() * 1000) + 300000  # Optional: 5 minute expiration (ms)
    })
    print(f"Positions signature: {positions_sig}")

Go Implementation

Using go-ethereum

package main
 
import (
    "crypto/ecdsa"
    "fmt"
    "math/big"
    "time"
 
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/common/math"
    "github.com/ethereum/go-ethereum/crypto"
    "github.com/ethereum/go-ethereum/signer/core/apitypes"
)
 
var domainFields = apitypes.Types{
    "EIP712Domain": {
        {"name", "string"},
        {"version", "string"},
        {"chainId", "uint256"},
        {"verifyingContract", "address"},
    },
}
 
type SynthetixSigner struct {
    privateKey *ecdsa.PrivateKey
    domain     apitypes.TypedDataDomain
}
 
func NewSynthetixSigner(privateKeyHex string, chainID int64) (*SynthetixSigner, error) {
    privateKey, err := crypto.HexToECDSA(privateKeyHex)
    if err != nil {
        return nil, err
    }
 
    domain := apitypes.TypedDataDomain{
        Name:              "Synthetix",
        Version:           "1",
        ChainId:           math.NewHexOrDecimal256(chainID),
        VerifyingContract: "0x0000000000000000000000000000000000000000",
    }
 
    return &SynthetixSigner{
        privateKey: privateKey,
        domain:     domain,
    }, nil
}
 
// SignWebSocketAuth signs authentication for WebSocket connection
func (s *SynthetixSigner) SignWebSocketAuth(subAccountId uint64, timestamp int64) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "AuthMessage": {
            {"subAccountId", "uint256"},
            {"timestamp", "uint256"},
            {"action", "string"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(subAccountId)),
        "timestamp":    math.NewHexOrDecimal256(timestamp),
        "action":       "websocket_auth",
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "AuthMessage",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignPlaceOrder signs a place orders request
func (s *SynthetixSigner) SignPlaceOrder(orderData PlaceOrderData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "Order": {
            {"symbol", "string"},
            {"side", "string"},
            {"orderType", "string"},
            {"price", "string"},
            {"triggerPrice", "string"},
            {"quantity", "string"},
            {"reduceOnly", "bool"},
            {"isTriggerMarket", "bool"},
            {"clientOrderId", "string"},
            {"closePosition", "bool"},
        },
        "PlaceOrders": {
            {"subAccountId", "uint256"},
            {"orders", "Order[]"},
            {"grouping", "string"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(orderData.SubAccountID)),
        "orders":       orderData.Orders,
        "grouping":     orderData.Grouping,
        "nonce":        math.NewHexOrDecimal256(orderData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(orderData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "PlaceOrders",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignCancelOrder signs a cancel orders request
func (s *SynthetixSigner) SignCancelOrder(cancelData CancelData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "CancelOrders": {
            {"subAccountId", "uint256"},
            {"orderIds", "uint256[]"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(cancelData.SubAccountID)),
        "orderIds":     cancelData.OrderIDs,
        "nonce":        math.NewHexOrDecimal256(cancelData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(cancelData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "CancelOrders",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignCancelAllOrders signs a cancel all orders request
func (s *SynthetixSigner) SignCancelAllOrders(cancelAllData CancelAllData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "CancelAllOrders": {
            {"subAccountId", "uint256"},
            {"symbol", "string"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(cancelAllData.SubAccountID)),
        "symbol":       cancelAllData.Symbol,
        "nonce":        math.NewHexOrDecimal256(cancelAllData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(cancelAllData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "CancelAllOrders",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignModifyOrder signs a modify order request
func (s *SynthetixSigner) SignModifyOrder(modifyData ModifyData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "ModifyOrder": {
            {"subAccountId", "uint256"},
            {"orderId", "uint256"},
            {"price", "string"},
            {"quantity", "string"},
            {"triggerPrice", "string"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(modifyData.SubAccountID)),
        "orderId":      math.NewHexOrDecimal256(int64(modifyData.OrderID)),
        "price":        modifyData.Price,
        "quantity":     modifyData.Quantity,
        "triggerPrice": modifyData.TriggerPrice,
        "nonce":        math.NewHexOrDecimal256(modifyData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(modifyData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "ModifyOrder",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignUpdateLeverage signs an update leverage request
func (s *SynthetixSigner) SignUpdateLeverage(leverageData LeverageData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "UpdateLeverage": {
            {"subAccountId", "uint256"},
            {"symbol", "string"},
            {"leverage", "string"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(leverageData.SubAccountID)),
        "symbol":       leverageData.Symbol,
        "leverage":     leverageData.Leverage,
        "nonce":        math.NewHexOrDecimal256(leverageData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(leverageData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "UpdateLeverage",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignCreateSubaccount signs a create subaccount request
func (s *SynthetixSigner) SignCreateSubaccount(createData CreateSubaccountData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "CreateSubaccount": {
            {"masterSubAccountId", "uint256"},
            {"name", "string"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "masterSubAccountId": math.NewHexOrDecimal256(int64(createData.MasterSubAccountID)),
        "name":               createData.Name,
        "nonce":              math.NewHexOrDecimal256(createData.Nonce),
        "expiresAfter":       math.NewHexOrDecimal256(createData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "CreateSubaccount",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignUpdateSubAccountName signs an update subaccount name request
func (s *SynthetixSigner) SignUpdateSubAccountName(updateData UpdateSubAccountNameData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "UpdateSubAccountName": {
            {"subAccountId", "uint256"},
            {"name", "string"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(updateData.SubAccountID)),
        "name":         updateData.Name,
        "nonce":        math.NewHexOrDecimal256(updateData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(updateData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "UpdateSubAccountName",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignAddDelegatedSigner signs an add delegated signer request
func (s *SynthetixSigner) SignAddDelegatedSigner(delegateData AddDelegatedSignerData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "AddDelegatedSigner": {
            {"delegateAddress", "address"},
            {"subAccountId", "uint256"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
            {"expiresAt", "uint256"},
            {"permissions", "string[]"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "delegateAddress": delegateData.DelegateAddress,
        "subAccountId":    math.NewHexOrDecimal256(int64(delegateData.SubAccountID)),
        "nonce":           math.NewHexOrDecimal256(delegateData.Nonce),
        "expiresAfter":    math.NewHexOrDecimal256(delegateData.ExpiresAfter),
        "expiresAt":       math.NewHexOrDecimal256(delegateData.ExpiresAt),
        "permissions":     delegateData.Permissions,
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "AddDelegatedSigner",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// SignRemoveAllDelegatedSigners signs a remove all delegated signers request
func (s *SynthetixSigner) SignRemoveAllDelegatedSigners(removeData RemoveAllDelegatedSignersData) (string, error) {
    types := apitypes.Types{
        "EIP712Domain": domainFields["EIP712Domain"],
        "RemoveAllDelegatedSigners": {
            {"subAccountId", "uint256"},
            {"nonce", "uint256"},
            {"expiresAfter", "uint256"},
        },
    }
 
    message := apitypes.TypedDataMessage{
        "subAccountId": math.NewHexOrDecimal256(int64(removeData.SubAccountID)),
        "nonce":        math.NewHexOrDecimal256(removeData.Nonce),
        "expiresAfter": math.NewHexOrDecimal256(removeData.ExpiresAfter),
    }
 
    typedData := apitypes.TypedData{
        Types:       types,
        PrimaryType: "RemoveAllDelegatedSigners",
        Domain:      s.domain,
        Message:     message,
    }
 
    return s.signTypedData(typedData)
}
 
// validateSignature validates EIP-712 signature components
func validateSignature(sig []byte) error {
    if len(sig) != 65 {
        return fmt.Errorf("invalid signature length: expected 65, got %d", len(sig))
    }
 
    // Validate v (recovery ID) - must be 0, 1, 27, or 28
    v := sig[64]
    if v != 0 && v != 1 && v != 27 && v != 28 {
        return fmt.Errorf("invalid v value: %d. Must be 0, 1, 27, or 28", v)
    }
 
    // r and s are validated by length (32 bytes each)
    r := new(big.Int).SetBytes(sig[:32])
    s := new(big.Int).SetBytes(sig[32:64])
 
    if r.Sign() == 0 {
        return fmt.Errorf("invalid r value: must be non-zero")
    }
    if s.Sign() == 0 {
        return fmt.Errorf("invalid s value: must be non-zero")
    }
 
    return nil
}
 
// signTypedData is a helper function to sign typed data and return hex signature
func (s *SynthetixSigner) signTypedData(typedData apitypes.TypedData) (string, error) {
    domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
    if err != nil {
        return "", fmt.Errorf("failed to hash domain: %w", err)
    }
 
    typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
    if err != nil {
        return "", fmt.Errorf("failed to hash message: %w", err)
    }
 
    // Calculate the digest
    rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
    digest := crypto.Keccak256Hash(rawData)
 
    // Sign the digest
    signature, err := crypto.Sign(digest.Bytes(), s.privateKey)
    if err != nil {
        return "", fmt.Errorf("failed to sign: %w", err)
    }
 
    // Adjust recovery ID for Ethereum
    if signature[64] < 27 {
        signature[64] += 27
    }
 
    // Validate signature before returning
    if err := validateSignature(signature); err != nil {
        return "", fmt.Errorf("invalid signature: %w", err)
    }
 
    return "0x" + common.Bytes2Hex(signature), nil
}
 
// Data structures
 
type OrderItem struct {
    Symbol          string `json:"symbol"`
    Side            string `json:"side"`
    OrderType       string `json:"orderType"`
    Price           string `json:"price"`
    TriggerPrice    string `json:"triggerPrice"`
    Quantity        string `json:"quantity"`
    ReduceOnly      bool   `json:"reduceOnly"`
    IsTriggerMarket bool   `json:"isTriggerMarket"`
    ClientOrderId   string `json:"clientOrderId"`
    ClosePosition   bool   `json:"closePosition"`
}
 
type PlaceOrderData struct {
    SubAccountID uint64        `json:"subAccountId"`
    Orders       []interface{} `json:"orders"` // Typed data message format
    Grouping     string        `json:"grouping"`
    Nonce        int64         `json:"nonce"`
    ExpiresAfter int64         `json:"expiresAfter"`
}
 
type CancelData struct {
    SubAccountID uint64        `json:"subAccountId"`
    OrderIDs     []interface{} `json:"orderIds"` // uint256[] as typed data
    Nonce        int64         `json:"nonce"`
    ExpiresAfter int64         `json:"expiresAfter"`
}
 
type CancelAllData struct {
    SubAccountID uint64 `json:"subAccountId"`
    Symbol       string `json:"symbol"`
    Nonce        int64  `json:"nonce"`
    ExpiresAfter int64  `json:"expiresAfter"`
}
 
type ModifyData struct {
    SubAccountID uint64 `json:"subAccountId"`
    OrderID      uint64 `json:"orderId"`
    Price        string `json:"price"`
    Quantity     string `json:"quantity"`
    TriggerPrice string `json:"triggerPrice"`
    Nonce        int64  `json:"nonce"`
    ExpiresAfter int64  `json:"expiresAfter"`
}
 
type LeverageData struct {
    SubAccountID uint64 `json:"subAccountId"`
    Symbol       string `json:"symbol"`
    Leverage     string `json:"leverage"`
    Nonce        int64  `json:"nonce"`
    ExpiresAfter int64  `json:"expiresAfter"`
}
 
type CreateSubaccountData struct {
    MasterSubAccountID uint64 `json:"masterSubAccountId"`
    Name               string `json:"name"`
    Nonce              int64  `json:"nonce"`
    ExpiresAfter       int64  `json:"expiresAfter"`
}
 
type UpdateSubAccountNameData struct {
    SubAccountID uint64 `json:"subAccountId"`
    Name         string `json:"name"`
    Nonce        int64  `json:"nonce"`
    ExpiresAfter int64  `json:"expiresAfter"`
}
 
type AddDelegatedSignerData struct {
    DelegateAddress string   `json:"delegateAddress"`
    SubAccountID    uint64   `json:"subAccountId"`
    Nonce           int64    `json:"nonce"`
    ExpiresAfter    int64    `json:"expiresAfter"`
    ExpiresAt       int64    `json:"expiresAt"`
    Permissions     []string `json:"permissions"`
}
 
type RemoveAllDelegatedSignersData struct {
    SubAccountID uint64 `json:"subAccountId"`
    Nonce        int64  `json:"nonce"`
    ExpiresAfter int64  `json:"expiresAfter"`
}
 
// Usage example
func main() {
    signer, _ := NewSynthetixSigner("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1)
 
    // WebSocket authentication (timestamp in seconds)
    authSig, _ := signer.SignWebSocketAuth(123456789, time.Now().Unix())
    fmt.Printf("Auth signature: %s\n", authSig)
 
    // Place order
    orderData := PlaceOrderData{
        SubAccountID: 123456789,
        Orders: []interface{}{
            map[string]interface{}{
                "symbol":          "BTC-USDT",
                "side":            "buy",
                "orderType":       "limitGtc",
                "price":           "50000",
                "triggerPrice":    "",
                "quantity":        "0.1",
                "reduceOnly":      false,
                "isTriggerMarket": false,
                "clientOrderId":   "0x1234567890abcdef1234567890abcdef",
                "closePosition":   false,
            },
        },
        Grouping: "na",
        Nonce:    time.Now().UnixMilli(),
    }
 
    orderSig, _ := signer.SignPlaceOrder(orderData)
    fmt.Printf("Order signature: %s\n", orderSig)
 
    // Cancel all orders for a symbol
    cancelAllData := CancelAllData{
        SubAccountID: 123456789,
        Symbol:       "BTC-USDT",
        Nonce:        time.Now().UnixMilli(),
    }
 
    cancelAllSig, _ := signer.SignCancelAllOrders(cancelAllData)
    fmt.Printf("Cancel all signature: %s\n", cancelAllSig)
 
    // Update leverage
    leverageData := LeverageData{
        SubAccountID: 123456789,
        Symbol:       "BTC-USDT",
        Leverage:     "20",
        Nonce:        time.Now().UnixMilli(),
    }
 
    leverageSig, _ := signer.SignUpdateLeverage(leverageData)
    fmt.Printf("Leverage signature: %s\n", leverageSig)
 
    // Create subaccount
    createData := CreateSubaccountData{
        MasterSubAccountID: 123456789, // An existing subaccount you own
        Name:               "New Trading Account",
        Nonce:              time.Now().UnixMilli(),
    }
 
    createSig, _ := signer.SignCreateSubaccount(createData)
    fmt.Printf("Create subaccount signature: %s\n", createSig)
 
    // Update subaccount name
    updateNameData := UpdateSubAccountNameData{
        SubAccountID: 123456789,
        Name:         "Renamed Account",
        Nonce:        time.Now().UnixMilli(),
    }
 
    updateNameSig, _ := signer.SignUpdateSubAccountName(updateNameData)
    fmt.Printf("Update subaccount name signature: %s\n", updateNameSig)
 
    // Add delegated signer
    addDelegateData := AddDelegatedSignerData{
        DelegateAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
        SubAccountID:    123456789,
        Permissions:     []string{"trading"},
        Nonce:           time.Now().UnixMilli(),
    }
 
    addDelegateSig, _ := signer.SignAddDelegatedSigner(addDelegateData)
    fmt.Printf("Add delegated signer signature: %s\n", addDelegateSig)
 
    // Remove all delegated signers
    removeAllData := RemoveAllDelegatedSignersData{
        SubAccountID: 123456789,
        Nonce:        time.Now().UnixMilli(),
    }
 
    removeAllSig, _ := signer.SignRemoveAllDelegatedSigners(removeAllData)
    fmt.Printf("Remove all delegated signers signature: %s\n", removeAllSig)
}

Signature Validation

Local Validation (JavaScript)

function validateSignature(signature) {
  // Check signature structure
  if (!signature || typeof signature !== 'object') {
    throw new Error('Signature must be an object');
  }
 
  const { v, r, s } = signature;
 
  // Validate v component (can be 0, 1, 27, or 28)
  if (typeof v !== 'number' || ![0, 1, 27, 28].includes(v)) {
    throw new Error('Invalid v component: must be 0, 1, 27, or 28');
  }
 
  // Validate r component
  if (typeof r !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(r)) {
    throw new Error('Invalid r component: must be 32-byte hex string');
  }
 
  // Validate s component
  if (typeof s !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(s)) {
    throw new Error('Invalid s component: must be 32-byte hex string');
  }
 
  return true;
}

Common Issues and Solutions

Issue: "Invalid signature" Error

Possible Causes:
  1. Wrong domain separator (must use zero address as verifying contract)
  2. Missing subAccountId field
  3. Incorrect message structure
  4. Wrong domain name (must be "Synthetix")
  5. Missing or wrong verifyingContract (must be zero address)
Solution:
// Verify domain matches exactly
const domain = {
  name: "Synthetix",
  version: "1",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000000"
};
 
// orderType is a plain string — no JSON.stringify needed
const order = {
  symbol: "BTC-USDT",
  side: "buy",
  orderType: "limitGtc", // Plain string value
  price: "50000",
  // ... other fields
};
 
// Include subAccountId in all trading messages
const message = {
  subAccountId: "123456789", // Required field
  // ... other fields
};

Issue: "Missing subAccountId" Error

Cause: New required field not included

Solution:
// SubAccountId required for all trading operations
const orderMessage = {
  subAccountId: BigInt(userSubAccountId), // Required
  symbol: "BTC-USDT",
  side: "buy",
  quantity: "0.1",
  price: "50000",
  orderType: "limitGtc",
  triggerPrice: "",
  isTriggerMarket: false,
  reduceOnly: false,
  nonce: BigInt(Date.now()),
  expiresAfter: BigInt(0)
};

Issue: "Nonce already used" Error

Cause: Replay protection triggered — the nonce value was already used for a previous request.

Solution:
// Each nonce must be a positive integer, incrementing and unique per request
// Option 1: Use crypto random (recommended - guaranteed unique)
const nonce = BigInt('0x' + crypto.randomBytes(8).toString('hex'));
 
// Option 2: Use timestamp (simple, usually unique)
const nonce = Date.now();
 
// For WebSocket auth timestamp field (this IS a timestamp, in seconds)
const timestamp = Math.floor(Date.now() / 1000);

Issue: WebSocket Authentication Failed

Cause: Wrong domain name or message structure

Solution:
// Use correct domain for WebSocket authentication
const wsDomain = {
  name: "Synthetix",
  version: "1",
  chainId: 1,
  verifyingContract: "0x0000000000000000000000000000000000000000"
};
 
const authMessage = {
  subAccountId: BigInt(subAccountId),
  timestamp: BigInt(Math.floor(Date.now() / 1000)), // Unix timestamp in seconds
  action: "websocket_auth" // Exact string required
};

Testing Your Implementation

// Test all signature types
async function testImplementation() {
  const signer = new SynthetixSigner("0x..." as `0x${string}`);
 
  try {
    // Test WebSocket auth
    const authSig = await signer.signWebSocketAuth("123456789");
    console.log('WebSocket auth signature valid');
 
    // Test order placement
    const orderSig = await signer.signPlaceOrder({
      subAccountId: "123456789",
      orders: [{
        symbol: "BTC-USDT",
        side: "buy",
        orderType: "limitGtc",
        price: "99999",
        triggerPrice: "",
        quantity: "0.001",
        reduceOnly: false,
        isTriggerMarket: false,
        clientOrderId: "0x1234567890abcdef1234567890abcdef",
        closePosition: false
      }],
      grouping: "na",
      nonce: Date.now()
    });
    console.log('Order signature valid');
 
    // Test order cancellation
    const cancelSig = await signer.signCancelOrder({
      subAccountId: "123456789",
      orderIds: ["999999999"],
      nonce: Date.now()
    });
    console.log('Cancel signature valid');
 
    // Test order modification
    const modifySig = await signer.signModifyOrder({
      subAccountId: "123456789",
      orderId: "999999999",
      price: "99998",
      quantity: "0.002",
      nonce: Date.now()
    });
    console.log('Modify signature valid');
 
  } catch (error) {
    console.error('Signature test failed:', error.message);
  }
}

Key Differences from Standard EIP-712

  1. Zero Address Contract: Off-chain authentication uses the zero address (0x0000000000000000000000000000000000000000) as the verifying contract
  2. SubAccount Required: All trading operations require a subAccountId field
  3. Simplified Modify: Modify orders only allow changing price, quantity, and/or triggerPrice (all optional)
  4. String Types: Financial values (price, quantity) are strings for precision
  5. Domain Name: Unified domain name "Synthetix" for all authentication
  6. Consistent TIF Format: Time in force values use PascalCase in both EIP-712 signing and API responses ("Gtc", "Alo", "Ioc")

Important Note on Field Naming

EIP-712 signing and API responses now use consistent format:
  • Both EIP-712 signing and API responses: timeInForce: "Gtc" (PascalCase)

Consistent format required across all API operations and signing workflows.

Next Steps