EIP-712 Order Signing
All orders on 4rho are signed off-chain using EIP-712 typed structured data and matched by the CLOB engine. Matched trades are settled on-chain by the operator.
Prerequisites
Placing orders requires two separate secrets:
| Secret | Purpose | Used For |
|---|---|---|
| API key secret | HMAC authentication | Request headers (X-4RHO-SIGNATURE) |
| Ethereum private key | EIP-712 order signing | Request body (signature field) |
Your API key secret authenticates you to the API. Your Ethereum private key proves you own the wallet placing the order. These are different keys — do not confuse them.
Common mistake: Passing your Ethereum address (20 bytes, starts with
0x) where a private key (32 bytes / 64 hex chars) is expected. The address identifies your wallet; the private key signs transactions from it.
Domain
The EIP-712 domain must match the deployed Exchange contract:
{
"name": "4rho Exchange",
"version": "1",
"chainId": 137,
"verifyingContract": "0x..."
}
chainId— Polygon Mainnet (137) or Amoy testnet (80002)verifyingContract— The deployed Exchange contract address (provided by the API via/v1/platform/config)
Order Type
The Order struct for EIP-712 signing:
| Field | Solidity Type | Description |
|---|---|---|
salt | uint256 | Random nonce for uniqueness |
maker | address | Wallet address of the order creator |
signer | address | Wallet that signs the order (usually same as maker) |
taker | address | Specific counterparty, or 0x0 for any |
tokenId | uint256 | Outcome token identifier |
makerAmount | uint256 | Amount the maker is offering (in wei) |
takerAmount | uint256 | Amount the maker wants in return (in wei) |
expiration | uint256 | Unix timestamp when the order expires |
nonce | uint256 | Wallet nonce for replay protection |
feeRateBps | uint256 | Fee rate in basis points |
side | uint8 | 0 = BUY, 1 = SELL |
Signing with ethers.js (v6)
import { ethers } from "ethers";
// Create a wallet from your private key (64 hex characters, NOT your address)
const PRIVATE_KEY = "0xYourPrivateKeyHere"; // 32 bytes = 64 hex chars
const wallet = new ethers.Wallet(PRIVATE_KEY);
// wallet.address → "0x..." (your public address, derived automatically)
const domain = {
name: "4rho Exchange",
version: "1",
chainId: 137,
verifyingContract: "0x...", // from /v1/platform/config
};
const types = {
Order: [
{ name: "salt", type: "uint256" },
{ name: "maker", type: "address" },
{ name: "signer", type: "address" },
{ name: "taker", type: "address" },
{ name: "tokenId", type: "uint256" },
{ name: "makerAmount", type: "uint256" },
{ name: "takerAmount", type: "uint256" },
{ name: "expiration", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "feeRateBps", type: "uint256" },
{ name: "side", type: "uint8" },
],
};
const order = {
salt: BigInt(Math.floor(Math.random() * 2 ** 128)),
maker: wallet.address,
signer: wallet.address,
taker: ethers.ZeroAddress,
tokenId: BigInt("12345"),
makerAmount: ethers.parseUnits("10", 6), // 10 USDC
takerAmount: ethers.parseUnits("5", 6), // 5 USDC
expiration: BigInt(Math.floor(Date.now() / 1000) + 3600),
nonce: BigInt(0),
feeRateBps: BigInt(20), // 0.20%
side: 0, // BUY
};
const signature = await wallet.signTypedData(domain, types, order);
Signing with web3.py
import time
from web3 import Web3
from eth_account import Account
from eth_account.messages import encode_typed_data
# Create an account from your private key (64 hex characters, NOT your address)
PRIVATE_KEY = "0xYourPrivateKeyHere" # 32 bytes = 64 hex chars
account = Account.from_key(PRIVATE_KEY)
# account.address → "0x..." (your public address, derived automatically)
domain_data = {
"name": "4rho Exchange",
"version": "1",
"chainId": 137,
"verifyingContract": "0x...",
}
order_types = {
"Order": [
{"name": "salt", "type": "uint256"},
{"name": "maker", "type": "address"},
{"name": "signer", "type": "address"},
{"name": "taker", "type": "address"},
{"name": "tokenId", "type": "uint256"},
{"name": "makerAmount", "type": "uint256"},
{"name": "takerAmount", "type": "uint256"},
{"name": "expiration", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "feeRateBps", "type": "uint256"},
{"name": "side", "type": "uint8"},
]
}
order_data = {
"salt": 123456789,
"maker": account.address,
"signer": account.address,
"taker": "0x0000000000000000000000000000000000000000",
"tokenId": 12345,
"makerAmount": 10_000_000, # 10 USDC (6 decimals)
"takerAmount": 5_000_000, # 5 USDC
"expiration": int(time.time()) + 3600,
"nonce": 0,
"feeRateBps": 20,
"side": 0, # BUY
}
signable = encode_typed_data(
domain_data, order_types, "Order", order_data
)
signed = account.sign_message(signable)
signature = signed.signature.hex()
Submitting the Signed Order
curl -X POST https://4rho.com/api/v1/orders \
-H "Content-Type: application/json" \
-H "X-4RHO-API-KEY: $API_KEY" \
-H "X-4RHO-SIGNATURE: $HMAC_SIGNATURE" \
-H "X-4RHO-TIMESTAMP: $TIMESTAMP" \
-H "X-4RHO-PASSPHRASE: $PASSPHRASE" \
-d '{
"market_id": "550e8400-e29b-41d4-a716-446655440000",
"outcome_index": 0,
"salt": "123456789",
"maker": "0xYourAddress",
"signer": "0xYourAddress",
"taker": "0x0000000000000000000000000000000000000000",
"token_id": "12345",
"maker_amount": "10000000",
"taker_amount": "5000000",
"expiration": 1735689600,
"nonce": 0,
"fee_rate_bps": 20,
"side": "BUY",
"signature": "0xYourEIP712Signature..."
}'
The market_id and outcome_index fields identify which market and outcome you are trading. The remaining fields are the EIP-712 signed order data. Both the HMAC signature (headers) and the EIP-712 signature (body) are required.
See the Orders reference for the full list of request fields including optional ones like time_in_force and post_only.