Schedule Cancel (WebSocket)
Schedule automatic order cancellation through the WebSocket connection as a safety mechanism (Dead Man's Switch) for WebSocket-based trading systems.
Endpoint
ws.send() wss://api.synthetix.io/v1/ws/tradeRequest
Request Format - Schedule Cancellation
{
"id": "schedule-1",
"method": "post",
"params": {
"action": "scheduleCancel",
"timeoutSeconds": 300,
"nonce": 1704067200000,
"expiresAfter": 1704067300,
"signature": {
"v": 28,
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
}
}
}Request Format - Cancel Schedule
{
"id": "cancel-schedule-1",
"method": "post",
"params": {
"action": "scheduleCancel",
"timeoutSeconds": 0,
"nonce": 1704067200000,
"signature": {
"v": 28,
"r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
}
}
}Parameters
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Client-generated unique request identifier |
method | string | Yes | Must be "post" |
params | object | Yes | Flat map containing all request fields |
params.action | string | Yes | Must be "scheduleCancel" |
params.timeoutSeconds | integer | Yes | Timeout in seconds; use 0 to cancel an existing schedule |
params.nonce | integer | Yes | Incrementing nonce (Unix ms timestamp as number) |
params.expiresAfter | integer | No | Optional expiration timestamp in seconds |
params.signature | object | Yes | EIP-712 signature |
Time Requirements
- Minimum timeout: 5 seconds (
timeoutSeconds >= 5) - Maximum timeout: 86400 seconds (24 hours)
- Cancel schedule: set
timeoutSeconds: 0
Response Format
Success Response - Scheduled
{
"id": "schedule-1",
"requestId": "schedule-1",
"status": 200,
"timestamp": 1704067200123,
"result": {
"status": "success",
"response": {
"isActive": true,
"message": "dead-man-switch armed",
"timeoutSeconds": 300,
"triggerTime": 1704067500000
}
}
}Success Response - Cancelled
{
"id": "cancel-schedule-1",
"requestId": "cancel-schedule-1",
"status": 200,
"timestamp": 1704067200456,
"result": {
"status": "success",
"response": {
"isActive": false,
"message": "dead-man-switch disabled",
"timeoutSeconds": 0
}
}
}Error Response
{
"id": "schedule-1",
"requestId": "schedule-1",
"status": 400,
"timestamp": 1704067200000,
"error": {
"code": 400,
"errorCode": "VALIDATION_ERROR",
"category": "REQUEST",
"message": "timeoutSeconds must be less than or equal to 86400",
"retryable": false
}
}Implementation Example
class DeadManSwitch {
constructor(ws, signer, subAccountId) {
this.ws = ws;
this.signer = signer;
this.subAccountId = subAccountId;
this.requestId = 0;
this.activeSchedule = null;
this.heartbeatInterval = null;
}
async scheduleCancel(delayMs) {
const timeoutSeconds = Math.floor(delayMs / 1000);
// Generate nonce
const nonce = Date.now();
// Create schedule signature
const signature = await this.signScheduleCancel(this.subAccountId, timeoutSeconds, nonce);
// Build request — all fields go inside flat params map
const request = {
id: `schedule-cancel-${++this.requestId}`,
method: "post",
params: {
action: "scheduleCancel",
timeoutSeconds,
nonce,
expiresAfter: Math.floor(nonce / 1000) + 30, // 30 seconds from now (seconds)
signature
}
};
// Send and wait for response
const result = await this.sendRequest(request);
this.activeSchedule = { timeoutSeconds };
return result;
}
async cancelSchedule() {
const nonce = Date.now();
const signature = await this.signScheduleCancel(this.subAccountId, 0, nonce);
const request = {
id: `cancel-schedule-${++this.requestId}`,
method: "post",
params: {
action: "scheduleCancel",
timeoutSeconds: 0,
nonce,
expiresAfter: Math.floor(nonce / 1000) + 30, // 30 seconds from now (seconds)
signature
}
};
const result = await this.sendRequest(request);
this.activeSchedule = null;
return result;
}
async signScheduleCancel(subAccountId, timeoutSeconds, nonce) {
const domain = {
name: "Synthetix",
version: "1",
chainId: environment === 'mainnet' ? 1 : 11155111, // Ethereum Mainnet or Sepolia
verifyingContract: "0x0000000000000000000000000000000000000000"
};
const types = {
ScheduleCancel: [
{ name: "subAccountId", type: "uint256" },
{ name: "timeoutSeconds", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" }
]
};
const message = {
subAccountId: BigInt(subAccountId),
timeoutSeconds: BigInt(timeoutSeconds),
nonce: BigInt(nonce),
expiresAfter: BigInt(Math.floor(nonce / 1000) + 30) // 30 seconds from now (seconds)
};
const signature = await this.signer._signTypedData(domain, types, message);
return ethers.utils.splitSignature(signature);
}
sendRequest(request) {
return new Promise((resolve, reject) => {
this.pendingRequests.set(request.id, { resolve, reject });
this.ws.send(JSON.stringify(request));
setTimeout(() => {
if (this.pendingRequests.has(request.id)) {
this.pendingRequests.delete(request.id);
reject(new Error('Request timeout'));
}
}, 10000); // 10 second timeout
});
}
// Automatic heartbeat system
startHeartbeat(intervalMs = 30000, emergencyDelayMs = 60000) {
this.heartbeatInterval = setInterval(async () => {
try {
// Extend the dead man's switch
await this.scheduleCancel(emergencyDelayMs);
console.log(`Heartbeat: Extended dead man's switch by ${emergencyDelayMs}ms`);
} catch (error) {
console.error('Heartbeat failed - manual intervention required:', error);
// Could trigger local emergency procedures here
}
}, intervalMs);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
async emergencyShutdown() {
console.log('🚨 EMERGENCY SHUTDOWN INITIATED');
try {
// Cancel any existing schedule
if (this.activeSchedule) {
await this.cancelSchedule();
}
// Schedule immediate cancellation (minimum 5 seconds)
await this.scheduleCancel(5000);
console.log('✅ Emergency cancellation scheduled in 5 seconds');
} catch (error) {
console.error('🔴 Emergency shutdown failed:', error);
throw error;
}
}
}
// Usage Examples
async function setupTradingSession(deadManSwitch) {
try {
// Set up dead man's switch for 5 minutes
await deadManSwitch.scheduleCancel(5 * 60 * 1000);
console.log('Dead man\'s switch activated - orders will cancel in 5 minutes');
// Start automatic heartbeat every 2 minutes
deadManSwitch.startHeartbeat(2 * 60 * 1000, 5 * 60 * 1000);
console.log('Heartbeat started - automatic extension every 2 minutes');
} catch (error) {
console.error('Failed to setup trading session safety:', error);
throw error;
}
}
async function cleanupTradingSession(deadManSwitch) {
try {
// Stop heartbeat
deadManSwitch.stopHeartbeat();
// Cancel scheduled cancellation
await deadManSwitch.cancelSchedule();
console.log('Trading session safety cleanup complete');
} catch (error) {
console.error('Failed to cleanup trading session:', error);
// Don't throw - safety is more important than cleanup errors
}
}Error Handling
Common Errors
errorCode | Example message | Description |
|---|---|---|
VALIDATION_ERROR | timeoutSeconds must be 0 or at least 5 | timeoutSeconds below minimum |
VALIDATION_ERROR | timeoutSeconds must be less than or equal to 86400 | timeoutSeconds exceeds maximum |
UNAUTHORIZED | nonce is required and must be non-zero | Missing or zero nonce |
UNAUTHORIZED | expiresAfter must be a future unix timestamp | Request has expired |
Signing
All trading methods are signed using EIP-712. Each successful trading request will contain:
- A piece of structured data that includes the sender address
- A signature of the hash of that structured data, signed by the sender
For detailed information on EIP-712 signing, see EIP-712 Signing.
Nonce Management
The nonce system prevents replay attacks and ensures order uniqueness:
- Use any positive integer as nonce
- Each nonce must be greater than the previous one (incrementing)
Date.now()is a convenient option, not a requirement- If nonce conflicts occur, increment by 1 and retry
:::note SubAccountAction Exception
SubAccountAction endpoints (getPositions, getOpenOrders, getOrdersHistory, getTrades, getFundingPayments, getSubAccount, getSubAccounts, getDelegatedSigners, getBalanceUpdates) do not require a nonce. Only the signature and optional expiresAfter parameters are needed.
:::
| Error Code | Description | Retryable |
|---|---|---|
UNAUTHORIZED | EIP-712 signature validation failed | No |
VALIDATION_ERROR | Request validation failed | No |
MISSING_REQUIRED_FIELD | Required field is missing | No |
INVALID_FORMAT | Field format is invalid | No |
INVALID_VALUE | Invalid parameter value | No |
RATE_LIMIT_EXCEEDED | Too many requests in time window | Yes |
INSUFFICIENT_MARGIN | Not enough margin for trade | No |
ORDER_NOT_FOUND | Order does not exist | No |
OPERATION_TIMEOUT | Operation timed out | Yes |
Next Steps
- Cancel All Orders - Emergency order cancellation
- WebSocket Authentication - Connection setup
- REST Alternative - REST API comparison
