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",
"walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
"permissions": ["session"],
"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 |
walletAddress | string | Yes | Ethereum wallet address of the delegated signer (42-character hex format). Note: this request field is walletAddress; the EIP-712 signed message uses delegateAddress for the same value |
permissions | string[] | Yes | Single-item permission array. Use ["session"] for trading access or ["delegate"] for trading access plus the ability to create session-level delegations. Older clients may still submit ["trading"], which is treated as ["session"]. |
expiresAt | integer | No | Optional Unix timestamp (milliseconds) when the delegation expires |
nonce | integer | Yes | Positive integer, incrementing nonce |
expiresAfter | integer | No | Optional request expiration timestamp in seconds |
signature | object | Yes | EIP-712 signature |
Important: Owners can add session or delegate signers. delegate signers can add session signers only.
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": ["session"]
}
}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)- Exactly one permission must be provided. Use
["session"]for trading access or["delegate"]for trading access plus the ability to create session-level delegations. Older clients may still submit["trading"], which is treated as["session"]. - The
noncefield must be a positive integer, incrementing per subaccount - Delegation operations must be signed by an authorized signer: owners can create
sessionordelegatesigners, anddelegatesigners can createsessionsigners only
Response Format
Success Response
{
"id": "delegate-add-1",
"status": 200,
"result": {
"subAccountId": "1867542890123456789",
"walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
"permissions": ["session"],
"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[] | Single-item permission array returned by the server |
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: "delegateAddress", type: "address" },
{ name: "subAccountId", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "expiresAfter", type: "uint256" },
{ name: "expiresAt", type: "uint256" },
{ name: "permissions", type: "string[]" }
]
};
const message = {
subAccountId: BigInt(subAccountId),
delegateAddress,
permissions,
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,
walletAddress: delegateAddress,
permissions,
expiresAt: expiresAt || undefined,
nonce,
expiresAfter,
signature: { v: signature.v, r: signature.r, s: signature.s }
}
}));
}
// Usage: Add a session-level trading bot as delegated signer
await addDelegatedSigner(
ws,
signer,
"1867542890123456789",
"0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
["session"]
);Code Examples
Add Session-Level Trading Bot Signer
{
"id": "delegate-add-bot",
"method": "post",
"params": {
"action": "addDelegatedSigner",
"subAccountId": "1867542890123456789",
"walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
"permissions": ["session"],
"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",
"walletAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E",
"permissions": ["session"],
"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, such as ["session"] or ["delegate"] |
expiresAt | integer | null | Unix timestamp (milliseconds) when the delegation expires. null indicates no expiration |
addedBy | string | omitted | Wallet address that created this delegation. Omitted for pre-migration records where the creator was not recorded |
Example Delegate Object
{
"subAccountId": "1867542890123456789",
"walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
"permissions": ["session"],
"expiresAt": null,
"addedBy": "0x1111111111111111111111111111111111111111"
}Example with Expiration
{
"subAccountId": "1867542890123456789",
"walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
"permissions": ["delegate"],
"expiresAt": 1767225600000
}Implementation Notes
- Caller Permissions: Owners can add
sessionordelegatesigners.delegatesigners can addsessionsigners only - Multiple Signers: A subaccount can have multiple delegated signers simultaneously
- Unique Addresses: Each wallet address can only be delegated once per subaccount
- Permission Levels: Use
["session"]for trading access or["delegate"]for trading access plus the ability to create session-level delegations - 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 |
| 403 | Caller is not authorized to add the requested delegation | Session signers cannot create delegations, and delegate signers cannot grant delegate-level access |
| 404 | Subaccount not found | Invalid subaccount ID |
| 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
- Remove Delegated Signer - Revoke delegation
- Get Delegated Signers - List all delegated signers
- WebSocket Authentication - Connection setup
- REST Alternative - REST API comparison
