Integration

This document describes how to integrate with the RISEx API integration. All signatures use EIP-712 typed data signing.


Table of Contents

  1. Overview
  2. EIP-712 Domain
  3. Register Signer
  4. Revoke Signer
  5. Place Order
  6. Cancel Order
  7. Update Leverage
  8. Update Margin Mode
  9. Update Isolated Margin
  10. WebSocket Authentication
  11. Deposit USDC
  12. Code Examples

Overview

RISEx uses a session key (signer) model for trading:

  1. Account - Your main wallet holding funds
  2. 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

FieldTypeDescription
accountaddressMain wallet address
signeraddressSession key address
messagestringArbitrary message (for display)
nonceuint256Unique number (use Date.now() in milliseconds)
expirationuint40Unix 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

FieldTypeValues
market_iduint64Market identifier
sizeuint128Order size in base asset (18 decimals)
priceuint128Limit price (18 decimals)
sideuint80=Long/Buy, 1=Short/Sell
order_typeuint80=Limit, 1=Market
time_in_forceuint80=GTC, 1=GTT, 2=FOK, 3=IOC
post_onlyboolReject if would take liquidity
reduce_onlyboolCan only reduce position
stp_modeuint80=CancelTaker, 1=CancelMaker, 2=CancelBoth
expiryuint32Unix 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

FieldTypeDescription
accountaddressMain wallet address
signeraddressSession key address (must be registered)
messagestringArbitrary message
nonceuint64Unique 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 deadline hasn't passed
  • For registration, check expiration hasn'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
ContractPurpose
RISExAuthorizationSigner registration, signature verification
PerpsManagerPerpetual futures trading
SpotManagerSpot 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.