This document describes how to integrate with the RISEx API integration. All signatures use EIP-712 typed data signing.
Table of Contents
- Overview
- EIP-712 Domain
- Register Signer
- Revoke Signer
- Place Order
- Cancel Order
- Update Leverage
- Update Margin Mode
- Update Isolated Margin
- WebSocket Authentication
- Deposit USDC
- Code Examples
Overview
RISEx uses a session key (signer) model for trading:
- Account - Your main wallet holding funds
- Signer - A hot wallet (session key) authorized to sign orders on behalf of the account
This separation allows:
- Keep main wallet secure (cold storage)
- Fast order signing with hot wallet
- Revoke signer anytime without moving funds
Signature Flow
┌─────────────────────────────────────────────────────────┐
│ 1. Register Signer (one-time setup) │
│ Account signs: RegisterSigner │
│ Signer signs: VerifySigner │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. Trading (ongoing) │
│ Signer signs: VerifySignature (with order hash) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. Revoke Signer (when needed) │
│ Account signs: RevokeSigner │
└─────────────────────────────────────────────────────────┘
EIP-712 Domain
All signatures use the same EIP-712 domain. Fetch it from the API:
GET /v1/auth/eip712-domain
Response:
{
"name": "RISEx",
"version": "1",
"chainId": "11155111",
"verifyingContract": "0x..."
}Domain Type:
EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
Domain Separator Calculation:
domainSeparator = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(bytes(name)), // keccak256("RISEx")
keccak256(bytes(version)), // keccak256("1")
chainId,
verifyingContract
)
)
Register Signer
Registers a session key (signer) to trade on behalf of an account. Requires two signatures: one from the account and one from the signer.
API Endpoint
POST /v1/auth/register-signer
Request Body
{
"account": "0x...",
"signer": "0x...",
"message": "Register signer for RISEx trading",
"nonce": "1234567890123456789", // just use a random nonce here, you can use current timestamp
"expiration": 1735689600,
"account_signature": "0x...",
"signer_signature": "0x..."
}Signature 1: Account Signs RegisterSigner
TypeHash:
RegisterSigner(address signer,string message,uint40 expiration,uint256 nonce)
Struct Hash:
structHash = keccak256(
abi.encode(
keccak256("RegisterSigner(address signer,string message,uint40 expiration,uint256 nonce)"),
signer, // address
keccak256(bytes(message)), // string → keccak256
expiration, // uint40 (unix timestamp)
nonce // uint256
)
)Final Hash (to sign):
digest = keccak256("\x19\x01" || domainSeparator || structHash)
Signature 2: Signer Signs VerifySigner
TypeHash:
VerifySigner(address account,uint256 nonce)
Struct Hash:
structHash = keccak256(
abi.encode(
keccak256("VerifySigner(address account,uint256 nonce)"),
account, // address (the account being authorized)
nonce // uint256 (same nonce as RegisterSigner)
)
)Parameters
| Field | Type | Description |
|---|---|---|
account | address | Main wallet address |
signer | address | Session key address |
message | string | Arbitrary message (for display) |
nonce | uint256 | Unique number (use Date.now() in milliseconds) |
expiration | uint40 | Unix timestamp when signer expires |
Revoke Signer
Revokes a previously registered signer. Only requires the account signature.
API Endpoint
POST /v1/auth/revoke-signer
Request Body
{
"account": "0x...",
"signer": "0x...",
"nonce": "1234567890123456789",
"account_signature": "0x..."
}Account Signs RevokeSigner
TypeHash:
RevokeSigner(address signer,uint256 nonce)
Struct Hash:
structHash = keccak256(
abi.encode(
keccak256("RevokeSigner(address signer,uint256 nonce)"),
signer, // address to revoke
nonce // uint256
)
)Place Order
Places an order using VerifySignature scheme. The signer signs this.
API Endpoint
POST /v1/orders/place
Request Body
{
"order_params": {
"market_id": 1,
"size": "1000000000000000000",
"price": "50000000000000000000000",
"side": 0,
"order_type": 0,
"time_in_force": 0,
"post_only": false,
"reduce_only": false,
"stp_mode": 0,
"expiry": 0
},
"permit_params": {
"account": "0x...",
"signer": "0x...",
"nonce": "1234567890123456789",
"deadline": 1735689600,
"signature": "0x..."
}
}Order Data Encoding (47 bytes)
The order must be encoded into 47 bytes before hashing:
Byte Layout:
┌────────────┬──────────┬─────────────────────────────────┐
│ Bytes │ Type │ Field │
├────────────┼──────────┼─────────────────────────────────┤
│ [0:8] │ uint64 │ marketId │
│ [8:24] │ uint128 │ size │
│ [24:40] │ uint128 │ price │
│ [40] │ uint8 │ flags (see below) │
│ [41] │ uint8 │ orderType │
│ [42] │ uint8 │ timeInForce │
│ [43:47] │ uint32 │ expiry │
└────────────┴──────────┴─────────────────────────────────┘
Flags byte [40]:
bit 0: side (0=Long/Buy, 1=Short/Sell)
bit 1: postOnly
bit 2: reduceOnly
bit 3-4: stpMode (2 bits)
bit 5-7: unused
Order Hash:
orderHash = keccak256(encodedOrderData) // 47 bytes
Signer Signs VerifySignature
TypeHash:
VerifySignature(address account,address target,bytes32 hash,uint256 nonce,uint256 deadline)
Struct Hash:
structHash = keccak256(
abi.encode(
keccak256("VerifySignature(address account,address target,bytes32 hash,uint256 nonce,uint256 deadline)"),
account, // address (main wallet)
target, // address (PerpsManager contract)
orderHash, // bytes32 (keccak256 of encoded order)
nonce, // uint256
deadline // uint256 (unix timestamp)
)
)Order Parameters
| Field | Type | Values |
|---|---|---|
market_id | uint64 | Market identifier |
size | uint128 | Order size in base asset (18 decimals) |
price | uint128 | Limit price (18 decimals) |
side | uint8 | 0=Long/Buy, 1=Short/Sell |
order_type | uint8 | 0=Limit, 1=Market |
time_in_force | uint8 | 0=GTC, 1=GTT, 2=FOK, 3=IOC |
post_only | bool | Reject if would take liquidity |
reduce_only | bool | Can only reduce position |
stp_mode | uint8 | 0=CancelTaker, 1=CancelMaker, 2=CancelBoth |
expiry | uint32 | Unix timestamp (for GTT orders) |
Cancel Order
Cancels an existing order. The signer signs this.
API Endpoint
POST /v1/orders/cancel
Request Body
{
"market_id": 1,
"order_id": "12345",
"permit_params": {
"account": "0x...",
"signer": "0x...",
"nonce": "1234567890123456789",
"deadline": 1735689600,
"signature": "0x..."
}
}Cancel Data Encoding (32 bytes)
Byte Layout (bytes32):
┌────────────┬──────────┬─────────────────────────────────┐
│ Bytes │ Type │ Field │
├────────────┼──────────┼─────────────────────────────────┤
│ [0:8] │ uint64 │ marketId │
│ [8:32] │ uint192 │ orderId │
└────────────┴──────────┴─────────────────────────────────┘
Cancel Hash:
cancelData = (uint256(marketId) << 192) | uint256(orderId)
cancelHash = keccak256(abi.encode(bytes32(cancelData)))Signer Signs VerifySignature
Same as Place Order, but with cancelHash instead of orderHash.
Update Leverage
Updates leverage for a market. The signer signs this.
API Endpoint
POST /v1/account/leverage
Request Body
{
"market_id": 1,
"leverage": "10000000000000000000",
"permit_params": {
"account": "0x...",
"signer": "0x...",
"nonce": "1234567890123456789",
"deadline": 1735689600,
"signature": "0x..."
}
}Leverage Hash
// Note: Solidity function signature is (uint256, uint128)
// ABI encoding pads uint128 to 32 bytes
leverageHash = keccak256(
abi.encode(
uint256(marketId), // 32 bytes
uint128(leverage) // 32 bytes (padded), 18 decimals (e.g., 10e18 = 10x)
)
)Signer Signs VerifySignature
Same structure as Place Order, with leverageHash.
Update Margin Mode
Switches between cross and isolated margin. The signer signs this.
API Endpoint
POST /v1/account/margin-mode
Request Body
{
"market_id": 1,
"margin_mode": 1,
"permit_params": {
"account": "0x...",
"signer": "0x...",
"nonce": "1234567890123456789",
"deadline": 1735689600,
"signature": "0x..."
}
}Margin Mode Hash
marginModeHash = keccak256(
abi.encode(
uint256(marketId),
uint8(marginMode) // 0=Cross, 1=Isolated
)
)Update Isolated Margin
Adds or removes margin from isolated position. The signer signs this.
API Endpoint
POST /v1/account/isolated-margin
Request Body
{
"market_id": 1,
"amount": "1000000000000000000",
"permit_params": {
"account": "0x...",
"signer": "0x...",
"nonce": "1234567890123456789",
"deadline": 1735689600,
"signature": "0x..."
}
}Isolated Margin Hash
isolatedMarginHash = keccak256(
abi.encode(
uint256(marketId),
int256(amount) // Positive=add, Negative=remove (18 decimals)
)
)WebSocket Authentication
Authenticates a WebSocket connection to receive private data (orders, positions, etc.).
Message Format
Send after WebSocket connection is established:
{
"type": "auth",
"payload": {
"account": "0x...",
"signer": "0x...",
"message": "WebSocket authentication",
"nonce": 1234567890,
"signature": "0x..."
}
}Signer Signs Register
TypeHash:
Register(address signer,string message,uint64 nonce)
Struct Hash:
structHash = keccak256(
abi.encode(
keccak256("Register(address signer,string message,uint64 nonce)"),
signer, // address
keccak256(bytes(message)), // string → keccak256
nonce // uint64
)
)Parameters
| Field | Type | Description |
|---|---|---|
account | address | Main wallet address |
signer | address | Session key address (must be registered) |
message | string | Arbitrary message |
nonce | uint64 | Unique number (use timestamp) |
Deposit USDC
Deposits USDC into the exchange. Uses a different signature type (Deposit) instead of VerifySignature.
API Endpoint
POST /v1/account/deposit
Request Body
{
"account": "0x...",
"amount": "1000000000000000000",
}Account Signs Deposit
TypeHash:
Deposit(address account,uint256 amount)
Struct Hash:
structHash = keccak256(
abi.encode(
keccak256("Deposit(address account,uint256 amount)"),
account, // address
amount // uint256 (18 decimals)
)
)Note: Deposit uses the account's main wallet signature, not the signer key.
Code Examples
TypeScript/JavaScript
import { ethers } from 'ethers';
// EIP-712 Domain (fetch from API)
const domain = {
name: 'RISEx',
version: '1',
chainId: 11155111,
verifyingContract: '0x...' // RISExAuthorization address
};
// --- Register Signer ---
const registerSignerTypes = {
RegisterSigner: [
{ name: 'signer', type: 'address' },
{ name: 'message', type: 'string' },
{ name: 'expiration', type: 'uint40' },
{ name: 'nonce', type: 'uint256' }
]
};
const verifySignerTypes = {
VerifySigner: [
{ name: 'account', type: 'address' },
{ name: 'nonce', type: 'uint256' }
]
};
async function registerSigner(
accountWallet: ethers.Wallet,
signerWallet: ethers.Wallet,
message: string,
expiration: number
) {
const nonce = Date.now().toString();
// Account signs RegisterSigner
const accountSignature = await accountWallet.signTypedData(
domain,
registerSignerTypes,
{
signer: signerWallet.address,
message: message,
expiration: expiration,
nonce: nonce
}
);
// Signer signs VerifySigner
const signerSignature = await signerWallet.signTypedData(
domain,
verifySignerTypes,
{
account: accountWallet.address,
nonce: nonce
}
);
return {
account: accountWallet.address,
signer: signerWallet.address,
message,
nonce,
expiration,
account_signature: accountSignature,
signer_signature: signerSignature
};
}
// --- Place Order ---
const verifySignatureTypes = {
VerifySignature: [
{ name: 'account', type: 'address' },
{ name: 'target', type: 'address' },
{ name: 'hash', type: 'bytes32' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
};
function encodeOrderData(params: {
marketId: bigint;
size: bigint;
price: bigint;
side: number;
postOnly: boolean;
reduceOnly: boolean;
stpMode: number;
orderType: number;
timeInForce: number;
expiry: number;
}): Uint8Array {
const data = new Uint8Array(47);
const view = new DataView(data.buffer);
// marketId (uint64, big-endian)
view.setBigUint64(0, params.marketId, false);
// size (uint128, big-endian) - simplified, need proper 128-bit handling
const sizeBytes = ethers.toBeArray(params.size);
data.set(sizeBytes.slice(-16).padStart(16, 0), 8);
// price (uint128, big-endian)
const priceBytes = ethers.toBeArray(params.price);
data.set(priceBytes.slice(-16).padStart(16, 0), 24);
// flags byte
let flags = 0;
flags |= (params.side & 1);
flags |= (params.postOnly ? 1 : 0) << 1;
flags |= (params.reduceOnly ? 1 : 0) << 2;
flags |= (params.stpMode & 3) << 3;
data[40] = flags;
// orderType, timeInForce, expiry
data[41] = params.orderType;
data[42] = params.timeInForce;
view.setUint32(43, params.expiry, false);
return data;
}
async function signPlaceOrder(
signerWallet: ethers.Wallet,
accountAddress: string,
targetContract: string, // PerpsManager address
orderParams: OrderParams
) {
const encodedOrder = encodeOrderData(orderParams);
const orderHash = ethers.keccak256(encodedOrder);
const nonce = Date.now().toString();
const deadline = Math.floor(Date.now() / 1000) + 300; // 5 minutes
const signature = await signerWallet.signTypedData(
domain,
verifySignatureTypes,
{
account: accountAddress,
target: targetContract,
hash: orderHash,
nonce: nonce,
deadline: deadline
}
);
return {
order_params: orderParams,
permit_params: {
account: accountAddress,
signer: signerWallet.address,
nonce,
deadline,
signature
}
};
}
// --- Cancel Order ---
function encodeCancelData(marketId: bigint, orderId: bigint): string {
// Pack into bytes32: marketId in high 64 bits, orderId in low 192 bits
const packed = (marketId << 192n) | orderId;
return ethers.solidityPacked(['bytes32'], [packed]);
}
async function signCancelOrder(
signerWallet: ethers.Wallet,
accountAddress: string,
targetContract: string,
marketId: bigint,
orderId: bigint
) {
const cancelData = encodeCancelData(marketId, orderId);
const cancelHash = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(['bytes32'], [cancelData])
);
const nonce = Date.now().toString();
const deadline = Math.floor(Date.now() / 1000) + 300;
const signature = await signerWallet.signTypedData(
domain,
verifySignatureTypes,
{
account: accountAddress,
target: targetContract,
hash: cancelHash,
nonce: nonce,
deadline: deadline
}
);
return {
market_id: marketId,
order_id: orderId,
permit_params: {
account: accountAddress,
signer: signerWallet.address,
nonce,
deadline,
signature
}
};
}Python
from eth_account import Account
from eth_account.messages import encode_typed_data
import time
# EIP-712 Domain
domain = {
"name": "RISEx",
"version": "1",
"chainId": 11155111,
"verifyingContract": "0x..."
}
# Register Signer
def sign_register_signer(account_key: str, signer_key: str, message: str, expiration: int):
account = Account.from_key(account_key)
signer = Account.from_key(signer_key)
nonce = str(int(time.time() * 1000))
# Account signature
register_data = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
"RegisterSigner": [
{"name": "signer", "type": "address"},
{"name": "message", "type": "string"},
{"name": "expiration", "type": "uint40"},
{"name": "nonce", "type": "uint256"},
],
},
"primaryType": "RegisterSigner",
"domain": domain,
"message": {
"signer": signer.address,
"message": message,
"expiration": expiration,
"nonce": int(nonce),
},
}
account_sig = account.sign_typed_data(full_message=register_data)
# Signer signature
verify_data = {
"types": {
"EIP712Domain": [...], # same as above
"VerifySigner": [
{"name": "account", "type": "address"},
{"name": "nonce", "type": "uint256"},
],
},
"primaryType": "VerifySigner",
"domain": domain,
"message": {
"account": account.address,
"nonce": int(nonce),
},
}
signer_sig = signer.sign_typed_data(full_message=verify_data)
return {
"account": account.address,
"signer": signer.address,
"message": message,
"nonce": nonce,
"expiration": expiration,
"account_signature": account_sig.signature.hex(),
"signer_signature": signer_sig.signature.hex(),
}Go
package main
import (
"crypto/ecdsa"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)
var domain = apitypes.TypedDataDomain{
Name: "RISEx",
Version: "1",
ChainId: (*math.HexOrDecimal256)(big.NewInt(11155111)),
VerifyingContract: "0x...",
}
func SignRegisterSigner(
accountKey *ecdsa.PrivateKey,
signerKey *ecdsa.PrivateKey,
message string,
expiration uint64,
) (accountSig, signerSig []byte, nonce *big.Int, err error) {
nonce = big.NewInt(time.Now().UnixNano())
accountAddr := crypto.PubkeyToAddress(accountKey.PublicKey)
signerAddr := crypto.PubkeyToAddress(signerKey.PublicKey)
// Account signs RegisterSigner
registerData := apitypes.TypedData{
Types: apitypes.Types{
"EIP712Domain": {
{Name: "name", Type: "string"},
{Name: "version", Type: "string"},
{Name: "chainId", Type: "uint256"},
{Name: "verifyingContract", Type: "address"},
},
"RegisterSigner": {
{Name: "signer", Type: "address"},
{Name: "message", Type: "string"},
{Name: "expiration", Type: "uint40"},
{Name: "nonce", Type: "uint256"},
},
},
PrimaryType: "RegisterSigner",
Domain: domain,
Message: apitypes.TypedDataMessage{
"signer": signerAddr.Hex(),
"message": message,
"expiration": fmt.Sprintf("%d", expiration),
"nonce": nonce.String(),
},
}
accountSig, err = signTypedData(accountKey, registerData)
if err != nil {
return nil, nil, nil, err
}
// Signer signs VerifySigner
verifyData := apitypes.TypedData{
Types: apitypes.Types{
"EIP712Domain": {...},
"VerifySigner": {
{Name: "account", Type: "address"},
{Name: "nonce", Type: "uint256"},
},
},
PrimaryType: "VerifySigner",
Domain: domain,
Message: apitypes.TypedDataMessage{
"account": accountAddr.Hex(),
"nonce": nonce.String(),
},
}
signerSig, err = signTypedData(signerKey, verifyData)
return accountSig, signerSig, nonce, err
}
func signTypedData(key *ecdsa.PrivateKey, data apitypes.TypedData) ([]byte, error) {
domainSeparator, _ := data.HashStruct("EIP712Domain", data.Domain.Map())
messageHash, _ := data.HashStruct(data.PrimaryType, data.Message)
rawData := []byte{0x19, 0x01}
rawData = append(rawData, domainSeparator...)
rawData = append(rawData, messageHash...)
digest := crypto.Keccak256(rawData)
sig, err := crypto.Sign(digest, key)
if err != nil {
return nil, err
}
// Adjust v for Ethereum (27/28)
sig[64] += 27
return sig, nil
}Common Issues
1. Invalid Signature
- Ensure domain parameters match exactly (name, version, chainId, verifyingContract)
- Check signature format:
r || s || v(65 bytes) - Verify v value is 27 or 28 (not 0 or 1)
2. Nonce Already Used
- Use unique nonce for each signature (timestamp in nanoseconds recommended)
- Nonces are tracked per-signer, not per-account
3. Signature Expired
- Check
deadlinehasn't passed - For registration, check
expirationhasn't passed
4. Signer Not Authorized
- Ensure signer is registered for the account
- Check signer has correct permission (Perps, Spot, or All)
- Verify signer hasn't been revoked
5. Wrong Target Contract
- Place order: use PerpsManager or SpotManager address
- Get target address from API or chain config
Contract Addresses
Fetch current addresses from:
GET /v1/config
| Contract | Purpose |
|---|---|
| RISExAuthorization | Signer registration, signature verification |
| PerpsManager | Perpetual futures trading |
| SpotManager | Spot trading |
Appendix: Type Hashes
// Signer Management
REGISTER_SIGNER_TYPEHASH = keccak256("RegisterSigner(address signer,string message,uint40 expiration,uint256 nonce)")
VERIFY_SIGNER_TYPEHASH = keccak256("VerifySigner(address account,uint256 nonce)")
REVOKE_SIGNER_TYPEHASH = keccak256("RevokeSigner(address signer,uint256 nonce)")
// Permissions
ENABLE_PERMISSION_TYPEHASH = keccak256("EnablePermission(address signer,uint8 permission,uint256 nonce)")
DISABLE_PERMISSION_TYPEHASH = keccak256("DisablePermission(address signer,uint8 permission,uint256 nonce)")
// Trading & Actions
VERIFY_SIGNATURE_TYPEHASH = keccak256("VerifySignature(address account,address target,bytes32 hash,uint256 nonce,uint256 deadline)")Support
For integration support, contact the RISEx team ([email protected]) or refer to API documentation.