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
- Connect to the WebSocket endpoint
- Send authentication message within 30 seconds
- Receive authentication confirmation
- 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Client-generated unique request identifier |
method | string | Yes | Must be "auth" |
params.message | string | Yes | JSON-stringified EIP-712 structured data (types, domain, primaryType, message) |
params.signature | string | Yes | Raw 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
subAccountIdspecifies 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
// 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:- Verify on-chain delegation permissions
- Check that signer has authority for the target subaccount
- 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
- Place Order - Start trading after auth
- Connection Management - Keep connection alive
- Authentication Guide - Complete authentication documentation
- EIP-712 Signing - Detailed signing implementation
- Nonce Management - Advanced nonce strategies