Skip to content

Add Delegated Signer (WebSocket)

Add a delegated signer to a subaccount through the WebSocket connection, allowing another wallet address to perform authorized actions on behalf of the subaccount. This enables secure delegation of trading operations to automated systems, trading bots, or team members without sharing private keys.

Endpoint

ws.send() wss://api.synthetix.io/v1/ws/trade

Request

Request Format

{
  "id": "delegate-add-1",
  "method": "post",
  "params": {
    "action": "addDelegatedSigner",
    "subAccountId": "1867542890123456789",
    "delegateAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
    "permissions": ["trading"],
    "nonce": 1735689600000,
    "expiresAfter": 1735689900,
    "signature": {
      "v": 28,
      "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
      "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
    }
  }
}

Parameters

Request Parameters

ParameterTypeRequiredDescription
idstringYesClient-generated unique request identifier
methodstringYesMust be "post"
paramsobjectYesContains all parameters for the request

Params Object

ParameterTypeRequiredDescription
actionstringYesMust be "addDelegatedSigner"
subAccountIdstringYesSubaccount identifier
delegateAddressstringYesEthereum wallet address of the delegated signer (42-character hex format)
permissionsstring[]YesArray of permission levels to grant. Currently supports: ["trading"]
expiresAtintegerNoOptional Unix timestamp (milliseconds) when the delegation expires
nonceintegerYesPositive integer, incrementing nonce
expiresAfterintegerNoOptional request expiration timestamp in seconds
signatureobjectYesEIP-712 signature

Important: Only the master account owner can add delegated signers.

EIP-712 Type Definition

EIP-712 Type Definitions for Delegation

AddDelegatedSigner

