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 requests8. 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_deadlinemust 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-clickheader), 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 legacyamountfield (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 tocontracts). The signedsizeis derived ascontracts ?? amount— i.e. sign whichever size field you submit (prefercontracts). RFQ legs are contracts-only. - Signature Requirements: When not using one-click sessions, both
signatureandsignature_deadlinefields 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:
- Individual Signatures: Sign each order/action separately using the appropriate signature type
- One-Click Sessions: Create a session once with
OneClickSignature, then use the session hash in thex-one-clickheader for subsequent requests (no individual signatures needed)
One-click sessions are more convenient for active trading but require session management.
