EIP-712 Signatures Guide

This guide covers all the different signature types used in the Kyan orderbook system for secure authentication and authorization.

Overview

Kyan uses EIP-712 typed data signatures for secure, structured message signing. There are several different signature types, each designed for specific trading operations (including the Block RFQ signing types in section 9).

EIP-712 Domain

All signatures use the same EIP-712 domain:

const EIP712Domain = {
  chainId: 421614, // Arbitrum Sepolia (use 42161 for Arbitrum One mainnet)
  name: 'Premia',
  verifyingContract: '0x...', // ClearingHouseProxy address from deployment
  version: '1',
};

Signature Types

1. UserLimitOrder

Purpose: Sign individual limit orders for both options and perpetual futures.

Type Definition:

const UserLimitOrder = [
  { name: 'deadline', type: 'uint256' },
  { name: 'instrumentName', type: 'string' },
  { name: 'size', type: 'uint256' },
  { name: 'price', type: 'uint256' },
  { name: 'taker', type: 'address' },
  { name: 'maker', type: 'address' },
  { name: 'direction', type: 'uint8' },
  { name: 'isLiquidation', type: 'bool' },
  { name: 'isPostOnly', type: 'bool' },
  { name: 'mmp', type: 'bool' },
];

Examples:

// Options Order Example
const optionsOrder = {
  instrument_name: 'BTC_USDC-31OCT25-130000-C',
  type: 'good_til_cancelled',
  contracts: 1.5,
  direction: 'buy',
  price: 1000.5,
  post_only: true,
  mmp: false,
  liquidation: false,
  maker: '0xYourAddress',
  taker: null,
};

// Perpetual Order Example
const perpsOrder = {
  instrument_name: 'BTC_USDC-PERPETUAL',
  type: 'good_til_cancelled',
  contracts: 0.2,
  direction: 'buy',
  price: 45000,
  post_only: false,
  mmp: false,
  liquidation: false,
  maker: '0xYourAddress',
  taker: null,
};

const deadline = Math.floor(Date.now() / 1000) + 30;

// Message structure (works for both options and perps)
const message = {
  deadline,
  instrumentName: order.instrument_name,
  size: parseUnits((order.contracts ?? order.amount).toString(), 6), // Canonical `contracts`; legacy notional `amount` still accepted for perps
  price: parseUnits(order.price.toString(), 6), // 6 decimals
  taker: order.taker ?? zeroAddress,
  maker: order.maker,
  direction: order.direction === 'buy' ? 0 : 1, // Buy=0, Sell=1
  isLiquidation: order.liquidation,
  isPostOnly: order.post_only,
  mmp: order.mmp,
};

const signature = await walletClient.signTypedData({
  domain: EIP712Domain,
  types: { UserLimitOrder },
  primaryType: 'UserLimitOrder',
  message,
});

2. UserMarketOrder

Purpose: Sign market orders with limit price protection.

Type Definition:

const OrderTyped = [
  { name: 'instrumentName', type: 'string' },
  { name: 'size', type: 'uint256' },
  { name: 'direction', type: 'uint8' },
];

const UserMarketOrder = [
  { name: 'deadline', type: 'uint256' },
  { name: 'marketOrder', type: 'OrderTyped' },
  { name: 'limitPrice', type: 'uint256' },
  { name: 'taker', type: 'address' },
];

Example:

const trade = {
  market_order: {
    instrument_name: 'BTC_USDC-31OCT25-130000-C',
    contracts: 1.0,
    direction: 'buy',
  },
  limit_price: 1050.0,
  taker: '0xYourAddress',
};

const deadline = Math.floor(Date.now() / 1000) + 30;

const message = {
  deadline,
  marketOrder: {
    instrumentName: trade.market_order.instrument_name,
    size: parseUnits(trade.market_order.contracts.toString(), 6),
    direction: trade.market_order.direction === 'buy' ? 0 : 1,
  },
  limitPrice: parseUnits(trade.limit_price.toString(), 6),
  taker: trade.taker,
};

const signature = await walletClient.signTypedData({
  domain: EIP712Domain,
  types: { OrderTyped, UserMarketOrder },
  primaryType: 'UserMarketOrder',
  message,
});

3. UserComboOrder

Purpose: Sign combo trades (multiple legs executed together).

Type Definition:

const UserComboOrder = [
  { name: 'deadline', type: 'uint256' },
  { name: 'marketOrders', type: 'OrderTyped[]' },
  { name: 'limitNetPrice', type: 'int256' },
  { name: 'limitPerpPrice', type: 'int256' },
  { name: 'taker', type: 'address' },
];

Example:

const comboTrade = {
  market_orders: [
    {
      instrument_name: 'BTC_USDC-31OCT25-130000-C',
      contracts: 1.0,
      direction: 'buy',
    },
    {
      instrument_name: 'BTC_USDC-31OCT25-108000-C',
      contracts: 1.0,
      direction: 'sell',
    },
    {
      instrument_name: 'BTC_USDC-PERPETUAL',
      contracts: 0.5,
      direction: 'buy',
    },
  ],
  limit_total_net_premium: -500.0,
  // limit_perp_price is required (and must be positive) because this combo includes a perpetual leg
  limit_perp_price: 65000.0,
  taker: '0xYourAddress',
};

const deadline = Math.floor(Date.now() / 1000) + 30;

const message = {
  deadline,
  marketOrders: comboTrade.market_orders.map((order) => ({
    instrumentName: order.instrument_name,
    size: parseUnits(order.contracts.toString(), 6),
    direction: order.direction === 'buy' ? 0 : 1,
  })),
  limitNetPrice: parseUnits(comboTrade.limit_total_net_premium.toString(), 6),
  limitPerpPrice: parseUnits(comboTrade.limit_perp_price.toString(), 6),
  taker: comboTrade.taker,
};

const signature = await walletClient.signTypedData({
  domain: EIP712Domain,
  types: { OrderTyped, UserComboOrder },
  primaryType: 'UserComboOrder',
  message,
});

4. CancelOrdersType

Purpose: Sign order cancellation requests for specific orders.

Type Definition:

const CancelOrdersType = [
  { name: 'deadline', type: 'uint256' },
  { name: 'maker', type: 'address' },
  { name: 'orderIds', type: 'string[]' },
];

Example:

const cancelRequest = {
  maker: '0xYourAddress',
  order_ids: ['order_123', 'order_456', 'order_789'],
};

const deadline = Math.floor(Date.now() / 1000) + 30;

const message = {
  deadline,
  maker: cancelRequest.maker,
  orderIds: cancelRequest.order_ids,
};

const signature = await walletClient.signTypedData({
  domain: EIP712Domain,
  types: { CancelOrdersType },
  primaryType: 'CancelOrdersType',
  message,
});

5. CancelAllOrdersType

Purpose: Sign cancel-all-orders requests.

Type Definition:

const CancelAllOrdersType = [
  { name: 'deadline', type: 'uint256' },
  { name: 'maker', type: 'address' },
];

Example:

const cancelAllRequest = {
  maker: '0xYourAddress',
};

const deadline = Math.floor(Date.now() / 1000) + 30;

const message = {
  deadline,
  maker: cancelAllRequest.maker,
};

const signature = await walletClient.signTypedData({
  domain: EIP712Domain,
  types: { CancelAllOrdersType },
  primaryType: 'CancelAllOrdersType',
  message,
});

6. FillRFQType

Purpose: Sign RFQ (Request for Quote) fill requests.

Type Definition:

const FillRFQType = [
  { name: 'deadline', type: 'uint256' },
  { name: 'taker', type: 'address' },
  { name: 'responseId', type: 'string' },
];

Example:

const rfqFillRequest = {
  taker: '0xYourAddress',
  response_id: 'rfq_response_123',
};

const deadline = Math.floor(Date.now() / 1000) + 30;

const message = {
  deadline,
  taker: rfqFillRequest.taker,
  responseId: rfqFillRequest.response_id,
};

const signature = await walletClient.signTypedData({
  domain: EIP712Domain,
  types: { FillRFQType },
  primaryType: 'FillRFQType',
  message,
});

7. OneClickSignature

Purpose: Create one-click trading sessions to avoid signing individual orders.

Type Definition:

const OneClickSignature = [
  { name: 'deadline', type: 'uint256' },
  { name: 'user', type: 'address' },
  { name: 'bindToIp', type: 'bool' },
];

Example:

const oneClickRequest = {
  user: '0xYourAddress',
  bind_to_ip: true,
};

const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour session

const message = {
  deadline,
  user: oneClickRequest.user,
  bindToIp: oneClickRequest.bind_to_ip,
};

const signature = await walletClient.signTypedData({
  domain: EIP712Domain,
  types: { OneClickSignature },
  primaryType: 'OneClickSignature',
  message,
});

// Use the signature to create a session
const sessionResponse = await fetch('/api/session', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    user: oneClickRequest.user,
    bind_to_ip: oneClickRequest.bind_to_ip,
    signature_deadline: deadline,
    signature,
  }),
});

const { hash } = await sessionResponse.json();

// Now use the session hash in the x-one-click header for subsequent requests

8. HeartbeatType

Purpose: Sign heartbeat pings for the dead man's switch (automatic order cancellation).

Type Definition:

const HeartbeatType = [
  { name: 'deadline', type: 'uint256' },
  { name: 'maker', type: 'address' },
  { name: 'timeout', type: 'uint256' },
];

