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

# Signing

<div className="api-reference-page" aria-hidden="true" />

Signing proves that a wallet authorized a request and binds the signature to its contents.
Notional uses EIP-712: sign locally, then send the signature with the request.

Signatures are required for trading, API-wallet approvals, withdrawals, protocol transfers, and
Polymarket deposit reports. Public info requests, WebSocket subscriptions, and `reportDeposit`
requests are unsigned.

## Signing Formats

The signing format depends on the request:

* **Trading actions** use `NotionalExchange`. Sign an `Agent` message containing the action hash,
  using chain ID `1337` and `source: "a"` for mainnet or `"b"` for testnet. The account owner or an
  approved API wallet can sign.
* **API-wallet approvals and withdrawals** use `NotionalSignTransaction`. The account owner signs
  the [approval](#api-wallet-approvals) or [withdrawal](#withdrawals) typed message using the wallet chain ID.
* **Protocol transfers** use `NotionalExchange` and require the account owner's signature. Supply
  the wallet chain ID in `signatureChainId`; omitting it uses chain ID `1337`. See the [transfer example](#protocol-transfers).
* **Polymarket deposit reports** use `NotionalPolymarketDeposit`, with a domain bound to the deposit
  recipient and Notional deployment. Follow the
  [deposit signing instructions](#polymarket-deposit-reports).
* **Isolated operation status reads** use `NotionalSignTransaction` with the wallet chain ID.
  Sign the user address, operation ID, and timestamp as shown in
  [Get isolated operation status](/api-reference/info-endpoints/isolated-operation-status#signing).

Vault addresses are unsupported. See the request schemas for action fields and
[Nonces and API wallets](/api-reference/nonces-and-api-wallets) for replay protection and approvals.

## Account routing

For orders, cancels, modifications, leverage and isolated-margin updates, TWAP actions, and
Hyperliquid outcome merges, include optional `expectedAccount: "0x..."` alongside `action`,
`nonce`, and `signature` to reject requests routed to a different account.

Protocol transfers and version 2 Polymarket split/merge requests reject this field. API-wallet
approvals, withdrawals, and Polymarket deposit reports do not enforce it.

## Signature object

Requests that use an object for `signature` expect these fields:

<ResponseField name="signature" type="object">
  Request signature.

  <Expandable title="child attributes">
    <ResponseField name="r" type="string" required>
      ECDSA `r` component as a `0x`-prefixed 32-byte hex string.
    </ResponseField>

    <ResponseField name="s" type="string" required>
      ECDSA `s` component as a `0x`-prefixed 32-byte hex string.
    </ResponseField>

    <ResponseField name="v" type="number" required>
      ECDSA recovery value, for example `27`.
    </ResponseField>
  </Expandable>
</ResponseField>

## Connect a Wallet

The examples below use public npm packages: `viem` for EIP-712 signing and signature parsing,
and `@msgpack/msgpack` for trading and protocol-transfer action encoding.

```bash theme={null}
npm install viem@2.39.0 @msgpack/msgpack@3.1.2
```

For browser signing, pass your wallet integration's EIP-1193 provider to `connectWallet`. For
example, an injected wallet exposes this provider as `window.ethereum`.

```typescript theme={null}
import { createWalletClient, custom, type EIP1193Provider } from "viem";

export async function connectWallet(provider: EIP1193Provider) {
  const walletClient = createWalletClient({ transport: custom(provider) });
  await walletClient.requestAddresses();
  return walletClient;
}
```

Pass the returned `walletClient` to a signing function below. Those functions return a
JSON-ready body without submitting it. Allocate a unique nonce for each exchange request using
the [nonce rules](/api-reference/nonces-and-api-wallets#notional-nonces).

The request functions also accept a [Viem wallet client with a local account](https://viem.sh/docs/clients/wallet#local-accounts-private-key-mnemonic-etc).
They preserve `walletClient.account` so private-key signing happens locally. For the order example
below, pass the API wallet's private key directly to `privateKeyToAccount` from `viem/accounts`.

## Sign an Order

Pass an approved API wallet's private key, the market ID returned by
[metadata](/api-reference/products-and-asset-ids), and a unique Unix-millisecond nonce.
This example uses normalized price and size strings and omits `expiresAfter`.

```typescript theme={null}
import { encode } from "@msgpack/msgpack";
import { concat, keccak256, parseSignature, toBytes, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";

export async function createOrderRequest(
  apiWalletPrivateKey: Hex,
  assetId: string,
  nonce: number,
  network: "mainnet" | "testnet",
) {
  const wallet = privateKeyToAccount(apiWalletPrivateKey);
  const isMainnet = network === "mainnet";
  // Create a buy order for the selected market.
  const action = {
    type: "order",
    orders: [
      { a: assetId, b: true, p: "50000", s: "0.01", r: false, t: { limit: { tif: "Gtc" } } },
    ],
    grouping: "na",
  };
  // connectionId is a fingerprint of the order and its request number.
  // The signature will approve these details together.
  const connectionId = keccak256(
    concat([
      encode(action, { ignoreUndefined: true }), // Keep the fields in the order shown.
      toBytes(BigInt(nonce), { size: 8 }), // Include the request number.
      new Uint8Array([0]),
    ]),
  );
  // Use the wallet's key to approve the order details.
  const signature = await wallet.signTypedData({
    domain: {
      name: "NotionalExchange",
      version: "1",
      chainId: 1337,
      verifyingContract: "0x0000000000000000000000000000000000000000",
    },
    types: {
      Agent: [
        { name: "source", type: "string" },
        { name: "connectionId", type: "bytes32" },
      ],
    },
    primaryType: "Agent",
    message: { source: isMainnet ? "a" : "b", connectionId },
  });
  // parseSignature splits the wallet's signature into the parts Notional needs:
  // r and s hold the signature data; yParity helps identify which wallet signed it.
  const { r, s, yParity } = parseSignature(signature);
  // Package the order and signature in the format Notional expects.
  return { action, nonce, signature: { r, s, v: 27 + yParity } };
}
```

## API Wallet Approvals

The account owner signs `NotionalTransaction:ApproveAgent` in the `NotionalSignTransaction`
domain, version `1`, using the wallet chain ID and zero address as `verifyingContract`.
The signed `notionalChain` matches `action.hyperliquidChain`; the other signed fields are
`agentAddress`, `agentName`, and `nonce`. Use the zero address and existing wallet name to revoke.

<Accordion title="Signing example">
  ```typescript theme={null}
  import { parseSignature, zeroAddress, type Address, type WalletClient } from "viem";

  export async function createApprovalRequest(
    walletClient: WalletClient,
    params: {
      network: "mainnet" | "testnet";
      agentAddress: Address;
      agentName?: string;
      nonce: number;
    },
  ) {
    const account = walletClient.account ?? (await walletClient.getAddresses())[0];
    if (!account) throw new Error("Connect the account owner's wallet.");
    const chainId = await walletClient.getChainId();
    const notionalChain = params.network === "mainnet" ? "Mainnet" : "Testnet";
    const agentAddress = params.agentAddress.toLowerCase() as Address;
    const agentName = (params.agentName ?? "").trim();
    const signature = await walletClient.signTypedData({
      account,
      domain: {
        name: "NotionalSignTransaction",
        version: "1",
        chainId,
        verifyingContract: zeroAddress,
      },
      types: {
        "NotionalTransaction:ApproveAgent": [
          { name: "notionalChain", type: "string" },
          { name: "agentAddress", type: "address" },
          { name: "agentName", type: "string" },
          { name: "nonce", type: "uint64" },
        ],
      },
      primaryType: "NotionalTransaction:ApproveAgent",
      message: { notionalChain, agentAddress, agentName, nonce: BigInt(params.nonce) },
    });
    if ((await walletClient.getChainId()) !== chainId) {
      throw new Error("The wallet network changed. Sign again on the current network.");
    }
    const { r, s, yParity } = parseSignature(signature);
    return {
      action: {
        type: "approveAgent",
        hyperliquidChain: notionalChain,
        signatureChainId: `0x${chainId.toString(16)}`,
        agentAddress,
        agentName,
        nonce: params.nonce,
      },
      nonce: params.nonce,
      signature: { r, s, v: 27 + yParity },
    };
  }
  ```
</Accordion>

## Withdrawals

The account owner signs `NotionalTransaction:Withdraw` in the `NotionalSignTransaction`
domain, version `1`, using the wallet chain ID and zero address as `verifyingContract`.
Signed fields are `notionalChain`, `destination`, `token`, `amount`, `source`, and `time`.
Keep the exact amount string, including its formatting, unchanged after signing.

<Accordion title="Signing example">
  ```typescript theme={null}
  import { parseSignature, zeroAddress, type Address, type WalletClient } from "viem";

  export async function createWithdrawalRequest(
    walletClient: WalletClient,
    params: {
      network: "mainnet" | "testnet";
      destination: Address;
      token: string;
      amount: string;
      source: "balance" | "segregated";
      nonce: number;
    },
  ) {
    const account = walletClient.account ?? (await walletClient.getAddresses())[0];
    if (!account) throw new Error("Connect the account owner's wallet.");
    const chainId = await walletClient.getChainId();
    const message = {
      notionalChain: params.network === "mainnet" ? "Mainnet" : "Testnet",
      destination: params.destination.toLowerCase(),
      token: params.token,
      amount: params.amount,
      source: params.source,
      time: params.nonce,
    };
    const signature = await walletClient.signTypedData({
      account,
      domain: {
        name: "NotionalSignTransaction",
        version: "1",
        chainId,
        verifyingContract: zeroAddress,
      },
      types: {
        "NotionalTransaction:Withdraw": [
          { name: "notionalChain", type: "string" },
          { name: "destination", type: "string" },
          { name: "token", type: "string" },
          { name: "amount", type: "string" },
          { name: "source", type: "string" },
          { name: "time", type: "uint64" },
        ],
      },
      primaryType: "NotionalTransaction:Withdraw",
      message: { ...message, time: BigInt(message.time) },
    });
    if ((await walletClient.getChainId()) !== chainId) {
      throw new Error("The wallet network changed. Sign again on the current network.");
    }
    const { r, s, yParity } = parseSignature(signature);
    return {
      action: { type: "withdraw", ...message, signatureChainId: `0x${chainId.toString(16)}` },
      nonce: message.time,
      signature: { r, s, v: 27 + yParity },
    };
  }
  ```
</Accordion>

## Protocol Transfers

The account in `quote.terms.userAddress` signs the complete action using `NotionalExchange`.
Retain every authenticated quote field and its order. Normalize numeric strings only in the
action hash; submit the quote unchanged. Include the signed `expiresAfter` in the action hash
and submit before that deadline.

<Accordion title="Signing example">
  ```typescript theme={null}
  import { encode } from "@msgpack/msgpack";
  import { concat, keccak256, parseSignature, toBytes, zeroAddress, type WalletClient } from "viem";

  // Only the fields read by this example are named; retain the full response, including its mac.
  type TransferQuote = {
    terms: { userAddress: string; expiresAt: number; [key: string]: unknown };
    mac: `0x${string}`;
  };

  // Normalize numeric strings for hashing only. Do not change the authenticated quote itself.
  function normalizeForHash(value: unknown): unknown {
    if (typeof value === "string" && /^-?\d+\.?\d*$/.test(value)) {
      return value.includes(".") ? value.replace(/\.?0+$/, "") || "0" : value;
    }
    if (Array.isArray(value)) return value.map(normalizeForHash);
    if (value && typeof value === "object") {
      return Object.fromEntries(
        Object.entries(value).map(([key, item]) => [key, normalizeForHash(item)]),
      );
    }
    return value;
  }

  export async function createProtocolTransferRequest(
    walletClient: WalletClient,
    quote: TransferQuote,
    nonce: number,
    network: "mainnet" | "testnet",
  ) {
    const account = walletClient.account ?? (await walletClient.getAddresses())[0];
    if (!account) throw new Error("Connect the account owner's wallet.");
    const signerAddress = typeof account === "string" ? account : account.address;
    if (signerAddress.toLowerCase() !== quote.terms.userAddress.toLowerCase()) {
      throw new Error("Connect the account owner's wallet.");
    }
    const chainId = await walletClient.getChainId();
    const expiresAfter = Math.min(Date.now() + 30_000, quote.terms.expiresAt);
    if (expiresAfter <= Date.now()) throw new Error("Get a new quote before signing.");
    const action = {
      type: "executeProtocolTransfer",
      terms: { kind: "quoted", quote },
    } as const;
    const connectionId = keccak256(
      concat([
        encode(normalizeForHash(action), { ignoreUndefined: true }),
        toBytes(BigInt(nonce), { size: 8 }),
        new Uint8Array([0]), // No vault address.
        new Uint8Array([0]), // Expiry marker, present only when expiresAfter is supplied.
        toBytes(BigInt(expiresAfter), { size: 8 }),
      ]),
    );
    const signed = await walletClient.signTypedData({
      account,
      domain: {
        name: "NotionalExchange",
        version: "1",
        chainId,
        verifyingContract: zeroAddress,
      },
      types: {
        Agent: [
          { name: "source", type: "string" },
          { name: "connectionId", type: "bytes32" },
        ],
      },
      primaryType: "Agent",
      message: { source: network === "mainnet" ? "a" : "b", connectionId },
    });
    if ((await walletClient.getChainId()) !== chainId) {
      throw new Error("The wallet network changed. Sign again on the current network.");
    }
    if (expiresAfter <= Date.now()) throw new Error("Get a new quote before submitting.");
    const { r, s, yParity } = parseSignature(signed);
    return {
      action,
      nonce,
      signatureChainId: `0x${chainId.toString(16)}`,
      expiresAfter,
      signature: { r, s, v: 27 + yParity },
    };
  }
  ```
</Accordion>

## Polymarket Deposit Reports

The source user or an approved API wallet signs `PolymarketDepositIntent` with one field:
`transactionHash: bytes32`. The domain is `NotionalPolymarketDeposit`, version `1`, chain ID `137`,
with the actual deposit recipient as `verifyingContract`. Its salt is the Keccak-256 hash of
the UTF-8 string `notional:mainnet` or `notional:testnet`, matching the deployment.
The endpoint accepts the full hex signature directly.

<Accordion title="Signing example">
  ```typescript theme={null}
  import { keccak256, stringToHex, type Address, type Hex, type WalletClient } from "viem";

  export async function createPolymarketDepositRequest(
    walletClient: WalletClient,
    params: {
      network: "mainnet" | "testnet";
      recipient: Address;
      polygonTxHash: Hex;
    },
  ) {
    const account = walletClient.account ?? (await walletClient.getAddresses())[0];
    if (!account) throw new Error("Connect the source user's wallet or an approved API wallet.");
    const recipient = params.recipient.toLowerCase() as Address;
    const polygonTxHash = params.polygonTxHash.toLowerCase() as Hex;
    const signature = await walletClient.signTypedData({
      account,
      domain: {
        name: "NotionalPolymarketDeposit",
        version: "1",
        chainId: 137,
        verifyingContract: recipient,
        salt: keccak256(stringToHex(`notional:${params.network}`)),
      },
      types: {
        PolymarketDepositIntent: [{ name: "transactionHash", type: "bytes32" }],
      },
      primaryType: "PolymarketDepositIntent",
      message: { transactionHash: polygonTxHash },
    });
    return {
      action: { type: "reportPolymarketDeposit", polygonTxHash },
      signature,
    };
  }
  ```
</Accordion>

## Submit a Signed Request

Use the API URL for the selected Notional deployment and submit the exact body returned by the
signing example. Exchange writes use `/exchange`; signed status reads use `/info`.

```typescript theme={null}
export async function submitSignedRequest(
  apiUrl: string,
  endpoint: "/exchange" | "/info",
  request: unknown,
) {
  const response = await fetch(new URL(endpoint, apiUrl), {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(request),
  });
  const body: unknown = await response.json();
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${JSON.stringify(body)}`);
  return body;
}
```

Also inspect the response body: an HTTP `200` can contain a rejected action or a pending
operation. Follow each endpoint's response and retry rules.

## Request Deadlines

For endpoints that accept it, `expiresAfter` must be a nonnegative safe integer in Unix
milliseconds. Sign and submit the same value. The deadline is inclusive: a request is rejected
when server time is greater than the deadline, including exact retries. Requests that expire
while queued are rejected before state changes. Protocol transfers must arrive before their
deadline. A request deadline does not set the lifetime of an already accepted resting order or
TWAP. Version 2 outcome requests omit `expiresAfter`.

## Common Signing Mistakes

An incorrect signature or a mismatch between the signed and submitted payload can cause the server
to recover a different signer. An order may then be rejected with an error such as:

```text theme={null}
Insufficient margin: account_value=0 < required_initial_margin=...
```

Here, `account_value=0` can refer to the incorrectly recovered account, even if your signing account
has collateral. When debugging, compare the signer recovered using the endpoint's expected format
with your signing wallet's address. Different mismatched inputs can recover different addresses;
the error itself does not identify the signing mistake.

Common causes include:

1. Using the wrong signing scheme, domain, chain ID, or network value.
2. Changing MessagePack field order or value types; decimal amounts must remain strings.
3. Mishandling trailing fractional zeroes; exchange action hashing normalizes decimal strings.
4. Changing address casing; lowercase address strings before signing and submitting.
5. Changing the action, nonce, or `expiresAfter` after signing.
6. Assuming local signer recovery proves correctness. Signing and recovery can share the same
   incorrect payload construction; compare the domain, types, message, and encoded bytes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.