> ## 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.

# Session Keys

> Delegated signing keys: create, edit, list, and their lifecycle.

A **session key** is a delegated EVM address (an EOA your client controls) that your wallet authorizes to sign [actions](/authentication/action-signing) on its behalf. Instead of exposing your wallet's private key to a trading service, you mint a scoped, expiring key that can sign only the actions you permit, on only the subaccounts you allow.

Every session key carries **two independent scope sets** on one record:

* **Protocol scopes** — on-chain authority (trade, transfer, withdraw, …). Signed into each action and re-validated by the protocol.
* **Off-chain scopes** — server-side capabilities (`account_info`). Never signed; enforced by the server.

See [Access scopes](/authentication/access-scopes) for the full scope catalog and per-route gating.

## How session keys are used

<Frame>
  <img className="block dark:hidden bg-white" src="https://mintcdn.com/derive-a0490cef/BhszcT-bQHpTuFnc/authentication/session-key-uses-light.png?fit=max&auto=format&n=BhszcT-bQHpTuFnc&q=85&s=81ceb60d4eb34cce8ee8b76452d16f14" alt="Diagram showing the two distinct ways a session key signs. First, signed actions: the key produces EIP-712 typed-data signatures over an Action struct for state-changing operations such as trading, RFQs, withdrawals, and transfers, and the protocol re-validates the key's protocol scopes against each action. Second, login headers: the key produces EIP-191 signatures over the login payload to authenticate HTTP and WebSocket sessions, and the server enforces the key's off-chain scopes, IP whitelist, and expiry. The wallet delegates authority to the session key, which then performs both signing roles on the wallet's behalf without ever exposing the wallet's own private key." width="1760" height="672" data-path="authentication/session-key-uses-light.png" />

  <img className="hidden dark:block bg-black" src="https://mintcdn.com/derive-a0490cef/BhszcT-bQHpTuFnc/authentication/session-key-uses.png?fit=max&auto=format&n=BhszcT-bQHpTuFnc&q=85&s=7ce575922874fa87c287fcc718dfb269" alt="Diagram showing the two distinct ways a session key signs. First, signed actions: the key produces EIP-712 typed-data signatures over an Action struct for state-changing operations such as trading, RFQs, withdrawals, and transfers, and the protocol re-validates the key's protocol scopes against each action. Second, login headers: the key produces EIP-191 signatures over the login payload to authenticate HTTP and WebSocket sessions, and the server enforces the key's off-chain scopes, IP whitelist, and expiry. The wallet delegates authority to the session key, which then performs both signing roles on the wallet's behalf without ever exposing the wallet's own private key." width="1760" height="672" data-path="authentication/session-key-uses.png" />
</Frame>

## Delegated attenuation

The key you create can never exceed the authority of the key that signs the creation action. A child key's **scopes, expiry, and subaccounts must each be a subset of its parent's**. A key without `admin` cannot mint an `admin` child; a key expiring next week cannot mint a child expiring next month. The protocol enforces this when the create action is applied.

The signing parent must itself hold the `set_session_key` protocol scope.

## Create a session key

