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/tradeRequest
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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Client-generated unique request identifier |
method | string | Yes | Must be "post" |
params | object | Yes | Contains all parameters for the request |
Params Object
| Parameter | Type | Required | Description |
|---|---|---|---|
action | string | Yes | Must be "addDelegatedSigner" |
subAccountId | string | Yes | Subaccount identifier |
delegateAddress | string | Yes | Ethereum wallet address of the delegated signer (42-character hex format) |
permissions | string[] | Yes | Array of permission levels to grant. Currently supports: ["trading"] |
expiresAt | integer | No | Optional Unix timestamp (milliseconds) when the delegation expires |
nonce | integer | Yes | Incrementing nonce (Unix ms timestamp as number) |
expiresAfter | integer | No | Optional request expiration timestamp in seconds |
signature | object | Yes | EIP-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" }
]
}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
delegateAddressis the wallet address being granted delegation permissionsexpiresAfteris the request expiration timestamp (when the request itself expires)expiresAtis when the delegation permission expires (use0for no expiration)- The
noncefield must be monotonically increasing 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
| Field | Type | Description |
|---|---|---|
subAccountId | string | The subaccount ID the signer was added to |
walletAddress | string | The delegated signer's wallet address |
permissions | string[] | The permission levels granted |
expiresAt | integer | null | Expiration 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.
| Field | Type | Description |
|---|---|---|
subAccountId | string | Subaccount ID this delegation applies to |
walletAddress | string | Ethereum wallet address of the delegated signer (42-character hex format) |
permissions | string[] | Array of permission levels granted. Currently supports: ["trading"] |
expiresAt | integer | null | Unix timestamp (milliseconds) when the delegation expires. null indicates no expiration |
Example Delegate Object
{
"subAccountId": "1867542890123456789",
"walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
"permissions": ["trading"],
"expiresAt": null
}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
removeDelegatedSignerendpoint
Common Errors
| Error Code | Message | Description |
|---|---|---|
| 400 | Delegated signer already exists | Address is already delegated for this subaccount |
| 400 | Maximum delegated signers limit reached | Too many delegates on this subaccount |
| 400 | Cannot delegate to self | Accounts cannot delegate to themselves |
| 401 | Only master account can add delegated signers | Must be signed by master account owner |
| 404 | Subaccount not found | Invalid subaccount ID |
| Error | Description |
|---|---|
| Invalid signature | EIP-712 signature validation failed |
| Invalid market symbol | Market symbol not recognized |
| Nonce already used | Nonce must be greater than previous value |
| Rate limit exceeded | Too many requests in time window |
| Request expired | expiresAfter timestamp has passed |
Next Steps
- Remove Delegated Signer - Revoke delegation
- Get Delegated Signers - List all delegated signers
- WebSocket Authentication - Connection setup
- REST Alternative - REST API comparison