Overview

Delegated access works by creating a specialized business-controlled user within each end-user’s sub-organization that has carefully scoped permissions to perform only specific actions, such as signing transactions to designated addresses. This can enable your backend to do things like:
  • Automate common transactions (e.g., staking, redemptions)
  • Sign transactions to whitelisted addresses without user involvement
  • Perform scheduled operations
  • Respond to specific onchain events programmatically

Implementation flow

Here’s how to implement delegated access for an embedded wallet setup:
  • Create a sub-organization with two root users: The end user and your “Delegated User” with an API key authenticator that you control
  • Enable the Delegated Account to take particular actions by setting policies explicitly allowing those specific actions
  • Update the root quorum to ensure only the end-user retains root privileges
A simple example demonstrating the delegated acess setup can be found here.

Step-by-step implementation

Step 1: Create a sub-organization with two root users

  • Create your sub-organization with the two root users being:
    • The end-user
    • A user you control (we’ll call it the ‘Delegated Account’)
{
  "type": "ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V7",
  "timestampMs": "<time-in-ms>",
  "organizationId": "your-organization-id",
  "parameters": {
    "subOrganizationName": "<sub-org-name>",
    "rootUsers": [
      {
        "userName": "<end-user-name>",
        "userEmail": "enduser@example.com",
        "authenticators": [
          {
            "authenticatorName": "<passkey-name>",
            "challenge": "<webauthn-challenge>",
            "attestation": {
              "credentialId": "<credential-id>",
              "clientDataJson": "<client-data-json>",
              "attestationObject": "<attestation-object>",
              "transports": ["AUTHENTICATOR_TRANSPORT_HYBRID"]
            }
          }
        ],
        "apiKeys": [],
      "oidcProviders": []
      },
      {
        "userName": "Delegated Account",
        "userEmail": "<email>(optional)",
        "authenticators": [],
        "apiKeys": [
          {
            "apiKeyName": "<delegated-account-api-key-name>",
            "publicKey": "<delegated-account-api-public-key>"
          }
        ],
      "oidcProviders": []
      }
    ],
    "rootQuorumThreshold": 1,
    "wallet": {
      "walletName": "Default ETH Wallet",
      "accounts": [
        {
          "curve": "CURVE_SECP256K1",
          "pathFormat": "PATH_FORMAT_BIP32",
          "path": "m/44'/60'/0'/0/0",
          "addressFormat": "ADDRESS_FORMAT_ETHEREUM"
        }
      ]
    }
  }
}

Step 2: Limit the permissions of the Delegated Account user via policies

  • Create a custom policy granting the Delegated Account specific permissions. You might grant that user permissions to:
    • Sign any transaction
    • Sign only transactions to a specific address
    • Create new users in the sub-org
    • Or any other activity you want to be able to take using your Delegated Account
Here’s one example, granting the Delegated Account only the permission to sign ethereum transactions to a specific receiver address:
{
  "type": "ACTIVITY_TYPE_CREATE_POLICY",
  "timestampMs": "<time-in-ms>",
  "organizationId": "sub-organization-id",
  "parameters": {
    "policyName": "Allow Delegated Account to sign transactions to specific address",
    "policy": {
      "effect": "EFFECT_ALLOW",
      "consensus": "approvers.any(user, user.id == <DELEGATED_ACCOUNT_USER_ID>)",
      "condition": "eth.tx.to == <RECIPIENT_ADDRESS>"
    },
  }
}

Step 3: Remove the Delegated Account from the root quorum using the Delegated Account’s credentials:

// Update the root quorum of the sub organization to ONLY include the end user, removing the delegated access user
{
  "type": "ACTIVITY_TYPE_UPDATE_ROOT_QUORUM",
  "timestampMs": "<time-in-ms>",
  "organizationId": "<sub-organization-id>",
  "parameters": {
    "threshold": 1,
    "userIds": [
      "<end-user-id>" 
    ]
  }
}
After completing these steps, the sub-organization will have two users: the end-user (the only root-user) and the Delegated Account user, which only has the permissions granted earlier via policies and no longer retains root user privileges.

Delegated Access Code Example

Below is a code example outlining the implementation of the delegated access setup flow described above
import { Turnkey } from "@turnkey/sdk-server";
import dotenv from "dotenv";

