Skip to main content

Signature Format

Most exchange actions include a signature and nonce:
{
  "action": { /* action details */ },
  "nonce": 1234567890,
  "expiresAfter": 1234569999,
  "signature": {
    "r": "0x1234...",
    "s": "0x5678...",
    "v": 27
  }
}
  • Unsigned actions: reportDeposit
  • Action-scoped EIP-712 signatures: acceptTerms, generateReferralCode (signature is a hex string inside action)

Nonce

The nonce is a timestamp in milliseconds used to prevent replay attacks:
const nonce = Date.now();
Each nonce can only be used once. Requests with duplicate nonces will be rejected.

Signature Components

The signature consists of three components (standard ECDSA):
  • r - First 32 bytes of signature (hex string)
  • s - Second 32 bytes of signature (hex string)
  • v - Recovery ID (27 or 28)

Creating Signatures

1. Construct the Action Hash

Hash the action payload using EIP-712 structured data signing:
import { ethers } from 'ethers';

const domain = {
  name: 'Notional',
  version: '1',
  chainId: 42161
};

const types = {
  Order: [
    { name: 'asset', type: 'uint32' },
    { name: 'isBuy', type: 'bool' },
    { name: 'limitPx', type: 'uint64' },
    { name: 'sz', type: 'uint64' },
    { name: 'reduceOnly', type: 'bool' },
    { name: 'orderType', type: 'uint8' }
  ]
};

const action = {
  asset: 0,
  isBuy: true,
  limitPx: '50000.0',
  sz: '1.0',
  reduceOnly: false,
  orderType: 1
};

const signature = await signer._signTypedData(domain, types, action);
Use the Notional SDK for exact typed-data definitions and action normalization.

2. Split the Signature

const sig = ethers.utils.splitSignature(signature);

const request = {
  action: action,
  nonce: Date.now(),
  signature: {
    r: sig.r,
    s: sig.s,
    v: sig.v
  }
};

Agent Approval

Instead of signing each request with your main wallet, you can authorize an API wallet (agent) to sign on your behalf.

Benefits

  • Security: Keep your main wallet offline
  • Convenience: No manual signature approval for each trade
  • Automation: Enable trading bots and automated strategies

Approving an Agent

Send an approveAgent action signed by your main wallet:
{
    "action": {
      "type": "approveAgent",
      "agentAddress": "0x...",
      "agentName": "My Trading Bot",
      "nonce": 1234567890
    },
    "nonce": 1234567890,
  "signature": {
    "r": "0x...",
    "s": "0x...",
    "v": 27
  }
}
Request to:
POST /exchange
Response:
{
  "status": "ok",
  "response": {
    "type": "approveAgent"
  }
}

Using an Approved Agent

Once approved, the agent can sign requests on behalf of the user:
  1. Agent creates the action payload
  2. Agent signs the action with its own private key
  3. Backend validates:
    • Signature is valid for agent address
    • Agent is approved for the user
    • Action is authorized
The userAddress is recovered from the approval record, not the signature.

Validation Process

When a signed request arrives, the backend:
  1. Validates signature structure - Checks r, s, v format
  2. Recovers signer address - Uses ECDSA recovery
  3. Checks authorization:
    • If signer = user → Direct mode (approved)
    • If signer = agent → Agent mode (checks approval table)
  4. Verifies nonce - Ensures uniqueness
  5. Validates action - Checks order parameters, margin, etc.

Error Responses

Invalid Signature

{
  "error": "Invalid signature"
}
HTTP Status: 401 Unauthorized

Agent Not Approved

{
  "error": "Agent not approved for user"
}
HTTP Status: 401 Unauthorized

Duplicate Nonce

{
  "error": "Nonce already used"
}
HTTP Status: 400 Bad Request

Security Best Practices

  • Never share your private key - Use hardware wallets for main account
  • Limit agent permissions - Only approve agents you trust
  • Monitor agent activity - Review orders placed by agents
  • Rotate agents - Revoke and re-approve agents periodically
  • Use unique nonces - Ensure timestamp-based nonces are unique

Example: Complete Order Flow

import { ethers } from 'ethers';

// 1. Connect wallet
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();

// 2. Create order action
const action = {
  type: 'order',
  orders: [{
    a: 0,  // BTC
    b: true,  // Buy
    p: '50000.0',  // Limit price
    s: '0.1',  // Size
    r: false,  // Not reduce-only
    t: { limit: { tif: 'Gtc' } }  // Good-til-cancel
  }],
  grouping: 'na'
};

// 3. Sign the action
const domain = { /* EIP-712 domain */ };
const types = { /* EIP-712 types */ };
const signature = await signer._signTypedData(domain, types, action);
const sig = ethers.utils.splitSignature(signature);

// 4. Submit to API
const response = await fetch('https://api.notional.xyz/exchange', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    action,
    nonce: Date.now(),
    signature: { r: sig.r, s: sig.s, v: sig.v }
  })
});

const result = await response.json();
console.log('Order placed:', result);

Next Steps