1. API -> call `private/set_session_key` with the owner or any session key with stronger scopes.
2. Onchain -> see [Smart Contract & Multi-sig Accounts](/authentication/contract-owned-accounts).

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

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

  // A fresh keypair for the delegated key; persist its private key for the bot.
  const sessionWallet = Wallet.createRandom();

  // The SDK builds and EIP-712-signs the create action for you (nonce, signature,
  // signature_expiry_sec, module) using the client's signer.
  const created = await client.sessionKeys.set({
    publicSessionKey: sessionWallet,
    expirySec: 1793491200,
    protocolScopes: [
      ProtocolScopeCode.TradeOrderbookAll,
      ProtocolScopeCode.TransferExistingSubaccount,
    ],
    offchainScopes: [OffchainScope.AccountInfo],
    subaccountIds: [9],
    label: 'market-maker-1',
  });

  console.log(created.public_session_key, created.subaccount_ids);
  await client.close();

  ```

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

  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()
      await client.connect()

      # A fresh keypair for the delegated key; persist its private key for the bot.
      session_wallet = Account.create()

      # The SDK builds and EIP-712-signs the create action for you (nonce,
      # signature, signature_expiry_sec, module) using the client's signer.
      created = await client.account.set_session_key(
          public_session_key=session_wallet.address,
          expiry_sec=1793491200,
          protocol_scopes=[
              ProtocolScope.TRADE_ORDERBOOK_ALL,
              ProtocolScope.TRANSFER_EXISTING_SUBACCOUNT,
          ],
          offchain_scopes=[OffchainScope.ACCOUNT_INFO],
          subaccount_ids=[9],
          label="market-maker-1",
      )
      print(created.public_session_key, created.subaccount_ids)

      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::Mainnet,
          Some(std::env::var("DERIVE_PRIVATE_KEY")?),
          Some(std::env::var("DERIVE_WALLET")?),
          Some(9),
      )
      .await?;
      client.login().await?;

      // A fresh keypair for the delegated key; persist its private key for the bot.
      let session_signer = PrivateKeySigner::random();

      // The SDK builds and EIP-712-signs the create action for you (nonce,
      // signature, signature_expiry_sec, module) using the client's signer.
      let args = SetSessionKeyArgs::builder()
          .public_session_key(session_signer.address().to_string())
          .expiry_second(1793491200)
          .protocol_scopes(vec![
              ProtocolScope::TradeOrderbookAll,
              ProtocolScope::TransferExistingSubaccount,
          ])
          .off_chain_scopes(vec![OffChainScope::AccountInfo])
          .subaccount_ids(vec![9])
          .label("market-maker-1".to_string())
          .build();

      let created = client.session_keys().create(args).await?;
      println!("{} {:?}", created.public_session_key, created.subaccount_ids);
      Ok(())
  }
  ```

  ```bash cURL theme={null}
  # Signed action: build nonce/signature/signature_expiry_sec per /authentication/action-signing.
  # X-Derive* headers authenticate the HTTP session (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": "0x1234...abcd",
      "public_session_key": "0x9f00...beef",
      "expiry_sec": 1793491200,
      "subaccount_ids": [9],
      "protocol_scopes": ["trade:orderbook:all", "transfer:existing_subaccount"],
      "offchain_scopes": ["account_info"],
      "label": "market-maker-1",
      "ip_whitelist": [],
      "nonce": "1710000000000123000",
      "signer": "0x1234...abcd",
      "signature": "0xabcd...1b",
      "signature_expiry_sec": 1710000600,
      "module": "0x0000000000000000000000000000000000000000"
    }'
  ```

  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "private/set_session_key",
    "params": {
      "wallet": "0x1234...abcd",
      "public_session_key": "0x9f00...beef",
      "expiry_sec": 1793491200,
      "subaccount_ids": [9],
      "protocol_scopes": ["trade:orderbook:all", "transfer:existing_subaccount"],
      "offchain_scopes": ["account_info"],
      "label": "market-maker-1",
      "ip_whitelist": [],
      "nonce": "1710000000000123000",
      "signer": "0x1234...abcd",
      "signature": "0xabcd...1b",
      "signature_expiry_sec": 1710000600,
      "module": "0x0000000000000000000000000000000000000000"
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "public_session_key": "0x9f00...beef",
      "protocol_scopes": ["trade:orderbook:all", "transfer:existing_subaccount"],
      "offchain_scopes": ["account_info"],
      "label": "market-maker-1",
      "ip_whitelist": [],
      "expiry_sec": 1793491200,
      "subaccount_ids": [9]
    }
  }
  ```
</CodeGroup>

## IP whitelist

If a key's whitelist is non-empty, [login](/authentication/session-login) from any other IP is rejected. An empty whitelist imposes no IP restriction.

Adding or removing IP Whitelists requires the owner or an admin-scoped key.

## Editing a session key

* Protocol Scopes: call the same `private/set_session_key` endpoint or onchain `Set Session Key` (see [Smart Contract & Multi-sig Accounts](/authentication/contract-owned-accounts)) action again for the session key you want to modify.
* Offchain Scopes and metadata: call `private/edit_session_key` to update the label, IP whitelist, or off-chain scopes. This does not require a signed action. **IP whitelist or off-chain scopes** requires protocol `admin` scope or the owner.

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

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

  // Off-chain patch: omitted fields are left unchanged. Editing ip_whitelist or
  // offchain_scopes requires the owner or an admin-scoped key.
  const updated = await client.sessionKeys.edit({
    publicSessionKey: '0x9f00...beef',
    label: 'market-maker-primary',
    ipWhitelist: ['203.0.113.7'],
  });

  console.log(updated.label, updated.ip_whitelist);
  await client.close();

  ```

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

  from derive_py import WebSocketClient


  async def main():
      client = WebSocketClient.from_env()
      await client.connect()

      # Off-chain patch: omitted fields are left unchanged. Editing ip_whitelist or
      # offchain_scopes requires the owner or an admin-scoped key.
      updated = await client.account.edit_session_key(
          public_session_key="0x9f00...beef",
          label="market-maker-primary",
          ip_whitelist=["203.0.113.7"],
      )
      print(updated.label, updated.ip_whitelist)

      await client.disconnect()


  asyncio.run(main())
  ```

  ```rust Rust (SDK) theme={null}
  use derive_rs::{Environment, WsClient, models::EditSessionKeyRequest};

  #[tokio::main]
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
      let client = WsClient::new(
          Environment::Mainnet,
          Some(std::env::var("DERIVE_PRIVATE_KEY")?),
          Some(std::env::var("DERIVE_WALLET")?),
          Some(9),
      )
      .await?;
      client.login().await?;

      // Off-chain patch, no signing: omitted fields are left unchanged. Editing
      // ip_whitelist or offchain_scopes requires the owner or an admin-scoped key.
      let params = EditSessionKeyRequest::builder()
          .wallet(std::env::var("DERIVE_WALLET")?)
          .public_session_key("0x9f00...beef")
          .label("market-maker-primary".to_string())
          .ip_whitelist(vec!["203.0.113.7".to_string()])
          .try_into()?;

      let updated = client.rpc().session_keys().edit_session_key(params).await?;
      println!("{:?} {:?}", updated.label, updated.ip_whitelist);
      Ok(())
  }
  ```

  ```bash cURL theme={null}
  # Off-chain patch (no signed action); X-Derive* headers: session auth (see /json-rpc).
  curl -X POST https://api.derive.xyz/v3/private/edit_session_key \
    -H "Content-Type: application/json" \
    -H "X-DeriveWallet: $WALLET_ADDRESS" \
    -H "X-DeriveTimestamp: $TS" \
    -H "X-DeriveSignature: $SIG" \
    -d '{
      "wallet": "0x1234...abcd",
      "public_session_key": "0x9f00...beef",
      "label": "market-maker-primary",
      "ip_whitelist": ["203.0.113.7"]
    }'
  ```

  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": 2,
    "method": "private/edit_session_key",
    "params": {
      "wallet": "0x1234...abcd",
      "public_session_key": "0x9f00...beef",
      "label": "market-maker-primary",
      "ip_whitelist": ["203.0.113.7"]
    }
  }
  ```
