> ## Documentation Index
> Fetch the complete documentation index at: https://docs.derive.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Access Scopes

> The two scope sets that bound what a session key may do.

Every [session key](/authentication/session-keys) carries **two independent scope sets**, enforced by two different layers:

* **Protocol scopes** — on-chain authority. Signed into each action and re-validated by the protocol state machine. These decide what state-changing actions the key can authorize.
* **Off-chain scopes** — server-side capabilities. Never signed, never seen by the protocol; enforced only by the server before a request is processed.

<Note>All of the steps in this guide can be done through the UX or SDKs.</Note>

## Protocol scopes

Protocol scopes form a **tree**. A grant for a branch covers everything under it, so `trade:all` covers `trade:orderbook:all` and `trade:rfq:option` alike, and `all` at any level covers its children. A request is checked by asking whether one of the key's grants *allows* the specific scope the action requires.

<Frame>
  <img className="block dark:hidden bg-white" src="https://mintcdn.com/derive-a0490cef/BhszcT-bQHpTuFnc/authentication/protocol-scopes-light.png?fit=max&auto=format&n=BhszcT-bQHpTuFnc&q=85&s=96dc88b66009b358895daa16bfc5c1eb" alt="Diagram of the Derive protocol scope tree. The root grant `admin` sits above the branches trade, transfer, withdraw, liquidate, set_session_key, and vault; a grant on any node implicitly covers all of its descendants (for example `trade:all` covers `trade:orderbook:all` and `trade:rfq:option`, and `all` at any level covers its children). The trade branch nests by venue (orderbook, rfq) and then instrument (perp, option, spot), and each node is labeled with the exact wire string a key is granted (e.g. `trade:orderbook:all`, `transfer:existing_subaccount`). A session key with no protocol scopes is read-only. A request is authorized by checking whether one of the key's grants allows the specific scope the action requires." width="1712" height="1534" data-path="authentication/protocol-scopes-light.png" />

  <img className="hidden dark:block bg-black" src="https://mintcdn.com/derive-a0490cef/BhszcT-bQHpTuFnc/authentication/protocol-scopes.png?fit=max&auto=format&n=BhszcT-bQHpTuFnc&q=85&s=b972b12f5a638c813d8357f83d3315a0" alt="Diagram of the Derive protocol scope tree. The root grant `admin` sits above the branches trade, transfer, withdraw, liquidate, set_session_key, and vault; a grant on any node implicitly covers all of its descendants (for example `trade:all` covers `trade:orderbook:all` and `trade:rfq:option`, and `all` at any level covers its children). The trade branch nests by venue (orderbook, rfq) and then instrument (perp, option, spot), and each node is labeled with the exact wire string a key is granted (e.g. `trade:orderbook:all`, `transfer:existing_subaccount`). A session key with no protocol scopes is read-only. A request is authorized by checking whether one of the key's grants allows the specific scope the action requires." width="1712" height="1534" data-path="authentication/protocol-scopes.png" />
</Frame>

## `admin` and `owner` special powers

These are actions only the `admin` or `owner` wallet can do:

* can call `private/update_whitelisted_recipients` to add or remove whitelisted recipients for external transfers and withdrawals.
* can withdraw and transfer to any recipient without setting a whitelist recipient.
* can modify the ip whitelist for a session key

## Off-chain scopes

Off-chain scopes are exact-match only — no tree, no hierarchy.

| Wire string    | Grants                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `account_info` | Read-tier account capability. Gates `private/change_subaccount_label` and a label-only `private/edit_session_key`. |

## How scopes are set

Creator must specify the exact scopes for a session key. A session key with no scopes is a purely read-only session key.

The "owner" wallet by default has all scopes.

<Note>
  See [Session keys](/authentication/session-keys) for the full create / edit / list lifecycle and
  [Authentication](/authentication/session-login) for how a session resolves to protocol and off-chain scopes. Amounts
  and constants referenced by signed actions live on [Action signing](/authentication/action-signing).
</Note>

