Skip to content

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/trade

Request

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

ParameterTypeRequiredDescription
idstringYesClient-generated unique request identifier
methodstringYesMust be "post"
paramsobjectYesFlat map containing all request fields
params.actionstringYesMust be "scheduleCancel"
params.timeoutSecondsintegerYesTimeout in seconds; use 0 to cancel an existing schedule
params.nonceintegerYesIncrementing nonce (Unix ms timestamp as number)
params.expiresAfterintegerNoOptional expiration timestamp in seconds
params.signatureobjectYesEIP-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

errorCodeExample messageDescription
VALIDATION_ERRORtimeoutSeconds must be 0 or at least 5timeoutSeconds below minimum
VALIDATION_ERRORtimeoutSeconds must be less than or equal to 86400timeoutSeconds exceeds maximum
UNAUTHORIZEDnonce is required and must be non-zeroMissing or zero nonce
UNAUTHORIZEDexpiresAfter must be a future unix timestampRequest 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 CodeDescriptionRetryable
UNAUTHORIZEDEIP-712 signature validation failedNo
VALIDATION_ERRORRequest validation failedNo
MISSING_REQUIRED_FIELDRequired field is missingNo
INVALID_FORMATField format is invalidNo
INVALID_VALUEInvalid parameter valueNo
RATE_LIMIT_EXCEEDEDToo many requests in time windowYes
INSUFFICIENT_MARGINNot enough margin for tradeNo
ORDER_NOT_FOUNDOrder does not existNo
OPERATION_TIMEOUTOperation timed outYes

Next Steps