Skip to content

Add Delegated Signer

Add a delegated signer to a subaccount, 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. Multiple delegated signers can be added to a single subaccount.

Endpoint

POST https://papi.synthetix.io/v1/trade

Request

Request Format

{
  "params": {
    "action": "addDelegatedSigner",
    "subAccountId": "1867542890123456789",
    "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
    "permissions": ["session"]
  },
  "nonce": 1735689600000,
  "expiresAfter": 1735689900000,
  "signature": {
    "v": 28,
    "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
  }
}

Request Parameters

ParameterTypeRequiredDescription
paramsobjectYesRequest parameters wrapper
params.actionstringYesMust be "addDelegatedSigner"
params.subAccountIdstringYesThe subaccount ID to add the delegated signer to
params.walletAddressstringYesEthereum 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
params.permissionsstring[]YesSingle-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"].
params.expiresAtintegerNoOptional Unix timestamp (milliseconds) when the delegation expires
expiresAfterintegerNoOptional request expiration timestamp (milliseconds)
ParameterTypeRequiredDescription
nonceuint64Yes*Positive integer nonce (must be incrementing and unique per request)
signatureobjectYesEIP-712 signature object
expiresAfteruint64NoUnix milliseconds expiration timestamp (5x rate limit on stale cancels)

:::info Common Parameters These are top-level request parameters. The subAccountId parameter is specified within the params object (see each endpoint's parameter table). :::

:::info SubAccountAction Endpoints Endpoints using SubAccountAction signing (getPositions, getOpenOrders, getOrdersHistory, getTrades, getFundingPayments, getSubAccount, getSubAccounts, getDelegatedSigners, getBalanceUpdates) do not require the nonce parameter. Only signature and optional expiresAfter are needed. :::

EIP-712 Signature Structure

The action object fields are signed using EIP-712. Note that when expiresAt is omitted in the request, it should be set to 0 (not null) in the EIP-712 message:

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
  • 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)
  • 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 nonce field must be a positive integer, incrementing per subaccount
  • Delegation operations must be signed by an authorized signer: owners can create session or delegate signers, and delegate signers can create session signers only

Response

Success Response

{
  "status": "ok",
  "response": {
    "subAccountId": "1867542890123456789",
    "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
    "permissions": ["session"],
    "expiresAt": null
  },
  "request_id": "5ccf215d37e3ae6d"
}

Response Fields

FieldTypeDescription
subAccountIdstringThe subaccount ID the signer was added to
walletAddressstringThe delegated signer's wallet address
permissionsstring[]Single-item permission array returned by the server
expiresAtinteger | nullExpiration timestamp (null if no expiration)

Error Response

{
  "status": "error",
  "error": {
    "message": "Delegated signer already exists",
    "code": "VALIDATION_ERROR"
  },
  "request_id": "5ccf215d37e3ae6d"
}

Common Error Cases

{
  "status": "error",
  "error": {
    "message": "Maximum delegated signers limit reached",
    "code": "VALIDATION_ERROR"
  },
  "request_id": "5ccf215d37e3ae6d"
}
{
  "status": "error",
  "error": {
    "message": "Subaccount not found",
    "code": "NOT_FOUND"
  },
  "request_id": "5ccf215d37e3ae6d"
}
{
  "status": "error",
  "error": {
    "message": "Cannot delegate to self",
    "code": "VALIDATION_ERROR"
  },
  "request_id": "5ccf215d37e3ae6d"
}

Code Examples

Add Session-Level Trading Bot Signer

{
  "params": {
    "action": "addDelegatedSigner",
    "subAccountId": "1867542890123456789",
    "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f89590",
    "permissions": ["session"]
  },
  "nonce": 1735689600000,
  "expiresAfter": 1735689900000,
  "signature": {
    "v": 28,
    "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
  }
}

Add Delegate-Level Team Member

{
  "params": {
    "action": "addDelegatedSigner",
    "subAccountId": "1867542890123456789",
    "walletAddress": "0x8B3a9A6F8D1e2C4E5B7A9D0F1C3E5A7B9D1F3E5A",
    "permissions": ["delegate"]
  },
  "nonce": 1735689600001,
  "expiresAfter": 1735689900000,
  "signature": {
    "v": 28,
    "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
  }
}

Add Temporary Delegated Signer with Expiration

{
  "params": {
    "action": "addDelegatedSigner",
    "subAccountId": "1867542890123456789",
    "walletAddress": "0x9C4b8E7F0A2D3B6C5E8A1F3D5B7C9E1A3F5D7B9E",
    "permissions": ["session"],
    "expiresAt": 1767225600000
  },
  "nonce": 1735689600002,
  "expiresAfter": 1735689900000,
  "signature": {
    "v": 28,
    "r": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
    "s": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
  }
}

Implementation Notes

  • Caller Permissions: Owners can add session or delegate signers. delegate signers can add session signers 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 removeDelegatedSigner endpoint
  • Limits: Platform may enforce maximum number of delegated signers per subaccount

Security Considerations

  • No Self-Delegation: Accounts cannot delegate to themselves
  • Address Validation: Wallet addresses must be valid Ethereum addresses
  • Scoped Access: ["session"] signers can trade; ["delegate"] signers can also create session-level delegations
  • Signature Required: All delegation operations require valid EIP-712 signatures
  • Master Control: The account owner can always manage delegations; ["delegate"] signers can only create or remove session-level delegations they control

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, such as ["session"] or ["delegate"]
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": ["session"],
  "expiresAt": null,
  "addedBy": "0x1111111111111111111111111111111111111111"
}

Example with Expiration

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

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 Handling

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