</CodeGroup>

## Lifecycle at a glance

<Steps>
  <Step title="Create">
    Wallet (or a parent key with `set_session_key` scope) signs a create
    action granting a subset of its own scopes, expiry, and subaccounts.

    Can be done both through API via `private/set_session_key` or on-chain
    via the `Set Session Key` action (see [Smart Contract & Multi-sig Accounts](/authentication/contract-owned-accounts)).
  </Step>

  <Step title="Authenticate">
    The session key logs in via EIP-191 [session
    login](/authentication/session-login); its IP whitelist and expiry are
    enforced at this step.
  </Step>

  <Step title="Sign actions">
    The key EIP-712-signs each action; the protocol re-checks that the key's
    scopes cover the action (see [Action
    signing](/authentication/action-signing)).
  </Step>

  <Step title="Edit metadata">
    Adjust label, IP whitelist, or off-chain scopes with `edit_session_key`.
    Protocol scopes require a fresh `set_session_key`.
  </Step>

  <Step title="Expire">
    The key stops working once `expiry_sec` passes. You can "revoke" session keys by bringing the expiry closer to now.
    However, there is a minimum cooldown of 5-15min.
  </Step>
</Steps>

## Related

<CardGroup cols={2}>
  <Card title="Access scopes" icon="key" href="/authentication/access-scopes">
    The full protocol and off-chain scope catalog and per-route gating.
  </Card>

  <Card title="Action signing" icon="signature" href="/authentication/action-signing">
    How the create action (and every trading action) is signed with EIP-712.
  </Card>

  <Card title="Smart Contract & Multi-sig Accounts" icon="building-columns" href="/authentication/contract-owned-accounts">
    Authorize a session key from L1 when the account owner is a contract or multi-sig.
  </Card>
</CardGroup>


## Related topics

- [Migration skill for your coding agent](/migrating/breaking-changes.md)
- [private/session_keys](/api-reference/session-keys/privatesession_keys.md)
- [private/edit_session_key](/api-reference/session-keys/privateedit_session_key.md)
