Skip to content

WebSocket Authentication

The Trade WebSocket requires authentication using EIP-712 signatures before any trading operations can be performed.

This document covers WebSocket-specific implementation details.

Authentication Flow

  1. Connect to the WebSocket endpoint
  2. Send authentication message within 30 seconds
  3. Receive authentication confirmation
  4. Begin trading operations

Authentication Message

Send this message immediately after connection:

{
  "id": "auth-1",
  "method": "auth",
  "params": {
    "message": "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"AuthMessage\":[{\"name\":\"subAccountId\",\"type\":\"uint256\"},{\"name\":\"timestamp\",\"type\":\"uint256\"},{\"name\":\"action\",\"type\":\"string\"}]},\"primaryType\":\"AuthMessage\",\"domain\":{\"name\":\"Synthetix\",\"version\":\"1\",\"chainId\":1,\"verifyingContract\":\"0x0000000000000000000000000000000000000000\"},\"message\":{\"subAccountId\":\"0x19f2e3c8b5a7d1f0\",\"timestamp\":\"0x187a3e4f2b1c\",\"action\":\"websocket_auth\"}}",
    "signature": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
  }
}

Parameters

ParameterTypeRequiredDescription
idstringYesClient-generated unique request identifier
methodstringYesMust be "auth"
params.messagestringYesJSON-stringified EIP-712 structured data (types, domain, primaryType, message)
params.signaturestringYesRaw EIP-712 signature as hex string (65 bytes: 0x + 130 hex chars)

EIP-712 Signature

The authentication signature uses this structure:

Domain

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

Types

const types = {
  AuthMessage: [
    { name: "subAccountId", type: "uint256" },
    { name: "timestamp", type: "uint256" },
    { name: "action", type: "string" }
  ]
};

Message

const message = {
  subAccountId: "1867542890123456789",
  timestamp: Math.floor(Date.now() / 1000), // Unix timestamp in SECONDS
  action: "websocket_auth"
};

Important: The timestamp field must be a Unix timestamp in seconds (not milliseconds). The server validates that timestamps are within ±60 seconds of server time to prevent replay attacks.

Implementation Examples

JavaScript/TypeScript

import { ethers } from 'ethers';
 
class WebSocketAuthenticator {
  constructor(privateKey) {
    this.wallet = new ethers.Wallet(privateKey);
    this.address = this.wallet.address;
  }
 
  async createAuthSignature(subAccountId, timestamp) {
    const timestampSec = Math.floor((timestamp || Date.now()) / 1000);
 
    const domain = {
      name: "Synthetix",
      version: "1",
      chainId: 1,
      verifyingContract: "0x0000000000000000000000000000000000000000"
    };
 
    const types = {
      EIP712Domain: [
        { name: "name", type: "string" },
        { name: "version", type: "string" },
        { name: "chainId", type: "uint256" },
        { name: "verifyingContract", type: "address" }
      ],
      AuthMessage: [
        { name: "subAccountId", type: "uint256" },
        { name: "timestamp", type: "uint256" },
        { name: "action", type: "string" }
      ]
    };
 
    const message = {
      subAccountId: BigInt(subAccountId),
      timestamp: BigInt(timestampSec),
      action: "websocket_auth"
    };
 
    // Sign the typed data
    const signature = await this.wallet._signTypedData(domain, types, message);
 
    // Create payload (EIP-712 structured data for the message parameter)
    const payload = {
      types,
      primaryType: "AuthMessage",
      domain,
      message: {
        subAccountId: `0x${BigInt(subAccountId).toString(16)}`,
        timestamp: `0x${BigInt(timestampSec).toString(16)}`,
        action: "websocket_auth"
      }
    };
 
    return {
      message: JSON.stringify(payload),
      signature: signature  // Raw hex string
    };
  }
 
