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": {
    "subAccountId": "1867542890123456789",
 
    "timestamp": 1704067200000,
 
    "action": "websocketAuth",
    "signature": {
      "v": 28,
      "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
      "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
    }
  }
}

Parameters

ParameterTypeRequiredDescription
idstringYesClient-generated unique request identifier
methodstringYesMust be "auth"
params.subAccountIdstringYesSubaccount identifier
params.timestampinteger (uint64)YesCurrent Unix timestamp in milliseconds
params.actionstringYesMust be "websocketAuth"
params.signatureobjectYesEIP-712 signature components

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: Date.now(), // Unix timestamp in milliseconds
  action: "websocketAuth"
};

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 domain = {
      name: "Synthetix",
      version: "1",
      chainId: 1,
      verifyingContract: "0x0000000000000000000000000000000000000000"
    };
 
    const types = {
      AuthMessage: [
        { name: "subAccountId", type: "uint256" },
        { name: "timestamp", type: "uint256" },
        { name: "action", type: "string" }
      ]
    };
 
    const message = {
      subAccountId: subAccountId,
      timestamp: timestamp || Date.now(), // Unix timestamp in milliseconds
      action: "websocketAuth"
    };
 
    // Sign the typed data
    const signature = await this.wallet._signTypedData(domain, types, message);
 
    // Split signature
    const sig = ethers.utils.splitSignature(signature);
 
    return {
      subAccountId: message.subAccountId,
      timestamp: message.timestamp,
      action: message.action,
      signature: {
        v: sig.v,
        r: sig.r,
        s: sig.s
      }
    };
  }
 
  async connectAndAuth(wsUrl, subAccountId) {
    return new Promise((resolve, reject) => {
      const ws = new WebSocket(wsUrl);
 
      ws.onopen = async () => {
        try {
          // Create auth signature
          const authParams = await this.createAuthSignature(subAccountId);
 
          // Send auth message
          const authMessage = {
            id: "auth-1",
            method: "auth",
            params: authParams
          };
 
          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?.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 websockets
from eth_account import Account
from eth_account.messages import encode_structured_data
 
class WebSocketAuthenticator:
    def __init__(self, privateKey):
        self.account = Account.from_key(privateKey)
        self.address = self.account.address
 
    def create_auth_signature(self, sub_account_id, timestamp=None):
        if timestamp is None:
            timestamp = int(time.time() * 1000)  # Unix timestamp in milliseconds
 
        # EIP-712 structured data
        data = {
            "types": {
                "EIP712Domain": [
                    {"name": "name", "type": "string"},
                    {"name": "version", "type": "string"},
                    {"name": "chainId", "type": "uint256"}
                ],
                "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": sub_account_id,
                "timestamp": timestamp,
                "action": "websocketAuth"
            }
        }
 
        # Encode and sign
        encoded = encode_structured_data(data)
        signed = self.account.sign_message(encoded)
 
        return {
            "subAccountId": sub_account_id,
            "timestamp": timestamp,
            "action": "websocketAuth",
            "signature": {
                "v": signed.v,
                "r": hex(signed.r),
                "s": hex(signed.s)
            }
        }
 
    async def connect_and_auth(self, ws_url, sub_account_id):
        async with websockets.connect(ws_url) as websocket:
            # Create auth signature
            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("type") == "response":
                if message.get("error"):
                    raise Exception(f"Auth failed: {message['error']['message']}")
                elif message.get("result", {}).get("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",
    "subAccountId": "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 monotonically increasing:

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

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 (should use "Synthetix" and no verifyingContract)
  • Incorrect message structure (missing subAccountId, timestamp, or action)
  • Key mismatch
  • Timestamp already used or not increasing
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