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

# Action Signing

> The EIP-712 scheme every state-changing action must carry.

The easiest way to begin signing is to either use the SDKs directly.

* [TypeScript SDK](github.com/derivexyz/derive-ts)
* [Rust SDK](github.com/derivexyz/derive-rs)
* [Python SDK](github.com/derivexyz/derive-py)

Continue reading for a deep dive into the signing scheme, its parameters, and how to debug signature issues.

### Why sign actions?

Every state-changing action — placing an order, transferring
spot, withdrawing, creating a session key — carries its own **EIP-712** signature
over an `Action` struct. This ensures the protocol is self-custodial.

<Note>
  Action signing is **not** [session login](/authentication/session-login).
  Login (EIP-191) authenticates a connection; it does not authorize actions.
  Each action is signed and verified on its own. Keep the two layers distinct.
</Note>

## Debugging signature issues

If a signature is rejected, use the signing-preview helpers to byte-compare
each stage (encoded data, hashes, digest) against what the server computes. Each
returns the `encoded_data`, `action_hash`, and `typed_data_hash` of the rebuilt
action. These are a debugging aid, not a required step:

* `private/order_debug`
* `private/transfer_positions_debug`
* `private/transfer_spot_debug`
* `private/transfer_spot_external_debug`
* `private/update_whitelisted_recipients_debug`
* `private/set_session_key_debug`
* `public/send_quote_debug`
* `public/execute_quote_debug`
* `public/withdraw_debug`

## Constants

The `module` field names the contract whose ABI layout `data` follows. Module
addresses are fixed protocol constants — identical across every deployment, safe
to hardcode:

| Action                  | Module                         | Address                                      | Purpose                                           |
| ----------------------- | ------------------------------ | -------------------------------------------- | ------------------------------------------------- |
| Order / trade           | `TRADE_MODULE`                 | `0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b` | Place an order                                    |
| Spot transfer           | `TRANSFER_MODULE`              | `0x01259207A40925b794C8ac320456F7F6c8FE2636` | Move a spot ERC-20 between subaccounts            |
| Withdraw                | `WITHDRAW_MODULE`              | `0x9d0E8f5b25384C7310CB8C6aE32C8fbeb645d083` | Withdraw to L1                                    |
| RFQ / position transfer | `RFQ_MODULE`                   | `0x9371352CCef6f5b36EfDFE90942fFE622Ab77F1D` | RFQ quotes; position transfers book as RFQ trades |
| External transfer       | `EXTERNAL_TRANSFER_MODULE`     | `0x8F9B8f12ddA05FB1F0DDDDe8f5af8cECF54f8aC9` | External spot transfer                            |
| Whitelisted recipient   | `WHITELISTED_RECIPIENT_MODULE` | `0xB86D6DE1b76c9839e4BA860848CD98A1dABd6B54` | Recipient allow-list                              |
| Liquidation             | `LIQUIDATION_MODULE`           | `0x66d23e59DaEEF13904eFA2D4B8658aeD05f59a92` | Bid into a Dutch auction                          |
| Vault                   | `VAULT_MODULE`                 | `0x2885c174ebf5524aED9c721d60c12b1537685186` | Vault actions                                     |
| Set session key         | `SET_SESSION_KEY_MODULE`       | `0xe330CF64ff6EbF41699aad344Cb21d78db1D2bb6` | Register a delegated session key                  |

### owner vs signer

`owner` is always the wallet that owns the subaccount. `signer` is whoever
produced the signature — the wallet itself (`owner == signer`) or a delegated
[session key](/authentication/session-keys), in which case `signer` is the key's address and
`owner` stays the wallet. The protocol recovers the ECDSA signer and requires it
to equal `signer`; if `signer != owner`, it loads the session key and checks its
scopes cover the action.

## Nonce and expiry

<ParamField path="nonce" type="uint256">
  The `nonce` param (decoded as a UTC timestamp in nanoseconds \* 6-digit suffix) has some special rules depending on the action.
</ParamField>