  async connectAndAuth(wsUrl, subAccountId) {
    return new Promise((resolve, reject) => {
      const ws = new WebSocket(wsUrl);
 
      ws.onopen = async () => {
        try {
          // Create auth signature and payload
          const { message, signature } = await this.createAuthSignature(subAccountId);
 
          // Send auth message
          const authMessage = {
            id: "auth-1",
            method: "auth",
            params: {
              message,      // JSON-stringified EIP-712 payload
              signature     // Raw hex signature string
            }
          };
 
          ws.send(JSON.stringify(authMessage));
        } catch (error) {
          reject(error);
          ws.close();
        }
      };
 
      ws.onmessage = (event) => {
        const message = JSON.parse(event.data);
 
        if (message.id === "auth-1") {
          if (message.error) {
            reject(new Error(message.error.message));
            ws.close();
          } else if (message.result?.status === "authenticated") {
            console.log('Authenticated successfully');
            resolve(ws);
          }
        }
      };
 
      ws.onerror = (error) => {
        reject(error);
      };
 
      // Timeout after 30 seconds
      setTimeout(() => {
        reject(new Error('Authentication timeout'));
        ws.close();
      }, 30000);
    });
  }
}
 
// Usage
async function connectToTrading() {
  const authenticator = new WebSocketAuthenticator(process.env.PRIVATE_KEY);
  const subAccountId = "1867542890123456789"; // Your subaccount ID
 
  try {
    const ws = await authenticator.connectAndAuth('wss://papi.synthetix.io/v1/ws/trade', subAccountId);
    console.log('Connected and authenticated');
 
    // Now you can use the authenticated WebSocket
    return ws;
  } catch (error) {
    console.error('Authentication failed:', error);
  }
}

Python

import json
import time
import asyncio
import os
import websockets
from eth_account import Account
from eth_account.messages import encode_structured_data
 
class WebSocketAuthenticator:
    def __init__(self, private_key):
        self.account = Account.from_key(private_key)
        self.address = self.account.address
 
    def create_auth_signature(self, sub_account_id, timestamp=None):
        if timestamp is None:
            timestamp = int(time.time())  # Unix timestamp in SECONDS
 
        # EIP-712 structured data
        data = {
            "types": {
                "EIP712Domain": [
                    {"name": "name", "type": "string"},
                    {"name": "version", "type": "string"},
                    {"name": "chainId", "type": "uint256"},
                    {"name": "verifyingContract", "type": "address"}
                ],
                "AuthMessage": [
                    {"name": "subAccountId", "type": "uint256"},
                    {"name": "timestamp", "type": "uint256"},
                    {"name": "action", "type": "string"}
                ]
            },
            "primaryType": "AuthMessage",
            "domain": {
                "name": "Synthetix",
                "version": "1",
                "chainId": 1,
                "verifyingContract": "0x0000000000000000000000000000000000000000"
            },
            "message": {
                "subAccountId": int(sub_account_id),
                "timestamp": timestamp,
                "action": "websocket_auth"
            }
        }
 
        # Encode and sign
        encoded = encode_structured_data(data)
        signed = self.account.sign_message(encoded)
 
        # Return params in correct format for WebSocket auth
        return {
            "message": json.dumps(data),  # JSON-stringified EIP-712 typed data
            "signature": signed.signature.hex()  # Raw hex signature string
        }
 
    async def connect_and_auth(self, ws_url, sub_account_id):
        async with websockets.connect(ws_url) as websocket:
            # Create auth signature and payload
            auth_params = self.create_auth_signature(sub_account_id)
 
            # Send auth message
            auth_message = {
                "id": "auth-1",
                "method": "auth",
                "params": auth_params
            }
 
            await websocket.send(json.dumps(auth_message))
 
            # Wait for response
            response = await websocket.recv()
            message = json.loads(response)
 
            if message.get("error"):
                raise Exception(f"Auth failed: {message['error']['message']}")
            elif message.get("result", {}).get("status") == "authenticated":
                print("Authenticated successfully")
                return websocket
 
            raise Exception("Unexpected auth response")
 
# Usage
async def main():
    authenticator = WebSocketAuthenticator(os.environ["PRIVATE_KEY"])
    sub_account_id = "1867542890123456789"  # Your subaccount ID
 
    try:
        ws = await authenticator.connect_and_auth("wss://papi.synthetix.io/v1/ws/trade", sub_account_id)
        # Use authenticated websocket
 
    except Exception as e:
        print(f"Authentication failed: {e}")
 