dotenv.config();

  // Initialize the Turnkey Server Client on the server-side
  const turnkeyServer = new Turnkey({
    apiBaseUrl: "https://api.turnkey.com",
    apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY,
    apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY,
    defaultOrganizationId: process.env.TURNKEY_ORGANIZATION_ID,
  }).apiClient();


  // To create an API key programmatically check https://github.com/tkhq/sdk/blob/main/examples/kitchen-sink/src/sdk-server/createApiKey.ts
  const publicKey = "<delegated_api_public_key>";
  const curveType = "API_KEY_CURVE_P256"; // this is the default
  const apiKeys = [
    {
      apiKeyName: "Delegated - API Key",
      publicKey,
      curveType,
    },
  ];

  // STEP 1: Create a sub org with End User and Delegated access user in Root Quorum

  const subOrg = await turnkeyClient.createSubOrganization({
    organizationId: process.env.TURNKEY_ORGANIZATION_ID!,
    subOrganizationName: `Sub Org - With Delegated Access User`,
    rootUsers: [
      {
        userName: "Delegated Access User",
        apiKeys,
        authenticators: [],
        oauthProviders: []
      },
      {
          userName: "End User",
          userEmail: "<some-email>",
          apiKeys: [],
          authenticators: [],
          oauthProviders: []
      },
    ],
    rootQuorumThreshold: 1,
    wallet: {
      "walletName": "Default ETH Wallet",
      "accounts": [
        {
          "curve": "CURVE_SECP256K1",
          "pathFormat": "PATH_FORMAT_BIP32",
          "path": "m/44'/60'/0'/0/0",
          "addressFormat": "ADDRESS_FORMAT_ETHEREUM"
        }
      ]
    },
  });

  console.log("sub-org id:", subOrg.subOrganizationId);

  // Initializing the Turkey client used by the Delegated Access User
  // Notice the subOrganizationId created above 
  const turnkeyDelegatedAccessClient = new Turnkey({
    apiBaseUrl: "https://api.turnkey.com",
    apiPrivateKey: process.env.DELEGATED_API_PRIVATE_KEY!,
    apiPublicKey: process.env.DELEGATED_API_PUBLIC_KEY!,
    defaultOrganizationId: subOrg.subOrganizationId,
  }).apiClient();

  // STEP 2: Create a policy allowing the Delegated access user to send Ethereum transactions to a particular address

  // Creating a policy for the Delegated account 
  const delegated_userid = subOrg.rootUserIds[0];
  const policyName = "Allow Delegated Account to sign transactions to specific address";
  const effect = "EFFECT_ALLOW";
  const consensus = `approvers.any(user, user.id == '${delegated_userid}')`;
  const condition = `eth.tx.to == '${process.env.RECIPIENT_ADDRESS}'`;
  const notes = "";

  const { policyId } = await turnkeyDelegated.createPolicy({
    policyName,
    condition,
    consensus,
    effect,
    notes,
  });

  // STEP 3: Update the root quorum to only include the End User, removing the Delegated Access user

  // Remove the Delegated Account from the root quorum
  const RootQuorum = await turnkeyDelegated.updateRootQuorum({
    threshold: 1,
    userIds: [subOrg.rootUserIds[1]], // retain the end user
  });

Frequently Asked Questions

Policy Design and Creation

Yes — if the delegated access (DA) user is part of the root quorum, they can create policies unilaterally. Once removed from the root quorum, only the remaining quorum member (typically the end-user) can make further policy changes.Note that this is also possible even if the initating user, the delegate, is not a root user: a policy may exist that grants explicit permissions to non-root users to create policies.
End-user approval typically happens during intent creation — for example, when the end-user authorizes a DA user by adjusting the quorum or signs an initial setup transaction.
As long as the DA user remains authorized, they can remove policies programmatically. If they’ve been removed from the quorum, policy deletion will require the user’s explicit approval.NOTE: Turnkey is looking to support the concept of ‘one-time-use policies’ to make it easier to manage redundany policies.

Security & Risk Management

Yes — if the key is attached to a broad policy. That’s why it’s important to limit the scope of policies and enforce API hygiene practices.
In effect, yes. The key difference is that granular policies can restrict what a DA user can do, offering better security hygiene even if there’s still elevated access.
Typically this is a combination of, or all of the following practices, though not exclusive to just these:
  • Using short-lived keys whenever viable
  • Rotating API keys regurarly
  • Monitoring the usage
  • Secure storage (e.g. in HSMs or vaults)

Best Practices

You can define strict transaction conditions. For example:
solana.tx.instructions.count() == 1 &&
solana.tx.transfers.count() == 1 &&
solana.tx.transfers.all(transfer, transfer.to == '<SPECIFIC_ADDRESS>')
You can also consider the following:
  • Recipient address restrictions (ie allowlisting addresses)
  • Contract method selectors
  • Transaction structure invariants
  • Blockhash constraints (on Solana)
Yes — this is a common pattern. You can add a policy per order (limit, stop loss, TWAP, etc.) using the DA user, without requiring the end-user to sign for each one.
Turnkey’s Policy Engine shines through its flexibility. There are many different approaches you can take based on your requirements, but various themes we see include:
  • Using broad policies with business-controlled API keys
  • Using **fine-grained policies, **scoped to predictable transaction shapes
  • Using delegated access to implement limit orders, automation flows, or advanced trading logic (e.g. perps, TWAPs)
  • Ensuring strong operational security (e.g. tight scoping & expiring keys) is increasingly common

EVM and SVM-Specific Strategies

Yes, on Solana via solana.tx.recent_blockhash, which restricts a transaction’s validity to a ~60–90 second window. Not ideal for delayed executions (e.g. limit orders), but useful for immediate, single-use actions.
Yes, though it’s limited today. You can inspect calldata (e.g., using eth.tx.data[...]) and enforce conditions like:
eth.tx.to == '<TOKEN_CONTRACT>' &&
eth.tx.data[0..4] == '<ERC20_FUNCTION_SELECTOR>'
Granular support for calldata parsing and value limits is coming soon.
Not entirely. Even if you allowlist the router, it could still be abused to swap all assets. You can’t control downstream behavior unless you control the contract.
Suggestion: Only allow DA keys to interact with contracts you fully trust or control. Limit scope as much as possible (e.g., to specific instructions, amounts, or recipients).