const AddDelegatedSignerTypes = {
  AddDelegatedSigner: [
    { name: "delegateAddress", type: "address" },
    { name: "subAccountId", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" },
    { name: "expiresAt", type: "uint256" },
    { name: "permissions", type: "string[]" }
  ]
}

GetDelegatedSigners

Uses the standard SubAccountAction type (same as other read operations like getTrades, getPositions):

const SubAccountActionTypes = {
  SubAccountAction: [
    { name: "subAccountId", type: "uint256" },
    { name: "action", type: "string" },
    { name: "expiresAfter", type: "uint256" }
  ]
}
 
// Message example:
const message = {
  subAccountId: "1867542890123456789",
  action: "getDelegatedSigners",
  expiresAfter: 0  // Optional, use 0 if not expiring
}

RemoveDelegatedSigner

const RemoveDelegatedSignerTypes = {
  RemoveDelegatedSigner: [
    { name: "delegateAddress", type: "address" },
    { name: "subAccountId", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
}

RemoveAllDelegatedSigners

const RemoveAllDelegatedSignersTypes = {
  RemoveAllDelegatedSigners: [
    { name: "subAccountId", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "expiresAfter", type: "uint256" }
  ]
}

Example Typed Data for AddDelegatedSigner

{
  "types": {
    "EIP712Domain": [
      { "name": "name", "type": "string" },
      { "name": "version", "type": "string" },
      { "name": "chainId", "type": "uint256" },
      { "name": "verifyingContract", "type": "address" }
    ],
    "AddDelegatedSigner": [
      { "name": "delegateAddress", "type": "address" },
      { "name": "subAccountId", "type": "uint256" },
      { "name": "nonce", "type": "uint256" },
      { "name": "expiresAfter", "type": "uint256" },
      { "name": "expiresAt", "type": "uint256" },
      { "name": "permissions", "type": "string[]" }
    ]
  },
  "primaryType": "AddDelegatedSigner",
  "domain": {
    "name": "Synthetix",
    "version": "1",
    "chainId": 1,
    "verifyingContract": "0x0000000000000000000000000000000000000000"
  },
  "message": {
    "delegateAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
    "subAccountId": "1867542890123456789",
    "nonce": 1735689600000,
    "expiresAfter": 1735689900000,
    "expiresAt": 0,
    "permissions": ["trading"]
  }
}

Important Notes:

  • Field order matters for EIP-712 - Fields must be in the exact order shown above for signature verification
  • delegateAddress is the wallet address being granted delegation permissions
  • expiresAfter is the request expiration timestamp (when the request itself expires)
  • expiresAt is when the delegation permission expires (use 0 for no expiration)
  • The nonce field must be a positive integer, incrementing per subaccount
  • All delegation operations must be signed by the master account owner

Response Format

Success Response

{
  "id": "delegate-add-1",
  "status": 200,
  "result": {
    "subAccountId": "1867542890123456789",
    "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
    "permissions": ["trading"],
    "expiresAt": null
  }
}

Response Fields

FieldTypeDescription
subAccountIdstringThe subaccount ID the signer was added to
walletAddressstringThe delegated signer's wallet address
permissionsstring[]The permission levels granted
expiresAtinteger | nullExpiration timestamp (null if no expiration)

Error Response

{
  "id": "delegate-add-1",
  "status": 400,
  "result": null,
  "error": {
    "code": 400,
    "message": "Delegated signer already exists"
  }
}

Implementation Example

import { ethers } from 'ethers';
 
async function addDelegatedSigner(ws, signer, subAccountId, delegateAddress, permissions, expiresAt = 0) {
  const nonce = Date.now();
  const expiresAfter = Math.floor(Date.now() / 1000) + 300; // 5 minutes
 
  const domain = {
    name: "Synthetix",
    version: "1",
    chainId: 1,
    verifyingContract: "0x0000000000000000000000000000000000000000"
  };
 
  const types = {
    AddDelegatedSigner: [
      { name: "subAccountId", type: "uint256" },
      { name: "delegateAddress", type: "address" },
      { name: "permissions", type: "uint8" },
      { name: "expiresAt", type: "uint256" },
      { name: "nonce", type: "uint256" },
      { name: "expiresAfter", type: "uint256" }
    ]
  };
 
  const message = {
    subAccountId: BigInt(subAccountId),
    delegateAddress,
    permissions: 1, // trading permission
    expiresAt: BigInt(expiresAt),
    nonce: BigInt(nonce),
    expiresAfter: BigInt(expiresAfter)
  };
 
  const sig = await signer.signTypedData(domain, types, message);
  const signature = ethers.Signature.from(sig);
 
  ws.send(JSON.stringify({
    id: `delegate-add-${Date.now()}`,
    method: "post",
    params: {
      action: "addDelegatedSigner",
      subAccountId,
      delegateAddress,
      permissions,
      expiresAt: expiresAt || undefined,
      nonce,
      expiresAfter,
      signature: { v: signature.v, r: signature.r, s: signature.s }
    }
  }));
}
 
// Usage: Add a trading bot as delegated signer
await addDelegatedSigner(
  ws,
  signer,
  "1867542890123456789",
  "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
  ["trading"]
);

Code Examples

Add Trading Bot Signer

{
  "id": "delegate-add-bot",
  "method": "post",
  "params": {
    "action": "addDelegatedSigner",
    "subAccountId": "1867542890123456789",
    "delegateAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
    "permissions": ["trading"],
    "nonce": 1735689600000,
    "expiresAfter": 1735689900,
    "signature": { "v": 28, "r": "0x...", "s": "0x..." }
  }
}

Add Temporary Delegated Signer with Expiration

{
  "id": "delegate-add-temp",
  "method": "post",
  "params": {
    "action": "addDelegatedSigner",
    "subAccountId": "1867542890123456789",
    "delegateAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E",
    "permissions": ["trading"],
    "expiresAt": 1767225600000,
    "nonce": 1735689600001,
    "expiresAfter": 1735689900,
    "signature": { "v": 28, "r": "0x...", "s": "0x..." }
  }
}

Delegate Object Structure

Delegate Object

The delegate object represents a delegated signer in API responses. Note that the response uses walletAddress while EIP-712 signing uses delegateAddress.

FieldTypeDescription
subAccountIdstringSubaccount ID this delegation applies to
walletAddressstringEthereum wallet address of the delegated signer (42-character hex format)
permissionsstring[]Array of permission levels granted. Currently supports: ["trading"]
expiresAtinteger | nullUnix timestamp (milliseconds) when the delegation expires. null indicates no expiration
addedBystring | omittedWallet address that created this delegation. Omitted for pre-migration records where the creator was not recorded

Example Delegate Object

{
  "subAccountId": "1867542890123456789",
  "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
  "permissions": ["trading"],
  "expiresAt": null,
  "addedBy": "0x1111111111111111111111111111111111111111"
}

Example with Expiration

{
  "subAccountId": "1867542890123456789",
  "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
  "permissions": ["trading"],
  "expiresAt": 1767225600000
}

Implementation Notes

  • Master Account Required: Only the master account owner can add delegated signers
  • Multiple Signers: A subaccount can have multiple delegated signers simultaneously
  • Unique Addresses: Each wallet address can only be delegated once per subaccount
  • Trading Permission: The ["trading"] permission grants ability to place, modify, and cancel orders
  • Immediate Effect: Delegations take effect immediately upon successful creation
  • Optional Expiration: Delegations can include an expiration timestamp after which they become invalid
  • Revocable: Delegations can be revoked at any time using the removeDelegatedSigner endpoint

Common Errors

Error CodeMessageDescription
400Delegated signer already existsAddress is already delegated for this subaccount
400Maximum delegated signers limit reachedToo many delegates on this subaccount
400Cannot delegate to selfAccounts cannot delegate to themselves
401Only master account can add delegated signersMust be signed by master account owner
404Subaccount not foundInvalid subaccount ID
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