asyncio.run(main())

Authentication Response

Success Response

{
  "id": "auth-1",
  "status": 200,
  "result": {
    "status": "authenticated",
    "sub_account_id": "12345"
  },
  "error": null
}

Error Response

{
  "id": "auth-1",
  "status": 401,
  "result": null,
  "error": {
    "code": 401,
    "message": "Authentication failed: Invalid signature"
  }
}

Subaccount Trading

After authentication, use subAccountId in trading operations to specify which account to trade on:

const orderMessage = {
  id: "order-1",
  method: "post",
  params: {
    action: "placeOrders",
    subAccountId: "1867542890123456789",  // Which subaccount to trade on
      orders: [{
      symbol: "BTC-USDT",
      side: "buy",
      orderType: "limitGtc",
      price: "50000",
      triggerPrice: "",
      quantity: "0.1",
      reduceOnly: false,
      isTriggerMarket: false
    }]
  }
};

Requirements:

  • Authentication signature determines who is trading
  • System verifies on-chain delegation permissions automatically
  • subAccountId specifies which account to operate on

Session Management

Session Lifetime

  • Sessions expire after 24 hours
  • Re-authentication required after expiration
  • Connection drops do not invalidate session immediately

Multiple Connections

  • Each connection requires separate authentication
  • Maximum 5 concurrent authenticated connections per address
  • Sessions are independent

Implementation Notes

Timestamp Management

Authentication timestamps must be in seconds and within ±60 seconds of server time:

class TimestampManager {
  constructor() {
    this.lastTimestamp = 0;
  }
 
  generateTimestamp() {
    const timestamp = Math.floor(Date.now() / 1000); // Unix timestamp in SECONDS
    // Ensure always increasing
    this.lastTimestamp = Math.max(timestamp, this.lastTimestamp + 1);
    return this.lastTimestamp;
  }
}

Note: The server enforces a ±60 second window for timestamps to prevent replay attacks.

Authentication Timeout

WebSocket authentication must complete within 30 seconds:

async function authenticateWithTimeout(ws, authMessage, timeoutMs = 30000) {
  return Promise.race([
    sendAuthMessage(ws, authMessage),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Auth timeout')), timeoutMs)
    )
  ]);
}

Common Issues

Invalid Signature

Causes:
  • Wrong domain parameters (must use name: "Synthetix", version: "1", chainId: 1, verifyingContract: "0x0000000000000000000000000000000000000000")
  • Incorrect message structure (missing subAccountId, timestamp, or action)
  • Timestamp not in seconds or outside ±60 second window
  • Key mismatch
Solution:
// Debug signature
console.log('Domain:', domain);
console.log('Types:', types);
console.log('Message:', message);
console.log('Signer address:', wallet.address);

Authentication Timeout

Cause: Message not sent within 30 seconds of connection

Solution:
ws.onopen = async () => {
  // Send auth immediately
  const authMessage = createAuthMessage();
  ws.send(JSON.stringify(authMessage));
};

Subaccount Access Errors

Cause: No permission to trade on specified subaccount

Solution:
  1. Verify on-chain delegation permissions
  2. Check that signer has authority for the target subaccount
  3. Ensure subaccount ID is correct

Testing Authentication

// Test authentication separately
async function testAuth() {
  const testAuth = new WebSocketAuthenticator(TEST_PRIVATE_KEY);
  const testSubAccountId = "1867542890123456789";
 
  try {
    // Test signature generation
    const signature = await testAuth.createAuthSignature(testSubAccountId);
    console.log('Signature generated:', signature);
 
    // Test connection
    const ws = await testAuth.connectAndAuth('wss://api.test.synthetix.io/v1/ws/trade', testSubAccountId);
    console.log('Test auth successful');
 
    ws.close();
  } catch (error) {
    console.error('Test auth failed:', error);
  }
}

Next Steps