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

# Action Signing

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

Every state-changing action — placing an order, transferring
spot, withdrawing, creating a session key — carries its own **EIP-712** signature
over an `Action` struct, re-verified independently by the protocol. This allows the
protocol to be low-latency and self-custodial at the same time.

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

## The Action struct

Every action is wrapped in the same 7-field envelope. This is the EIP-712 typed
structure that gets signed:

```solidity theme={null}
Action(
  uint256 subaccountId,  // subaccount the action applies to; session-key registration uses 0 (wallet-level)
  uint256 nonce,         // anti-replay nonce (see "Nonce and expiry" below)
  address module,        // per-action module contract that decodes `data`; fixed constant, identical across deployments
  bytes   data,          // ABI-encoded, action-specific payload; hashed before it enters the struct hash
  uint256 expiry,        // unix seconds after which the signed action is rejected
  address owner,         // the wallet that owns the subaccount
  address signer         // the address that actually signs — the wallet itself, or 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.

## Building the signature

<Steps>
  <Step title="ABI-encode the action data">
    Encode the module-specific payload into `data` (layouts [below](#per-action-modules-and-data)).
  </Step>

  <Step title="Compute the struct hash">
    `data` is hashed, then the whole struct is ABI-encoded with the type hash:

    ```text theme={null}
    structHash = keccak256(abi.encode(
        ACTION_TYPEHASH,
        subaccountId, nonce, module,
        keccak256(data),          // note: data is hashed here
        expiry, owner, signer
    ))
    ```

    `ACTION_TYPEHASH` is `keccak256` of the struct signature above and is **invariant
    across every deployment**:

    ```text theme={null}
    0x4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17
    ```
  </Step>

  <Step title="Compute the EIP-712 digest">
    Prefix with `0x1901` and the deployment's domain separator:

    ```text theme={null}
    digest = keccak256(0x1901 || domainSeparator || structHash)
    ```

    The domain is the **Matching** contract domain —
    `EIP712Domain(name="Matching", version="1.0", chainId, verifyingContract)`. The
    `verifyingContract` is the Matching contract, **constant across every deployment**:

    ```text theme={null}
    verifyingContract = 0xeB8d770ec18DB98Db922E9D83260A585b9F0DeAD
    ```

    Only `chainId` varies per deployment, so the resulting `domainSeparator` is
    **deployment-specific** and must match the deployment's on-chain `ActionEip712Params`:

    | Environment | Chain            | `chainId`  |
    | ----------- | ---------------- | ---------- |
    | Testnet     | Ethereum Sepolia | `11155111` |
    | Mainnet     | Ethereum mainnet | `1`        |

    The per-action `module` addresses, by contrast, are fixed protocol constants
    (identical on every deployment) — see the [table below](#per-action-modules-and-data).
  </Step>

  <Step title="Sign the digest">
    Sign the 32-byte `digest` directly with the signing key, producing a **65-byte
    `r || s || v`** signature. Submit it with the request's `nonce`, `signer`, and
    `signature` fields.
  </Step>
</Steps>

<Info>
  The testnet domain separator is
  `0x24d674cd5f2b9d564691c51e9d88f649b99246a2244dd74ce27b96578d773e85`. Do not
  treat this as universal — mainnet and other deployments differ. The mainnet
  separator is not published here; read it from the deployment's on-chain
  `ActionEip712Params`, or via the `*_debug` helpers below. Always verify
  against your target.
</Info>

## Per-action modules and data

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                              |
| Vault                   | `VAULT_MODULE`                 | `0x2885c174ebf5524aED9c721d60c12b1537685186` | Vault actions                                     |
| Create session key      | `CREATE_SESSION_KEY_MODULE`    | `0xe330CF64ff6EbF41699aad344Cb21d78db1D2bb6` | Register a delegated session key                  |

<Warning>
  The `module` is a **field inside the Action struct**, not the domain's
  `verifyingContract`. The `verifyingContract` is always the single Matching
  contract; `module` selects the per-action payload contract. Signing against
  the wrong `module` yields a valid-looking but rejected signature.
</Warning>

### Amounts are e18 on the wire

Numeric amounts, prices, and fees are **e18 fixed-point** in the ABI-encoded
`data` — i.e. signers sign `parseUnits(x, 18)`. Withdrawal's underlying amount is
the exception: it uses the asset's **native ERC-20 decimals** and is never scaled.

The exact ABI word layout for each module's `data` is defined by the SDK codecs —
use them as the source of truth rather than hand-encoding the payload:

| SDK        | Action-`data` encoders                                                                                                                                                                                                                                                                                                                                         |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| TypeScript | [`src/codecs/`](https://github.com/derivexyz/derive-ts/tree/main/src/codecs) — e.g. [`trade.ts`](https://github.com/derivexyz/derive-ts/blob/main/src/codecs/trade.ts), [`transfer.ts`](https://github.com/derivexyz/derive-ts/blob/main/src/codecs/transfer.ts), [`withdrawal.ts`](https://github.com/derivexyz/derive-ts/blob/main/src/codecs/withdrawal.ts) |
| Python     | Coming soon                                                                                                                                                                                                                                                                                                                                                    |
| Rust       | Coming soon                                                                                                                                                                                                                                                                                                                                                    |

See [Transfers & withdrawals](/trading/transfers-withdrawals) for the full request flows.

## Nonce and expiry

<ParamField path="nonce" type="uint256">
  A **UTC timestamp in nanoseconds** (\~19 digits, e.g. `1751558400000000123`). A
  common convention is `now_ms × 1_000_000 + random 6-digit suffix`, producing a
  time-ordered value; the suffix keeps concurrent actions unique within the same
  millisecond. A nanosecond timestamp exceeds JavaScript's safe-integer range,
  so build it with `BigInt` and submit it as a string (the API accepts a string
  or a number). For **non-trade** signed actions the nonce must be **strictly
  increasing per subaccount** (monotonic anti-replay); **trades and RFQs** are
  tracked per `(owner, nonce)`, so partial fills accumulate against a single
  nonce and need not increase.
</ParamField>

The nonce must also fall inside a validity window, which differs by action type:

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

Windows are environment-tunable; the server also applies a coarser ± 5-minute pre-check before the action reaches the
protocol.

<ParamField path="expiry" type="uint256">
  The Action's `expiry`, in unix seconds; rejected once `now > expiry`. It must also sit at least the deployment's
  **minimum signature-validity window** in the future (`MIN_ORDER_SIGNATURE_VALIDITY_SEC`, deployment-specific), or the
  order is rejected with [`11011 order_invalid_signature_expiry`](/error-codes) — leave a comfortable margin (e.g.
  orders `now + 3600`, `create_session_key` `now + 600`).
</ParamField>

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

Whichever transport you use, this is the `private/order` request the SDK puts on
the wire:

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

  ```rust Rust (SDK) theme={null}
  // Coming soon.
  ```
</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>

## Verify your bytes

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` (orders) — takes the same params as `private/order`; needs a logged-in session
* `public/withdraw_debug`
* `public/send_quote_debug` and `public/execute_quote_debug`

<CardGroup cols={2}>
  <Card title="Session keys" href="/authentication/session-keys">
    Delegate signing to a session key and manage its scopes.
  </Card>

  <Card title="Transfers & withdrawals" href="/trading/transfers-withdrawals">
    Full flows for the transfer and withdraw modules.
  </Card>

  <Card title="Contracts" href="/getting-started/contracts">
    On-chain contract addresses per deployment (action manager, vApp, outbox,
    spot vault).
  </Card>

  <Card title="Authentication" href="/authentication/session-login">
    The session-login layer (EIP-191).
  </Card>
</CardGroup>


## Related topics

- [Session Keys](/authentication/session-keys.md)
- [Introduction](/getting-started/introduction.md)
- [Contracts](/getting-started/contracts.md)