Example:

const heartbeatRequest = {
  maker: '0xYourAddress',
  timeout: 60, // Cancel all orders if no heartbeat within 60 seconds
};

const deadline = Math.floor(Date.now() / 1000) + 30;

const message = {
  deadline,
  maker: heartbeatRequest.maker,
  timeout: heartbeatRequest.timeout,
};

const signature = await walletClient.signTypedData({
  domain: EIP712Domain,
  types: { HeartbeatType },
  primaryType: 'HeartbeatType',
  message,
});

// POST /heartbeat
const requestPayload = {
  maker: heartbeatRequest.maker,
  timeout: heartbeatRequest.timeout,
  signature,
  signature_deadline: deadline,
};

Important Notes:

  • Deadline monotonicity: The signature_deadline must be strictly greater than the last accepted deadline for this maker. This prevents replay attacks without per-call nonce storage.
  • Deadline bound: Deadlines are bounded to at most 30 seconds in the future.
  • One-click alternative: When using a one-click session (x-one-click header), no signature is needed.

9. Block RFQ Signing Types

These types support the Block RFQ flow. Field order is signature-critical and must match exactly.

PostRFQRequestType — signs a taker's RFQ request (POST /rfq/request). It nests one RFQOrderType per requested leg.

const RFQOrderType = [
  { name: 'instrumentName', type: 'string' },
  { name: 'size', type: 'uint256' },
  { name: 'direction', type: 'uint8' },
];

const PostRFQRequestType = [
  { name: 'deadline', type: 'uint256' },
  { name: 'taker', type: 'address' },
  { name: 'rfqOrders', type: 'RFQOrderType[]' },
  { name: 'duration', type: 'uint256' },
];

CancelRFQRequestType — signs a taker's RFQ request cancellation (DELETE /rfq/request).

const CancelRFQRequestType = [
  { name: 'deadline', type: 'uint256' },
  { name: 'taker', type: 'address' },
  { name: 'orderId', type: 'string' },
];

RFQResponseLimitOrder — signs each leg of a maker's RFQ response (POST /rfq/response). It is the UserLimitOrder field set plus a trailing orderId that binds the quote to a specific RFQ request.

const RFQResponseLimitOrder = [
  { name: 'deadline', type: 'uint256' },
  { name: 'instrumentName', type: 'string' },
  { name: 'size', type: 'uint256' },
  { name: 'price', type: 'uint256' },
  { name: 'taker', type: 'address' },
  { name: 'maker', type: 'address' },
  { name: 'direction', type: 'uint8' },
  { name: 'isLiquidation', type: 'bool' },
  { name: 'isPostOnly', type: 'bool' },
  { name: 'mmp', type: 'bool' },
  { name: 'orderId', type: 'string' },
];

As with all order signing, size is the order size scaled to 6 decimals via parseUnits(value.toString(), 6). For perpetual RFQ legs, sign the number of base contracts — RFQ legs are contracts-only and the legacy notional amount is not accepted; for options, sign the number of contracts.

Implementation Notes

Account Type Support

All signatures support both:

  • EOA (Externally Owned Accounts): Standard wallet signatures
  • Safe Wallets: Smart contract signature verification

Field Precision

  • Size/Amount fields: Always converted to 6 decimal precision using parseUnits(value.toString(), 6)
  • Price fields: Always converted to 6 decimal precision
  • Direction mapping: Buy = 0, Sell = 1

Critical Requirements

  • Field Order: The EIP-712 type definition field order is critical and must match exactly as shown. Changing the order will result in invalid signatures.
  • Options vs Perpetuals: Both options and perpetual orders use contracts (base contracts) as the canonical size field in the request payload. For perpetuals, the legacy amount field (USD notional) is still accepted for backward compatibility, but is deprecated and will be removed in a future release (the perpetual sizing model is migrating fully to contracts). The signed size is derived as contracts ?? amount — i.e. sign whichever size field you submit (prefer contracts). RFQ legs are contracts-only.
  • Signature Requirements: When not using one-click sessions, both signature and signature_deadline fields are required in the request payload.

Security Considerations

  • Deadline validation: All signatures must have a deadline within 30 seconds of current time
  • Replay protection: EIP-712 domain separator prevents cross-chain replay attacks
  • Taker defaults: If no taker is specified, defaults to zeroAddress (any taker)

One-Click Sessions vs Individual Signatures

You can choose between two authentication methods:

  1. Individual Signatures: Sign each order/action separately using the appropriate signature type
  2. One-Click Sessions: Create a session once with OneClickSignature, then use the session hash in the x-one-click header for subsequent requests (no individual signatures needed)

One-click sessions are more convenient for active trading but require session management.