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": {
"subAccountId": "1867542890123456789",
"timestamp": 1704067200000,
"action": "websocketAuth",
"signature": {
"v": 28,
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
}
}
}Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Client-generated unique request identifier |
method | string | Yes | Must be "auth" |
params.subAccountId | string | Yes | Subaccount identifier |
params.timestamp | integer (uint64) | Yes | Current Unix timestamp in milliseconds |
params.action | string | Yes | Must be "websocketAuth" |
params.signature | object | Yes | EIP-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
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 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
// 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