| Action type                                                              | Nonce window (relative to server clock) | Must increase? |
| ------------------------------------------------------------------------ | --------------------------------------- | -------------- |
| Signed actions — withdraw, transfer, session key, whitelist, liquidation | ± 1 hour                                | Yes            |
| Orders                                                                   | 120 days before → 1 hour after          | No             |
| RFQs                                                                     | ± 1 hour                                | No             |
| Vault actions                                                            | 60 days before → 1 hour after           | Yes            |

<ParamField path="expiry" type="uint256">
  The Action's `expiry`, in unix seconds; rejected once `now > expiry`. The exchange enforces certain minimums depending on the action type to ensure the action is valid long enough.
</ParamField>

| Action type                                                        | Minimum expiry (from server clock)                                              | Maximum expiry (from server clock)           |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------- | -------------------------------------------- |
| Orders                                                             | 10 seconds                                                                      | 120 days — or 15 minutes if the order is MMP |
| RFQ quotes — maker quote and taker execute                         | 60 seconds past the RFQ's expiry (an RFQ is valid for 10 minutes from creation) | 1 day                                        |
| Position transfers                                                 | 60 seconds (both sides; the taker's expiry must also cover the maker's)         | —                                            |
| Withdraw, spot transfer, external transfer, liquidation, whitelist | None — must simply be unexpired when verified                                   | —                                            |
| Vault actions                                                      | None — must simply be unexpired when verified                                   | 30 days                                      |
| Set session key                                                    | None on the signature — but the key's own `expiry_sec` must be ≥ 5 minutes      | —                                            |

## Worked example: a buy order

An option buy order signed by the wallet directly (`owner == signer`). The SDK
ABI-encodes the payload, builds and signs the EIP-712 digest, and submits it in a
single call — amounts and prices are plain decimals, with e18 scaling handled
internally:

<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();

  // Signed by the wallet key held in the client (owner == signer). The nonce,
  // signature, and signature_expiry_sec are generated and signed for you.
  const { order } = await client.orders.place({
    subaccountId: 9,
    instrumentName: 'ETH-20260626-3000-C',
    direction: 'buy',
    amount: '1',
    limitPrice: '310',
    maxFee: '0.01',
  });
  console.log(order.order_id);

  ```

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

  from derive_py import WebSocketClient
  from derive_py.data_types import Direction


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

      # Signed by the key held in the client. The nonce, signature, and
      # signature_expiry_sec are generated and signed for you.
      response = await client.orders.create(
          instrument_name="ETH-20260626-3000-C",
          direction=Direction.buy,
          amount=Decimal("1"),
          limit_price=Decimal("310"),
          max_fee=Decimal("0.01"),
      )
      print(response.order.order_id)

      await client.disconnect()


  asyncio.run(main())
  ```

  ```rust Rust (SDK) theme={null}
  use bigdecimal::BigDecimal;
  use derive_rs::{
      Environment, WsClient,
      actions::OrderArgs,
      models::{Direction, OrderType, TimeInForce},
  };

  #[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?;

      // Signed by the wallet key held in the client (owner == signer). The nonce,
      // signature, and signature_expiry_sec are generated and signed for you.
      let order = OrderArgs::builder()
          .instrument_name("ETH-20260626-3000-C".to_string())
          .direction(Direction::Buy)
          .order_type(OrderType::Limit)
          .time_in_force(TimeInForce::Gtc)
          .amount(BigDecimal::from(1))
          .limit_price(BigDecimal::from(310))
          .build();

      let response = client.orders().place(order).await?;
      println!("{}", response.order.order_id);
      Ok(())
  }
  ```
</CodeGroup>

<Info>
  The instrument name above is an option, named `<CURRENCY>-<YYYYMMDD>-<STRIKE>-<C|P>` (perps are `<CURRENCY>-PERP`).
  See [Instrument names](/trading/instrument-names) for the full grammar.
</Info>


## Related topics

- [Introduction](/getting-started/introduction.md)
- [Contracts](/getting-started/contracts.md)
- [Quickstart](/getting-started/quickstart.md)
