EIP-712 Signing
GX Exchange uses EIP-712 typed data signatures for all authenticated exchange operations. This page provides a complete guide to constructing and signing requests.
Overview
EIP-712 provides structured, human-readable message signing. When a user signs an exchange action, the wallet can display the action details clearly, enabling informed approval.
EIP-712 Domain
All signatures use the following domain separator:
{
"name": "GXExchange",
"version": "1",
"chainId": 42069,
"verifyingContract": "0x0000000000000000000000000000000000000000"
}| Field | Value | Description |
|---|---|---|
name | "GXExchange" | Protocol name |
version | "1" | Domain version |
chainId | 42069 | GX Chain L1 chain ID |
verifyingContract | 0x000...000 | Zero address (no specific contract) |
Signing Flow
- Construct the action payload — the order, cancel, transfer, or other operation.
- Generate a nonce — current Unix timestamp in milliseconds.
- Derive the connectionId — zero-pad the nonce to 32 bytes.
- Build the EIP-712 typed data — domain + types + message.
- Sign — produce the signature using
eth_signTypedData_v4or equivalent. - Submit — send the action, nonce, and signature to
POST /exchange.
Type Definitions
Agent Type (for API wallet signing)
const agentTypes = {
Agent: [
{ name: "source", type: "string" },
{ name: "connectionId", type: "bytes32" },
]
};Order Wire Type
The order action is hashed and signed as part of the Agent message. The source field is set to "a" for mainnet and "b" for testnet.
Connection ID
The connectionId is derived from the nonce:
import { ethers } from "ethers";
function nonceToConnectionId(nonce: number): string {
return ethers.zeroPadValue(ethers.toBeHex(nonce), 32);
}def nonce_to_connection_id(nonce: int) -> bytes:
return nonce.to_bytes(32, byteorder="big")Complete Signing Example
TypeScript (ethers.js v6)
import { ethers } from "ethers";
const EIP_712_DOMAIN = {
name: "GXExchange",
version: "1",
chainId: 42069,
verifyingContract: "0x0000000000000000000000000000000000000000",
};
const AGENT_TYPES = {
Agent: [
{ name: "source", type: "string" },
{ name: "connectionId", type: "bytes32" },
],
};
async function signAction(
wallet: ethers.Wallet,
action: any,
nonce: number,
isMainnet: boolean = true
): Promise<{ action: any; nonce: number; signature: any }> {
const connectionId = ethers.zeroPadValue(ethers.toBeHex(nonce), 32);
const message = {
source: isMainnet ? "a" : "b",
connectionId,
};
const signature = await wallet.signTypedData(
EIP_712_DOMAIN,
AGENT_TYPES,
message
);
const { r, s, v } = ethers.Signature.from(signature);
return {
action,
nonce,
signature: { r, s, v },
};
}
// Usage: Place a limit buy on BTC
async function placeOrder(wallet: ethers.Wallet) {
const action = {
type: "order",
orders: [{
a: 0, // BTC index
b: true, // buy
p: "67000.0",
s: "0.01",
r: false,
t: { limit: { tif: "Gtc" } },
}],
grouping: "na",
};
const nonce = Date.now();
const signed = await signAction(wallet, action, nonce);
const response = await fetch("https://api.gx.exchange/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(signed),
});
return response.json();
}Python (eth_account)
import time
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data
EIP_712_DOMAIN = {
"name": "GXExchange",
"version": "1",
"chainId": 42069,
"verifyingContract": "0x0000000000000000000000000000000000000000",
}
AGENT_TYPES = {
"Agent": [
{"name": "source", "type": "string"},
{"name": "connectionId", "type": "bytes32"},
]
}
def sign_action(account, action: dict, nonce: int, is_mainnet: bool = True) -> dict:
connection_id = nonce.to_bytes(32, byteorder="big")
message = {
"source": "a" if is_mainnet else "b",
"connectionId": connection_id,
}
# Build EIP-712 structured data
typed_data = {
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
],
**AGENT_TYPES,
},
"primaryType": "Agent",
"domain": EIP_712_DOMAIN,
"message": message,
}
encoded = encode_typed_data(full_message=typed_data)
signed = account.sign_message(encoded)
return {
"action": action,
"nonce": nonce,
"signature": {
"r": hex(signed.r),
"s": hex(signed.s),
"v": signed.v,
},
}
# Usage
account = Account.from_key("0xYourPrivateKey...")
action = {
"type": "order",
"orders": [{
"a": 0,
"b": True,
"p": "67000.0",
"s": "0.01",
"r": False,
"t": {"limit": {"tif": "Gtc"}},
}],
"grouping": "na",
}
nonce = int(time.time() * 1000)
signed = sign_action(account, action, nonce)
resp = requests.post("https://api.gx.exchange/exchange", json=signed)
print(resp.json())Signing with an Agent Wallet
When using an API wallet (agent), the signing process is identical except:
- You sign with the agent’s private key instead of the main wallet key.
- You include
vaultAddressin the request to specify which main address the agent acts for.
const signed = await signAction(agentWallet, action, nonce);
const response = await fetch("https://api.gx.exchange/exchange", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...signed,
vaultAddress: "0xMainWalletAddress...",
}),
});Approve Agent Signing
The approveAgent action has its own signing flow since it authorizes a new signer:
const approveAction = {
type: "approveAgent",
agentAddress: agentWallet.address,
agentName: "Trading Bot",
nonce: Date.now(),
};
// This MUST be signed with the main wallet
const signed = await signAction(mainWallet, approveAction, Date.now());Verification
The server verifies signatures by:
- Recovering the signer address from the EIP-712 signature
- Checking if the signer matches the expected address (or an authorized agent)
- Validating the nonce is within the 60-second window
- Ensuring the nonce has not been used before
Common Mistakes
| Issue | Cause | Fix |
|---|---|---|
Invalid signature | Wrong chain ID in domain | Use chainId: 42069 |
Invalid signature | Wrong source string | Use "a" for mainnet, "b" for testnet |
Invalid signature | Incorrect connectionId derivation | Zero-pad nonce to 32 bytes |
Nonce too old | Clock skew | Synchronize via NTP |
Agent not authorized | Agent not approved | Submit approveAgent first with main wallet |