<CodeGroup>
  ```typescript TypeScript (SDK) theme={null}
  import { Wallet } from 'ethers';
  import {
    DeriveClient,
    OffchainScope,
    ProtocolScopeCode,
  } from '@derivexyz/derive-ts';

  const client = new DeriveClient({
    network: 'testnet',
    wallet: process.env.PRIVATE_KEY!,
  });

  // set_session_key is an action-signed private call: open and authenticate first.
  await client.connect();
  await client.login();

  // Generate the key locally — only its address is registered.
  const sessionKey = Wallet.createRandom();

  const created = await client.sessionKeys.set({
    publicSessionKey: sessionKey,
    expirySec: Math.floor(Date.now() / 1000) + 30 * 24 * 3600, // the KEY's lifetime: 30 days
    protocolScopes: [
      ProtocolScopeCode.TradeOrderbookAll,
      ProtocolScopeCode.TradeRfqOption,
    ],
    offchainScopes: [OffchainScope.AccountInfo],
    label: 'trading-bot',
  });

  // The ack echoes exactly what the key holds — there is no implicit default:
  console.log(created.protocol_scopes); // ['trade:orderbook:all', 'trade:rfq:option']
  console.log(created.offchain_scopes); // ['account_info']
  ```

  ```python Python (SDK) theme={null}
  import asyncio
  import time

  from eth_account import Account

  from derive_py import WebSocketClient
  from derive_py.data_types import OffchainScope, ProtocolScope


  async def main():
      client = WebSocketClient.from_env()
      # set_session_key is an action-signed private call: connect first.
      await client.connect()

      # Generate the key locally — only its address is registered.
      session_wallet = Account.create()

      created = await client.account.set_session_key(
          public_session_key=session_wallet.address,
          expiry_sec=int(time.time()) + 30 * 24 * 3600,  # the KEY's lifetime: 30 days
          protocol_scopes=[
              ProtocolScope.TRADE_ORDERBOOK_ALL,
              ProtocolScope.TRADE_RFQ_OPTION,
          ],
          offchain_scopes=[OffchainScope.ACCOUNT_INFO],
          label="trading-bot",
      )

      # The ack echoes exactly what the key holds — there is no implicit default:
      print(created.protocol_scopes)  # ['trade:orderbook:all', 'trade:rfq:option']
      print(created.offchain_scopes)  # ['account_info']

      await client.disconnect()


  asyncio.run(main())
  ```

  ```rust Rust (SDK) theme={null}
  use alloy::signers::local::PrivateKeySigner;
  use derive_rs::{
      Environment, WsClient,
      actions::session_key::{OffChainScope, ProtocolScope, SetSessionKeyArgs},
  };

  #[tokio::main]
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
      let client = WsClient::new(
          Environment::Testnet,
          Some(std::env::var("DERIVE_PRIVATE_KEY")?),
          Some(std::env::var("DERIVE_WALLET")?),
          Some(9),
      )
      .await?;
      // set_session_key is an action-signed private call: authenticate first.
      client.login().await?;

      // Generate the key locally — only its address is registered.
      let session_signer = PrivateKeySigner::random();
      let expiry_sec = chrono::Utc::now().timestamp() as u64 + 30 * 24 * 3600;

      let args = SetSessionKeyArgs::builder()
          .public_session_key(session_signer.address().to_string())
          .expiry_second(expiry_sec) // the KEY's lifetime: 30 days
          .protocol_scopes(vec![
              ProtocolScope::TradeOrderbookAll,
              ProtocolScope::TradeRfqOption,
          ])
          .off_chain_scopes(vec![OffChainScope::AccountInfo])
          .label("trading-bot".to_string())
          .subaccount_ids(vec![9])
          .build();

      let created = client.session_keys().create(args).await?;

      // The ack echoes exactly what the key holds — there is no implicit default:
      println!("{:?}", created.protocol_scopes); // ["trade:orderbook:all", "trade:rfq:option"]
      println!("{:?}", created.offchain_scopes); // ["account_info"]
      Ok(())
  }
  ```

  ```bash cURL theme={null}
  # Body nonce/signature: EIP-712 action signing under the set-session-key
  # module (see /action-signing). X-Derive* headers: session auth (see /json-rpc).
  curl -X POST https://api.derive.xyz/v3/private/set_session_key \
    -H "Content-Type: application/json" \
    -H "X-DeriveWallet: $WALLET_ADDRESS" \
    -H "X-DeriveTimestamp: $TS" \
    -H "X-DeriveSignature: $SIG" \
    -d '{
      "wallet": "0xYourWallet",
      "public_session_key": "0xSessionKeyAddress",
      "expiry_sec": 1733592000,
      "subaccount_ids": null,
      "nonce": "1730999700000123000",
      "signature_expiry_sec": 1731000600,
      "signer": "0xYourWallet",
      "signature": "0x…",
      "protocol_scopes": ["trade:orderbook:all", "trade:rfq:option"],
      "offchain_scopes": ["account_info"],
      "label": "trading-bot"
    }'
  ```
</CodeGroup>


## Related topics

- [Smart Contracts & Multi-sigs](/authentication/contract-owned-accounts.md)
- [New features](/migrating/new-features.md)
- [Market Maker Protection](/trading/market-maker-protection.md)
