> ## Documentation Index
> Fetch the complete documentation index at: https://v3.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`, `delete_session_key`). 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 `create_session_key` protocol scope.

## Create a session key

`private/create_session_key` is a **signed action**: your wallet (or a parent session key with `create_session_key` scope) EIP-712-signs a create-session-key action, which you then submit to the API. See [Action signing](/authentication/action-signing) for how to build the `nonce`, `signature`, and `signature_expiry_sec` fields.

<ParamField path="wallet" type="string" required>
  Wallet the session key is registered for (0x-prefixed).
</ParamField>

<ParamField path="public_session_key" type="string" required>
  The delegated EVM address being authorized (0x-prefixed).
</ParamField>

<ParamField path="expiry_sec" type="integer" required>
  Unix seconds (UTC) after which the key is expired. Must not exceed the
  parent's expiry.
</ParamField>

<ParamField path="subaccount_ids" type="integer[]">
  Subaccounts this key may act on. **Omit or empty ⇒ all current and future
  subaccounts.** Must be a subset of the parent's subaccounts.
</ParamField>

<ParamField path="protocol_scopes" type="string[]" required>
  Wire-string protocol scopes signed into the action (e.g.
  `"trade:orderbook:all"`, `"withdraw"`). Validated and attenuated in-protocol.
  See [Access scopes](/authentication/access-scopes).
</ParamField>

<ParamField path="offchain_scopes" type="string[]" required>
  Off-chain scopes (`account_info`, `delete_session_key`). Backend-only, not
  signed.
</ParamField>

<ParamField path="label" type="string">
  Human-readable label.
</ParamField>

<ParamField path="ip_whitelist" type="string[]">
  If non-empty, the key may only authenticate from these IPs.
</ParamField>

<ParamField path="nonce" type="string" required>
  Strictly-increasing anti-replay nonce (see [Action
  signing](/authentication/action-signing)).
</ParamField>

<ParamField path="signer" type="string" required>
  Address that produced `signature` — the wallet or a parent session key.
</ParamField>

<ParamField path="signature" type="string" required>
  0x-prefixed 65-byte `r||s||v` EIP-712 signature over the create action.
</ParamField>

<ParamField path="signature_expiry_sec" type="integer" required>
  Unix seconds after which the signed action is rejected. A typical client
  default is `now + 600` (10 min).
</ParamField>

<ParamField path="module" type="string" required>
  EIP-712 action module address (the `CREATE_SESSION_KEY_MODULE`). Part of the
  signed struct hash — client and server must agree. A fixed protocol constant,
  identical across deployments; see [Action
  signing](/authentication/action-signing#per-action-modules-and-data).
</ParamField>

<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.create({
    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}
  # Coming soon.
  ```

  ```rust Rust (SDK) theme={null}
  // Coming soon.
  ```

  ```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/create_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/create_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>

<Info>
  `module` is a fixed protocol constant (identical across deployments), but it
  must still match exactly or the signature will be rejected. Verify your
  encoding with the signing-preview helpers described in [Action
  signing](/authentication/action-signing).
</Info>

## Edit a session key

`private/edit_session_key` updates **off-chain fields only**: `label`, `ip_whitelist`, and `offchain_scopes`. It requires no re-signing.

<Warning>
  **Protocol scopes are not editable.** To change a key's on-chain authority,
  mint a new key with `create_session_key`. This edit method only touches
  off-chain metadata.
</Warning>

Authorization depends on what you change:

* **Label only** → requires the off-chain `account_info` scope.
* **IP whitelist or off-chain scopes** → requires protocol `admin` (or the owning wallet).

<ParamField path="wallet" type="string" required>
  Wallet the key belongs to.
</ParamField>

<ParamField path="public_session_key" type="string" required>
  The session key address to edit.
</ParamField>

<ParamField path="label" type="string">
  New label.
</ParamField>

<ParamField path="ip_whitelist" type="string[]">
  Replacement IP whitelist. Empty ⇒ no IP restriction.
</ParamField>

<ParamField path="offchain_scopes" type="string[]">
  Replacement off-chain scopes.
</ParamField>

<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}
  # Coming soon.
  ```

  ```rust Rust (SDK) theme={null}
  // Coming soon.
  ```

  ```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>

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

## Lifecycle at a glance

<Steps>
  <Step title="Create">
    Wallet (or a parent key with `create_session_key` scope) signs a create
    action granting a subset of its own scopes, expiry, and subaccounts.
  </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 `create_session_key`.
  </Step>

  <Step title="Expire">The key stops working once `expiry_sec` passes.</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>
</CardGroup>


## Related topics

- [Migration skill for your coding agent](/migrating/breaking-changes.md)
- [List a wallet's session keys](/api-reference/session-keys/list-a-wallets-session-keys.md)
- [List wallets for a session key](/api-reference/session-keys/list-wallets-for-a-session-key.md)
