Sign DerivaDEX requests with EIP-712
Use this when you need a clear implementation of the signing flow DerivaDEX expects. The examples use Python because the procedure is easiest to read there, but the same steps apply in any language that can build the same bytes and hashes.
You need the chainId, the DerivaDEX verifying smart contract address, the exact request you plan to send, and the wallet key that controls the trader account.
Use the network values you are signing for:
| Network | chainId | DerivaDEX verifying smart contract address |
|---|
| Testnet (Sepolia) | 11155111 | 0x5d1a3b4181d3cad422f404f28e9e972d0ba4dad6 |
| Mainnet | 1 | 0x6fb8aa6fc6f27e591423009194529ae126660027 |
After the signature is correct, move to How to Encrypt Requests for the Operator.
Follow the signing procedure
- Define the EIP-191 prefix.
EIP191_HEADER = b"\x19\x01"
-
Build the EIP-712 domain separator from the
chainId and the DerivaDEX verifying smart contract address.
Use the same domain values for every request:
| Field | Value |
|---|
name | DerivaDEX |
version | 1 |
chainId | The Ethereum chain ID for the DerivaDEX network you are signing for |
verifyingContract | The DerivaDEX verifying smart contract address for that same network |
-
Choose the request type you are signing, then build its message hash with the exact field order DerivaDEX expects.
| Request | Struct signature | Fields, in order |
|---|
| Order | OrderParams | symbol, strategy, side, orderType, nonce, amount, price, stopPrice |
| Cancel one order | CancelOrderParams | symbol, orderHash, nonce |
| Cancel all orders in one strategy | CancelAllParams | symbol, strategy, nonce |
| Update profile | UpdateProfileParams | payFeesInDdxState, referralAddress, deniedDelegatedSessionAction, deniedDelegatedSessionAddress, deniedDelegatedSessionExpiry, nonce |
| Withdraw collateral | WithdrawParams | strategyId, currency, amount, nonce |
-
Compute the final digest as
keccak256(EIP191_HEADER + domain_separator + message_struct_hash).
-
Sign that digest with the trader wallet.
-
Recover the signer locally before submission and confirm that it matches the trader wallet you expect.
Worked examples
Start with the helper functions that stay the same across request types.
Each request type can be implemented in two ways here:
Direct implementation shows the hashing steps directly.
DDX client library shows the shortest path if you already use the library.
Both approaches should produce the same EIP-712 hash for the same input.
from decimal import ROUND_DOWN, Decimal
from eth_abi import encode
from eth_utils.crypto import keccak
EIP191_HEADER = b"\x19\x01"
TOKEN_UNIT_SCALE = 6
def encode_string_as_bytes32(value: str) -> bytes:
raw = value.encode("utf-8")
if len(raw) > 31:
raise ValueError("DerivaDEX string fields must fit in 31 bytes")
return bytes([len(raw)]) + raw + bytes(31 - len(raw))
def scale_decimal(value: Decimal) -> int:
fixed = value.quantize(Decimal("0.000001"), rounding=ROUND_DOWN)
return int(fixed * (10 ** TOKEN_UNIT_SCALE))
def compute_eip712_domain_separator(chain_id: int, verifying_contract: str) -> bytes:
domain_type_hash = keccak(
b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
)
return keccak(
domain_type_hash
+ keccak(b"DerivaDEX")
+ keccak(b"1")
+ encode(["uint256"], [chain_id])
+ encode(["address"], [verifying_contract])
)
def compute_eip712_hash(
chain_id: int,
verifying_contract: str,
message_struct_hash: bytes,
) -> str:
domain_separator = compute_eip712_domain_separator(chain_id, verifying_contract)
digest = keccak(EIP191_HEADER + domain_separator + message_struct_hash)
return "0x" + digest.hex()
Example: sign an order request
This example follows the steps above for OrderParams.
Direct implementation
from dataclasses import dataclass
from enum import IntEnum
class OrderSide(IntEnum):
BID = 0
ASK = 1
class OrderType(IntEnum):
LIMIT = 0
MARKET = 1
STOP = 2
LIMIT_POST_ONLY = 3
@dataclass
class OrderIntent:
symbol: str
strategy: str
side: OrderSide
order_type: OrderType
nonce: str
amount: Decimal
price: Decimal
stop_price: Decimal
def compute_message_struct_hash(intent: OrderIntent) -> bytes:
order_type_hash = keccak(
b"OrderParams(bytes32 symbol,bytes32 strategy,uint256 side,uint256 orderType,bytes32 nonce,uint256 amount,uint256 price,uint256 stopPrice)"
)
return keccak(
order_type_hash
+ encode(["bytes32"], [encode_string_as_bytes32(intent.symbol)])
+ encode(["bytes32"], [encode_string_as_bytes32(intent.strategy)])
+ encode(["uint256"], [intent.side])
+ encode(["uint256"], [intent.order_type])
+ encode(["bytes32"], [bytes.fromhex(intent.nonce.removeprefix("0x"))])
+ encode(["uint256"], [scale_decimal(intent.amount)])
+ encode(["uint256"], [scale_decimal(intent.price)])
+ encode(["uint256"], [scale_decimal(intent.stop_price)])
)
Example input:
intent = OrderIntent(
symbol="ETHP",
strategy="main",
side=OrderSide.BID,
order_type=OrderType.LIMIT,
nonce="0x3137373038373530313938323238333436363300000000000000000000000000",
amount=Decimal("0.1"),
price=Decimal("1800"),
stop_price=Decimal("0"),
)
message_struct_hash = compute_message_struct_hash(intent)
eip712_hash = compute_eip712_hash(
chain_id=11155111,
verifying_contract="0x5d1a3b4181d3cad422f404f28e9e972d0ba4dad6",
message_struct_hash=message_struct_hash,
)
For this input, the target result is:
"0xacdcc010cbe31e9387e8faf29d533bdfd20483d36d599e97b63fb8319933ee16"
DDX client library
If you already use ddx-python, this is the shortest path through the same procedure:
from ddx._rust.common.enums import OrderSide, OrderType
from ddx._rust.common.requests.intents import OrderIntent
from ddx._rust.decimal import Decimal
chain_id = 11155111
verifying_contract = "0x5d1a3b4181d3cad422f404f28e9e972d0ba4dad6"
order_intent = OrderIntent(
"ETHP",
"main",
OrderSide.Bid,
OrderType.Limit,
"0x3137373038373530313938323238333436363300000000000000000000000000",
Decimal("0.1"),
Decimal("1800"),
Decimal("0"),
None,
)
eip712_hash = order_intent.hash_eip712((chain_id, verifying_contract))
print(f"Order hash: {eip712_hash}")
For this input, the DDX client library should return the same result as the native example above:
"0xacdcc010cbe31e9387e8faf29d533bdfd20483d36d599e97b63fb8319933ee16"
Sign the hash and recover the signer
Once the EIP-712 hash is correct, signing it is the short final step. This example uses a generated wallet only to show the mechanics:
from eth_account import Account
account = Account.create()
digest_bytes = bytes.fromhex(eip712_hash.removeprefix("0x"))
signed = account.signHash(digest_bytes)
signature = signed.signature.hex()
recovered_address = Account._recover_hash(digest_bytes, signature=signed.signature)
print(f"Address: {account.address}")
print(f"Signature: {signature}")
print(f"Recovered: {recovered_address}")
The recovered address should match the wallet address that signed the hash. In production, use the trader wallet that is meant to authorize the request, not a generated example wallet.
Use Signed Private Requests for the field tables behind cancel, cancel-all, profile-update, and withdraw request content.
Check your result before you submit
- Recover the signing address from the signature and digest.
- Confirm that it matches the trader wallet you expect to authorize the request.
If the recovered address is wrong, stop there. The operator will reject the request with SignatureRecoveryMismatch, SignerNotFound, or a later action-specific rejection.
Appendix
Use this section when you need the exact field rules behind the steps and examples.
| Field family | Rule |
|---|
symbol, strategy, strategyId | Encode as bytes32 with one length byte, then UTF-8 bytes, then zero padding |
nonce | Keep it as a 32-byte hex value and make it monotonically increasing for the signer |
| Decimal values | Scale to fixed-point integers at 10^6, cutting off anything past six decimal places first |
| Order side | Bid = 0, Ask = 1 |
| Order type | Limit = 0, Market = 1, Stop = 2, LimitPostOnly = 3 |
Withdraw currency | Sign the ERC-20 token address directly |
Profile updates need three extra rules:
| Field | Rule |
|---|
payFeesInDdxState | false = 0, true = 1 |
Empty referralAddress | Use zero-filled bytes32 |
| Empty delegated-session update | Use action 0, zero address, and expiry 0 |
Continue with submission
After the signature is correct, fetch the operator key, encrypt the signed JSON, and submit the bytes to POST /v2/request. Use How to Encrypt Requests for the Operator for that step.
If the request still fails after local recovery matches the trader wallet, use Error Reference for the exact rejection path.Last modified on May 1, 2026